Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 06/06/2024 in all areas

  1. Welcome to the ProcessWire forums! This is a great question and I think ProcessWire is a great platform to begin transitioning into OOP because ProcessWire itself is object oriented and is built using OOP. It includes powerful tools and features that can help make your code cleaner, more efficient, and reusable. I recommend starting with custom Page classes. Custom page classes lets you use OOP principles to extend ProcessWire and add additional custom behaviors by thinking with objects. There are a couple of examples in that link, but I'll provide one here that specifically contrasts different methods of doing the same thing. This example is a real-world case that I use on many projects, and because of how it's written I can replicate this feature easily when I start new projects. This is just a simple example of code I use that hopefully opens the door to thinking in OOP when working with ProcessWire. On blog posts I like to add something that shows how long it will take to read it, a la "5 minute read" like articles on Medium do. I wrote code that implements Medium's own method of calculating read time and use it often. The code that calculates the reading time is real, but I threw this together for illustration so please excuse any errors. Lets assume that you have a template called blog-post.php where you calculate reading time and output that value to the page. Here is what that looks like using procedural code: <?php namespace ProcessWire; // site/templates/blog-post.php // Calculate the read time for this article by using the words contained in the title, summary, and // body fields $text = "{$page->title} {$page->summary} {$page->blog_body}"; $blogText = explode( ' ', $sanitizer->chars($text, '[alpha][digit] ')); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // Account for images in text and add to read time // Starts at 12 seconds for first img, 11, 10, 9, etc. until floor of 3 secs $secondsPerImage = 12; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } // Word count divided by average adult reading speed per minute with image read time added $readTime = (int) (ceil($wordCount / 275) + ceil($imageReadTimeInSeconds / 60)); ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $readTime; ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> So, there's nothing wrong with that- gets the job done! But it could be better... It adds a lot of logic to our template and makes it harder to read, imagine if we had to add more logic for other features Mixing raw PHP and HTML works, but can be confusing when it comes to managing and maintaining our code We can't reuse the the code that calculates reading time, if we wrote this in another place then we have to make sure both are bug free and accurate Of course, we could create a function called readTime() that does the same thing and cleans up the template. But now we are writing functions that do a specific thing but exist without context and are harder to organize and maintain flexibility. Luckily, there's a better way. I'll let the notes by Ryan in that link I shared above explain how to start using custom Page classes, so I'll assume you have that set up. So now lets think in objects and use OOP to improve our code. Now we have our template, blog-post.php, and a custom Page class called BlogPostPage.php. Lets refactor. Here's our BlogPostPage.php file: <?php namespace ProcessWire; // site/classes/BlogPostPage.php class BlogPostPage extends Page { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; public function readTime(): int { $text = "{$this->title} {$this->summary} {$this->blog_body}"; $blogText = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Now that our code is in the context of a class method, we've made a couple of extra changes: $page->{name of field} is now $this->{name of field} because we're now working within BlogPostPage which extends the Page class itself. We've added constants to define values that would otherwise be a little difficult to understand at first glance. Seeing self::WORDS_READ_PER_MINUTE is clear and self-documenting where only using the integer 275 in place doesn't really state what that number means We switched $sanitizer to wire('sanitizer') because the $sanitizer variable does not exist in the method scope and the wire() function makes the entire ProcessWire API available to us both inside and outside classes ProcessWire will use BlogPostPage class to create the $page object we use in our templates when it boots, executes our code, and renders content via our templates. Thanks to OOP inheritance, BlogPostPage has all of the methods and properties available in the Page class and can be used in our templates with the $page object. And now let's go back to our blog-post.php template: <?php namespace ProcessWire; // site/templates/blog-post.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $page->readTime(); ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> Now we're talking. With a little extra code and some OOP we've created a method on the Page object. Some benefits: Our template is cleaner and easier to maintain We've made the BlogPostPage class extend Page, so it inherits all of the methods and properties you access via $page The $page object is used to output the reading time to the page, just like $page outputs our field content, so our custom behavior is predictable and and feels at home with the core ProcessWire API It's easier to find where your programming logic is and keep a separation of concerns What's even better is that because we have used OOP to extend the Page class and add new functionality, we can use this a lot more places in our templates (so now it's reusable too). Let's say that you want to add a blog feed to the home page that shows the latest 3 blog posts and displays them with their title, read time, summary, and a link to read the post. <?php namespace ProcessWire; // site/templates/home.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <header> <h1><?= $page->headline; ?></h1> </header> <section class="blog-feed"> <!-- Create an <article> preview card for each blog post --> <?php foreach ($pages->get('template=blog-post')->slice(0, 3) as $blogPost): ?> <article> <h2><?= $blogPost->title; ?></h2> <span class="read-time"> <?= $blogPost->readTime(); ?> minute read </span> <div class="post-summary"> <?= $blogPost->summary; ?> </div> <a href="<?= $blogPost->url; ?>">Read More</a> </article> <?php endforeach ?> </section> </body> </html> That would be a lot harder to do if you had to write more procedural code to calculate the read time for each blog post. Thanks to that method in BlogPostPage, we can use it anywhere we reference a blog post. Think we can make this better? Let's improve it using PHP traits. Using a Trait will allow us to reuse our code that calculates reading time in many places thanks to OOP. We'll create another file called CalculatesReadingTime.php and put it in a new folder at /site/classes/traits. Time to refactor, here's our new trait file: <?php namespace ProcessWire; // site/classes/traits/CalculatesReadingTime.php trait CalculatesReadingTime { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; /** * Takes an arbitrary number of field values and calculates the total reading time * @param string $fieldValues Contents of fields to calculate reading time for * @return int Total read time, in minutes */ public function calculateReadingTime(string ...$fieldValues): int { $text = array_reduce( $fieldValues, fn ($content, $fieldValue) => $content = trim("{$content} {$fieldValue}"), '' ); $text = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($text); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$text}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Let's take a look at that before we go back to our other files. Our new trait has a name that is an "action" because now, as we'll see, we can add this ability to other classes so they can calculate reading times We've abstracted our code. Now instead of referring to content as $blogText, we are calling it $text because it can be used in many places and many contexts but provide the same behavior The readTime() method is now called calculateReadingTime() and has been converted to a variadic function so you can pass as many field values as needed We've taken an extra step of type hinting our parameters as strings to make sure the method is always getting the proper type of data to work with. This will help a lot as this method becomes used more in different places Our docblock is more robust to help understand what this method does, what parameters it takes, and what it will return, another extra step to help us as we use this method in more places Now back to our BlogPostPage.php file <?php namespace ProcessWire; // site/classes/BlogPostPage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class BlogPostPage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime($this->title, $this->summary, $this->blog_body); } } Now we're giving our BlogPostPage class the ability to calculate reading time and all of the functionality has been kept the same. We were able to abstract the logic for calculating reading time to a reusable trait that can be included in any Page class Our readTime() method still exists and is available to all of the templates where we were already using $page->readTime() Now our readTime() method calls the calculateReadingTime() method and passes the fields we know we need as they exist in our blog-post template This code is clean, concise, and easy to maintain. Right now it looks like OOP has made things look nice but caused some extra work... but then the phone rings. Your client wants to add Press Releases to the blog section of the site and it's going to need a new layout and different fields, but they love your reading time calculator so much that they want it on the new Press Release pages too. So we create our new files- a press-release.php template, and a PressReleasePage.php file. We'll skip writing out the press-release.php HTML, but while creating the new template in ProcessWire you've created new fields. The new fields are 'pr_abstract', 'pr_body', 'company_information', and 'pr_contact_info'. Here's our new PressReleasePage.php file: <?php namespace ProcessWire; // site/classes/PressReleasePage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class PressReleasePage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime( $this->pr_abstract, $this->pr_body, $this->company_information, $this->pr_contact_info ); } } Since we've created this simple class that uses the CalculatesReadingTime trait, we can use $page->readTime() in all of our Press Release pages, and anywhere that a Press Release $page object is present. Very nice. Now OOP has really shown how useful it is and we can appreciate how ProcessWire uses objects and provides tools for us extend that power with our own code. There's also some other OOP things happening here: Our BlogPostPage and PressReleasePage classes do one thing and one thing only: handle logic and features for their respective pages. So, they have a single responsibility The calculateReadingTime() and readTime() methods do one thing and one thing only, calculate reading time based on content. They only care about one thing and have no side effects. Our BlogPostPage and PressReleasePage clases can both calculate reading time, but other hypothetical page classes, like "HomePage" and "AboutUsPage" aren't required to have a readTime() method that isn't used, thanks to making use of traits to share behavior only where it's needed. So, that's composition over inheritance Our readTime() method does not expose how it calculates reading time and it provides an interface to only expose information we want our object to make available. So, readTime() is read-only and can safely be used knowing that the value will never be overwritten or modified except when content is changed by editing the page. This is a great tool that shows the difference between setting a value to $page->title and getting a value from readTime(), each have their purposes and roles. Our code is modular and easy to maintain. If we had to adjust how reading time was calculated- we could for example adjust the value of WORDS_READ_PER_MINUTE in our CalculatesReadingTime trait. Then all of our Press Releases and Blog Posts would have their reading time correctly calculated with one change. We can also add CalculatesReadingTime to any future page classes that need it. ProcessWire's strong OOP foundation and the way that it uses objects that are created from classes for everything (like $page, $config, $input, etc.) is the reason that the API is easy to work with, enjoyable, and powerful. If you get the hang of working with OOP in ProcessWire you can build even more powerful websites and applications, better understand the ProcessWire core code, and write your own modules (which is actually pretty fun). Wasn't sure of your overall exposure to OOP but hopefully this helps and inspires!
    15 points
  2. Your post should be a blog post here on the ProcessWire blog, be listed as a prime example why PW and its API is so awesome and should be linked on the frontpage under "(Why) Web developers love ProcessWire". ? @FireWire
    6 points
  3. Wow. And the award for the greatest answer of the year goes to...
    6 points
  4. There's so much more to write about ProcessWire in practice. On a personal note, I have to give ProcessWire credit for being a point of education for me years ago as I started to transition to OOP. An example is seeing how the ProcessWire API is structured with fluent methods, I thought that was so cool that I learned how to implement them myself by studying the core source code. ProcessWire grows with you as a developer and it continually gets better as time goes on. Many thanks to @ryan, contributors, and module authors for their inspiration and impact on my skills. Thank you for the kind words! This truly only scratches the surface and, as you can see, OOP is not something you do "in" ProcessWire, it's something you do with ProcessWire should that be your choice, as much or as little as you want, when and where it makes sense. Go forth and build! If you get stuck, there are plenty of friendly and knowledgeable devs here in the forums who are happy to help.
    5 points
  5. Hmm. What's your .htaccess like? Could this be related? Also referenced is the page name whitelist. I wonder if these are kicking in somehow for the slug. # PW-PAGENAME # ----------------------------------------------------------------------------------------------- # 16A. Ensure that the URL follows the name-format specification required by PW # See also directive 16b below, you should choose and use either 16a or 16b. # ----------------------------------------------------------------------------------------------- RewriteCond %{REQUEST_URI} "^/~?[-_.a-zA-Z0-9/]*$" # ----------------------------------------------------------------------------------------------- # 16B. Alternative name-format specification for UTF8 page name support. (O) # If used, comment out section 16a above and uncomment the directive below. If you have updated # your $config->pageNameWhitelist make the characters below consistent with that. # ----------------------------------------------------------------------------------------------- # RewriteCond %{REQUEST_URI} "^/~?[-_./a-zA-Z0-9æåäßöüđжхцчшщюяàáâèéëêěìíïîõòóôøùúûůñçčćďĺľńňŕřšťýžабвгдеёзийклмнопрстуфыэęąśłżź]*$" # END-PW-PAGENAME
    4 points
  6. In case anyone else has this problem, I just wanted to update with a workaround. Simply turn off the update checking at login. In the admin panel, it's Modules -> Configure -> ProcesswireUpgradeCheck Now I have blazingly fast logins again! Obviously, to keep on top of PW updates, you will have to check for updates every so often ( setup -> Upgrades ) I think my shared hosting provider might have some ModSecurity rule that is inadvertently being triggered, or a slow DNS lookup, or something like that. But if you're on shared hosting like I am, you might never be able to find the problem, and even if you found it, the host might not be willing to make any changes. I have updated the title to show that there's a workaround.
    4 points
  7. Ok, so I decided to scratch my own itch on this one... I need to handle WebP's which include transparent regions so I've created a modified version of the @Robin S module (https://processwire.com/modules/webp-to-jpg/) but which allows you to choose between PNG or JPEG as the target format. https://gitlab.com/applab/pw-uploadwebp
    3 points
  8. Great article, @FireWire! I would add that Wireframe is another great way to start working with PW in a more MVC-frameworkish-OOP-kinda-way. So check it out too.
    3 points
  9. Hey everyone, I've been programming in PHP for a long time and know a bit about OOP, like what a class is ?. However, I’ve never written any serious code using OOP. Any advice on how to use ProcessWire to develop my OOP skills? Cheers!
    2 points
  10. Don't you need to sanitize encode (selectorValue) your variables before you use them in your get selector?
    2 points
  11. Everybody using the cool ajax endpoints feature please update to v3.16.1 as it contains an important fix (added missing return statement in public endpoints). @gebeer
    2 points
  12. Hi, just a few words to say don't forget the .htaccess file at the site root that may need to be changed - to force with or without the www - probably to force https too - and, something if nearly always do in the htaccess, to add some headers, pw default ones in the htaccess are <IfModule mod_headers.c> # prevent site from being loaded in an iframe on another site # you will need to remove this one if you want to allow external iframes Header always append X-Frame-Options SAMEORIGIN # To prevent cross site scripting (IE8+ proprietary) Header set X-XSS-Protection "1; mode=block" # Optionally (O) prevent mime-based attacks via content sniffing (IE+Chrome) # Header set X-Content-Type-Options "nosniff" </IfModule> and i often end with something like that <IfModule mod_headers.c> # prevent site from being loaded in an iframe on another site # you will need to remove this one if you want to allow external iframes Header always append X-Frame-Options SAMEORIGIN # To prevent cross site scripting (IE8+ proprietary) Header set X-XSS-Protection "1; mode=block" # Optionally (O) prevent mime-based attacks via content sniffing (IE+Chrome) Header set X-Content-Type-Options "nosniff" Header set Content-Security-Policy "frame-ancestors 'self' https://www.helloasso.com" Header set Access-Control-Allow-Origin "*" Header set Referrer-Policy "no-referrer" Header set Permissions-Policy "geolocation=(), camera=(), microphone=()" Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains" </IfModule> of course, just an exemple as helloasso is just here to say i allow iframes from this url (a french asso donation website) but you may need extra iframes, some of those can be more complex depending on your website needs more infos on those curious things here https://securityheaders.com/ ? have a nice day
    2 points
  13. Added support for PageFrontEdit for the TinyMCE plugin. Now you can save your TinyMCE fields on the frontend via CMD-S (or CTRL-S). Works great in conjunction with RockFrontend and RockPageBuilder. quicksave-frontend.mp4 QuickSave.zip
    2 points
  14. @bernhardTwo things I can spot here: 1. You are calling renderReady() from a hook after Inputfield::render. That is too late. What @adrian mentioned about hooking Inputfield::renderReadyHook() instead would likely enable you to do what you want to do. 2. Your renderReady() method has if(!$grid) return; and you would need to change that to if(!$grid) return parent::renderReady($parent, $renderValueMode);
    1 point
  15. Maybe your hosting company needs that whiskey to unlock it faster. Solution found. That's perfect.
    1 point
  16. Yes @wbmnfktr (now I count bottles of whiskey I own you), I am on my mobile, waited to come home to sit down by the computer to post here, but you posted before me. In the end, I found out it was my server blocking the outgoing SMTP connection. The module is working great! Still waiting the host to unlock it.
    1 point
  17. I found these settings for ZOHO - and they use a different server: Outgoing Server Name: smtp.zoho.eu Port: 465 Security Type: SSL Require Authentication: Yes. https://www.zoho.com/mail/help/zoho-smtp.html
    1 point
  18. 1 point
  19. sorry, hi again ? just to say, in your config file, don't forget this line $config->httpHosts = array('www.yourdomain.com'); as it's important for a lot of things, beginning with preview pages clicking on wiew buttons in the admin have a nice day
    1 point
  20. Hello, Sorry if it was not clear, I think it wasn't also in mind. After thinking again about it, I just switched everything to a RepeaterMatrix and it becomes way easier. Thanks, Mel PS but to answer questions : there is only one page (a basic-page) who should listed all members. And I realized I shouldn't use at all member template, since I don't want individual page for each member.
    1 point
  21. Hi. I am using two site config files. The site/config-dev.php holds the DB settings and debug settings for my local site, the site/config.php holds all the settings for the live site. PW checks for presence of a config-dev.php and uses this file instead of default site/config.php if it exists. On my local dev setup, I do have a config-dev.php file next to my site/config.php file. On the live server I just upload the site/config.php or delete/rename the site/config-dev.php via FTP. Then I just ZIP my entire local setup once I am finished, upload the ZIP via FTP into the root of my live server together with an PHP unzip file and then visit the URL of the unzip script on my live server in the browser. The unzip scripts extracts all the files, sets right permissions if needed and deletes the ZIP folder and the unzip script itself once done. The local DB dump is done via DDEV DB export feature and then just loaded to the live server via phpMyAdmin or whatever DB tools the hosting company offers. This is done before I upload the ZIP file to my live server. For some extra safety, I create a dump of the live DB and rename the live site/ and wire/ folders to site.bak/ and wire.bak/ via Ftp before doing the update. This way I can easily revert back to the last working live state if needed - never needed for my PW sites so far, but happened in the past with some WP sites I tried to update.
    1 point
  22. I do it essentially the same as what @TomPich said, but on the first move from local to remote I find it's a lot faster and more reliable to compress all the website files to a ZIP archive, upload that to the remote server, then extract it on the server. If you're using cPanel then the included File Manager is a convenient way to upload and extract. And when using a host that doesn't include a file manager I like to use TinyFileManager, although you need to take due care with security - as extra protection I rename the containing folder to include a dot prefix to prevent access when I'm not actively using it.
    1 point
  23. Hello @dan222 and welcome to this forum! To push a site online : go to your db manager on laragon and export your db as a sql file then, from your hosting control panel, create a db (or empty it if it already exists) and import your local db with FTP, copy your local files in your hosting folder for your site (don’t forget to change the database config in /site/config.php file) And that’s all. ? Now, you can be a bit more efficient by using an "if" statement for your database config, so that you don’t have to worry about changing them when you push your site online (see below) using ssh/rsync to synchronize your local and distant files I’m sure you can optimize database export/import too, I didn’t dig in yet... // db config online / on localhost if ( $_SERVER["HTTP_HOST"] === "my-online-url.com") { $config->dbHost = '...'; $config->dbName = '...'; $config->dbUser = '...'; $config->dbPass = '...'; } else { $config->dbHost = 'localhost'; $config->dbName = 'my-local-dbname'; $config->dbUser = 'root'; $config->dbPass = ''; } $config->dbPort = '3306'; I generally need 4 minutes to pull or push a website after or before working on it, if it involves db update. If it’s only changes in files, it’s done in 10-15 seconds.
    1 point
  24. Does it work as expected with renderReadyHook() as per the docs?
    1 point
×
×
  • Create New...