Jump to content

FireWire

Members
  • Posts

    474
  • Joined

  • Last visited

  • Days Won

    31

Everything posted by FireWire

  1. I meant to mention that I didn't know about that method before your comment. Now that I know about it though I'll probably end up using it. So thank you for the tip! There was one instance where it may have come in handy. I created a PageClass method that analyzes the current page request to determine if it was an AJAX request for a specific purpose, but decided to just call $page->whateverMethdName() at the top of the template before anything rendered. The loaded() method is the way I'll go with that next time. I learned something new today ? Bookmarking this. Keeping hooks closer to the location/context that makes sense is a good idea.
  2. Well, funny story, I have no reasoning. I just misremembered between reading that and hacking out this post. Jumping between projects in and outside of ProcessWire didn't help haha. I've used DefaultPage in the past, it was just an oversight this time around. Ryan's is of course the correct way. I generally always have a custom page class for each of my templates so I've never had an issue where DefaultPage was loaded on it's own. Appreciate you mentioning it, I've updated the post to use DefaultPage. My first reaction is that unexpected behaviors may indicate that there may be too much abstraction. My DefaultPage classes have traditionally been lightweight and stick to only containing what can be reliably counted on as valuable across all pages. One of the criticisms of OOP is that classes and inheritance can make for brittle code so it's something that I try to be mindful of. It may also be good to try an keep anything that executes automatically or hooks into the Page lifecycle to a minimum (maybe none at all) in DefaultPage since it spans the entire application. I use names like these myself for the same reason. Really helps keep a sane number of fields and reduce redundancy. I haven't taken this approach to reassigning values to properties like this but it's an interesting idea. I reviewed the code for the Page class and there is a good amount of logic in Page::set() that can be useful when working with values but may not be needed within the context of a page class. At first blush though I would just assign the value of the field to an instance variable directly rather than call Page::set() because at time of authorship- you know what field you'll be working with, it's data type, and intended usage within the context of that template/page. <?php public $introTitle; public $introText; // set custom properties in loaded public function loaded() { // load custom properties only for allowed statuses if (!empty(array_intersect(array_keys($this->status(true)), self::excludeStatuses))) return; $this->introTitle = $this->text; $this->introText = nl2br($this->textarea); } I may be missing the added value of using Page::set() in your use case though. My other thought though is that if these fields have consistent and wide enough usage through all templates, enough to reassign them to alternate property names, they might be a candidate for dedicated fields rather than repurposing generalized fields. Both introTitle and introText seem like they have wide enough usage, so I would create those fields and skip any extra logic needed here in DefaultPage. I actually haven't set a lot of instance variables in page classes. I've focused on methods that can abstract logic out of templates, or perform computationally expensive operations only on demand. Here's a DefaultPage from a production site: <?php namespace ProcessWire; use Ramsey\Uuid\Uuid as Uuid; use \DateTime; class DefaultPage extends Page { /** * Paths for rendering methods */ const COMPONENTS_PATH = '/../templates/components/'; const FORMS_PATH = '/../templates/components/forms/'; const PARTIALS_PATH = '/../templates/partials/'; const SVG_PATH = '/../templates/img/svg/'; /** * Stub method that may be overridded by inheriting classes where needed */ public function renderStructuredData(): ?string { return null; } /** * Outputs full path and filename for CSS files */ public function css(string $file): string { return wire('config')->urls->templates . "dist/styles/{$file}"; } /** * Outputs full path and filename for JavaScript files */ public function js(string $file): string { return wire('config')->urls->templates . "dist/scripts/{$file}"; } /** * Returns contents of an SVG file with unique IDs for title/description attributes for accessibility */ final public function renderSvg(string $file, array $variables = []): string { $variables = array_merge($variables, [ '{ARIA_TITLE_ID}' => Uuid::uuid4()->toString(), '{ARIA_DESC_ID}' => Uuid::uuid4()->toString(), '{CLASSES}' => $variables['classes'] ?? null, ]); return strtr(file_get_contents(__DIR__ . self::SVG_PATH . $file), $variables); } /** * Renders a file in /templates/partials/ */ final public function renderPartial(string $file, array $variables = []): string { return wire('files')->render($file, $variables, [ 'defaultPath' => __DIR__ . self::PARTIALS_PATH ]); } /** * Renders file located in templates/components/ */ final public function renderComponent(string $file, array $variables = []): string { return wire('files')->render($file, $variables, [ 'defaultPath' => __DIR__ . self::COMPONENTS_PATH ]); } /** * Renders a form */ final public function renderForm(string $file, array $variables = []): string { return wire('files')->render($file, $variables, [ 'defaultPath' => __DIR__ . self::FORMS_PATH ]); } /** * Gets the time period of the day based on the hour */ final public function getPeriodOfDay(): string { $hour = (new DateTime())->format('H'); switch (true) { case $hour < 12: return 'morning'; case $hour >= 12 && $hour <= 17: return 'afternoon'; default: return 'evening'; } } /** * Returns all top level pages for the main navigation */ final public function topLevelPages(): PageArray { $pages = wire('pages'); $selector = 'parent=1'; $excludedIds = $pages->get('template=admin_website_settings') ->nav_main_excluded_pages ->explode('id'); // Check for excluded pages from nav defined in the "Website Settings" page count($excludedIds) && $selector .= ',id!=' . implode('|', $excludedIds); $homePage = $pages->get(1); $topLevelPages = $pages->find($selector); $topLevelPages->prepend($homePage); return $topLevelPages; } } All of the other magic goes into traits or inheriting page classes. My soft guidelines when it comes to code organization is: Save abstraction until something is repeated 3 times, consistency should be maintained, or complexity warrant it for the sake of the surrounding code. Abstract to a trait or enum that can be selectively used where needed. Consider abstracting to a parent class last, there has to be a case for why it should be shared between all classes because it's more than ubiquity, it comes down to rock solid shared behavior and purpose
  3. This is incredibly useful and could not come at a better time!
  4. I'd be happy to provide something like that. I'm really short on free time right now but I may be able to adapt a real life example once I get enough done on one of my projects. That way I can avoid double work and have a good example. Will keep it in mind and bring that info back here as a demo repository when I can do it right. I've implemented the exact same method on sites before and that's a great example. I like to create a jsonld() method which returns null in my BasePage class that exists specifically to be overwritten by inheriting pages. That way I can call $page->jsonld() in a partial like site_header.php without any logic, it renders something or it doesn't.
  5. I second this wholeheartedly. It's truly too much to cover in a comment, but this is what I mean when I said "thinking with objects". It requires starting to work with encapsulation and breaking down/dividing your code into behavioral chunks. I'm pretty sure at some point I read the book that @da² recommended. The Head First series really does explain things in ways that I have seldom seen elsewhere. Also +1 for real life books, I find that they let me "unplug" from the problem at hand and focus on the content presented. In my closing list of items I mentioned "single responsibility", "composition over inheritance", "does not expose how it calculates", and an example of how properties and methods can each have their purposes and roles. You're going to immediately be introduced to these fundamental concepts as soon as you start down the OOP path, I peppered them in to contextualize them in ProcessWire. I've referenced this site before and it's a good next step. You'll write OOP code and sometimes think "there's got to be a better way to do this", and these are the abstract concepts that you'll be able to take into consideration. Just be mindful of "premature optimization" and get comfortable with OOP to the point where before you even start coding, your mental model will naturally begin with OOP. Approach LLMs with caution while learning. They require that you ask a question the right way and may not tell you why what you asked maybe isn't the right question to ask in the way a human can. You also have to be familiar enough with the concepts to recognize when they give you the wrong answer. LLMs require large scale data ingestion and that means their models are indiscriminately built with code examples whether they're good or bad. You'll get there but they're wrong, a lot, so maybe consider this one a little further down the road. Just my two cents.
  6. Very happy to hear you enjoyed it!. Writing a course on anything hasn't ever crossed my mind haha. I just put this together to share a great ProcessWire feature that has really boosted my workflow and thought maybe sharing some examples might be useful to others. I hadn't thought about this post as a walkthrough on OOP specifically, but I did want to be descriptive in a way that would be useful to as many people as possible- whether you are familiar with OOP or not, and without any assumptions. All said, I'm happy to try and answer questions if it would help anyone ?
  7. If anyone's interested, I posted a deeper dive into page classes with more real world examples. I like page classes so much I had to write a love letter.
  8. Page classes are an outstanding feature of ProcessWire and probably ranks among my favorites overall (and that's really saying something with ProcessWire). I don't remember how I lived without them, well I do, but I don't like to think about it. I wrote a response to a question asking how ProcessWire can help transition from procedural code to OOP and in the process (pardon the pun) of answering, I realized how much I've come to use, if not outright rely on, page classes in every project. I wanted to compile a few more thoughts and examples here because there may be devs who are finding this feature for the first time, and besides, why not go down the rabbit hole? Page classes have been around since ProcessWire 3.0.152 and if you're not familiar this will all make a lot more sense if you do a quick read of the feature announcement by Ryan here because this post assumes familiarity. We're also going to pick up the pace between the examples below compared to my comment in the link above. Here are a few goals that I have that page classes put solutions for within reach. You may have some of your own and I'd love to hear about those as well. Keep templates clean by restricting logic to flow operations- if statements and loops. Work with data in context, accessing data and fields closer to their source. Create "universal" methods available in every template, but also have scoped methods per-template Increase the scalability of a project, growth with limited increases in complexity. Stay DRY Embrace and extend the power of OOP in ProcessWire's DNA This seems like quite a list for one feature to handle, but rest assured, this is an example of how much power comes with using page classes. I'll continue and build on the blog example from my linked response above, and you can "follow along" with the Blog module since the templates mentioned are present out of the box. Some of the examples below are taken from production code, but please excuse any errors that I may have introduced by accident and I'm happy to update this post with corrections. Some of these things can be done other ways but they're just to illustrate, replace with your ideas and think of times that this would be useful. First up, what are some features and behaviors that everyone needs on every project? What are some universal methods that would be great to have everywhere? Let's start out by creating a "base" page class called DefaultPage. Going forward, page classes will extend this class instead of Page and benefit from having access to universal methods and properties. EDIT: I initially wrote this using BasePage rather than DefaultPage for this class name as described by the custom page class writeup by Ryan in the article above. I've changed this to now use the correct DefaultPage class name. <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { /** * Returns all top level pages for the main navigation. */ public function navigationPages(): PageArray { $pages = wire('pages'); $selector = 'parent=1'; $excludedIds = $pages->get('template=website_settings') ->nav_main_excluded_pages ->explode('id'); // Check for excluded pages from nav defined in the "Website Settings" page count($excludedIds) && $selector .= ',id!=' . implode('|', $excludedIds); $homePage = $pages->get('id=1'); $topLevelPages = $pages->find($selector); $topLevelPages->prepend($homePage); return $topLevelPages; } } All your base page are belong to us. Right off the bat we've managed to pull complex logic usually in templates and kept our markup clean. This isn't for DRY, it's to store logic out of templates. This simple example only illustrates working with a top level page nav. We can start to appreciate the simplicity when considering how much more the navigation may call for in the future. A navigationChildren method that also accounts for excluded pages is a prime example. In our markup: <!-- /site/templates/components/site_nav.php --> <nav> <ul> <?php foreach ($page->navigationPages() as $navPage): ?> <li> <a href="<?= $navPage->url; ?>"><?= $navPage->title; ?></a> </li> <?php endforeach ?> </ul> </nav> Next up, our settings page has newsletter signup fields where users can copy/paste form embed code from Mailchimp. Our website has a "settings" page where global values and fields can be edited and maintained There are multiple textarea fields on the settings page that can each contain a different mailchimp embed code There is an embed select field that can be added to templates. The values are textarea field names so an embed can be chosen by the user. An embed select field has been added to the settings page which allows for choosing a default embed code if one isn't selected when editing a page Embedded forms should be available anywhere on any template <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->signup_embed_select ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } Excellent. We've got some solid logic that otherwise wouldn't have a great home to live in without page classes. We can also modify this one method if there are additional options or complexity added to the admin pages in the future. Wherever we use renderEmailSignup, the return value will be either the selected or default form embed code. Let's create a folder called "components" in our templates directory that will hold standalone reusable markup we can use wherever needed, here's our newsletter signup component: <!-- /site/templates/components/newsletter_signup.php --> <div class="newsletter-signup"> <?= $page->renderEmailSignup(); ?> </div> Great! We've kept logic out of our code. We can render the field, and we can account for an empty value with a fallback. Unfortunately this is pretty limited in that it only handles a specific field, and it's implementation isn't as flexible as a component should be. We can fix that, and here are some new requirements to boot: Some templates may have multiple embed select fields which may or may not have a value Each embed select field is now paired up with a text field to add some content that appears with each form embed, think "Sign up for our newsletter today!" We want to render our mailchimp form embeds using the same component file, but handle different fields Sounds like a tall order, but it's a challenge easily overcome with page classes. We're going to introduce a new method called renderComponent that will be global, reusable, and very flexible. We're also going to make use of another very great ProcessWire feature to do it. Back to our DefaultPage class: <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders a file located in /site/templates/components/ with optional variables */ final public function renderComponent(string $file, ...$variables): string { $componentsPath = wire('config')->paths->templates . 'components/'; return wire('files')->render($file, $variables, ['defaultPath' => $componentsPath]); } /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(?string $embedSelectField = null): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->$embedSelectField ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } We've done a couple of things here. We've updated renderEmailSignup to accept a field name, so now we're flexible on exactly which field select we'd like to check for a value on before falling back to default. We've also created a renderComponent method that is going to be super useful throughout the rest of our ProcessWire application. Our renderComponent receives a file name located in the components directory and any number of named parameters. You could change the $variables parameter to an array if you'd like, but I'm a big fan of the great features we have now in PHP 8+. Here's our refactored component file: <!-- /site/templates/components/newsletter_signup.php --> <?php if ($embedField): ?> <div class="newsletter-signup"> <?php if ($text): ?> <h2><?= $text; ?></h2> <?php endif ?> <?= $page->renderEmailSignup($embedField); ?> </div> <?php endif ?> And let's hop over to our (abbreviated) home page template: <body> <!-- ...Sections full of great content --> <section class="signup-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select, text: $page->text_1); ?> </section> <!-- ...Your awesome template design --> <section class="page-end-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select_2, text: $page->text_2); ?> </section> <!-- Your footer here --> </body> I don't know about you, but this is looking really good to me. The number of things we've accomplished while having written so little code is remarkable: Because we used wire('files')->render(), the entire ProcessWire API is available within the component, so now our renderEmailSignup method is too. The variadic function parameters (or array if preferred) let us pass an arbitrary number of variables to any component file, unrestricted future flexibility Variables are scoped to each component! There's no reference to template fields in our component that could break if changes are made No more PHP includes, we don't have to juggle paths or constantly repeat them in our code, nor rely on declaring variables before including a file. ProcessWire will throw an exception if we try to render a file that does not exist which makes locating issues very easy We'll also see an exception if we try to reference a variable in our component that wasn't passed which can also help troubleshooting. Notice that the renderComponent is final. We want that behavior to remain consistent everywhere we use it and not overwritten either intentionally or by accident on our inheriting page classes. We want to eliminate any confusion between templates by knowing it will always do the same thing the same way. We can explore other uses too, perhaps a renderPartial for files in /site/templates/partials where we store files like site_header.php. As mentioned above however, if a variable is expected in the rendered file but not included in our render method, we'll see an exception. Let's use site_header.php as an example because we're sure to run into situations where variables may or may not exist: <?php namespace ProcessWire; // /site/templates/partials/site_header.php ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= $page->title; ?></title> <meta name="description" content="<?= $metaDescription ?? null; ?>"> <?php if ($includeAnalytics ?? true): ?> <script> // All that Google Analytics jazz </script> <?php endif ?> </head> <body class="<?= $bodyClasses ?? null; ?>"> <header> <?= $page->renderComponent('site_nav.php', includeEmailButton: true); ?> </header> Problem: solved. By using nullsafe ?? operators, we can call $page->renderPartial('site_header', description: 'The ultimate Spice Girls fan page.'); and never get errors for variables that may not be included when calling renderComponent, such as includeAnalytics, which now also has a default value of 'true'. Nice. We haven't even gotten to our actual page classes yet... Our templates are about to receive superpowers. Let's take our blog to the next level. In my comment on the other thread, I created a specific example of adding a readTime method to our blog posts, let's go one level higher to our blog.php template. We'll populate some methods up front and then talk about what we've done: <?php namespace ProcessWire; // /site/classes/BlogPage.php class BlogPage extends DefaultPage { /** * Get latest blog posts, optionally with/without pinned post, optionally in blog category */ public function latestPosts( int $limit = 3, bool $includePinnedPost = true, ?int $categoryId = null ): PageArray { $selector = 'template=blog-post'; if ($categoryId) { $selector .= ",blog_category={$categoryId}"; } $posts = wire('pages')->get($selector); if ($includePinnedPost) { $pinnedPost = $this->getPinnedPost(); if ($pinnedPost) { $posts->remove($pinnedPost); $posts->prepend($pinnedPost); } } return $posts->slice(0, $limit); } /** * Gets an optional pinned post if set/chosen * pin_blog_post A checkbox to indicate whether a post should be pinned * pinned_post A InputfieldPage field to choose a blog post */ public function getPinnedPost(): ?Page { if (!$this->pin_blog_post) { return null; } return wire('pages')->get($this->pinned_post); } } On our main blog page a user has the ability to choose whether a blog post is "pinned". A pinned post will always remain the first post anywhere a list of posts is needed, something like a big company announcement that the client wants to keep visible. These two methods alone have given us awesome abilities. For our main blog page, when someone visits our blog page, the most recent or pinned post is presented at the top, followed by the next two most recent posts, followed by two rows of 3 posts, for a total of 9. Let's assume that we've already created the BlogPostPage.php with the readTime method from my previous example. Here's our blog.php template <?php namespace ProcessWire; echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-page', metaDescription: $page->blog_description ); $posts = $page->latestPosts(9); $firstPost = $blogPosts->first(); ?> <section class="main-post"> <article> <img src="<?= $firstPost->blog_image->url; ?>" alt="<?= $firstPost->blog_image->description; ?>"> <h1><?= $firstPost->title; ?></h1> <?= $firstPost->summary; ?> <span><?= $firstPost->readTime(); ?></span> <a href="<?= $firstPost->url; ?>">Read More</a> </article> </section> <section class="recent-Posts"> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(1)); ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(2)); ?> </section> <section class="past-posts"> <?php foreach ($posts->slice(3, 6) as $post): ?> <?= $page->renderComponent('blog-preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> So first off, we've really started to use our renderComponents method and component files. We also implemented a renderPartial as speculated upon above. Each does a similar thing, but having separate methods makes everything clear, handles paths, but still has a similar interface when calling them. A big thing to notice here is that at no point have we added any markup to our page classes, and no business logic to our templates. If we need to find anything, we know where to look just by glancing at the template. Ultimate maintainability. Here's our blog_preview_card.php component: <?php namespace ProcessWire; ?> <!-- /site/templates/components/blog_preview_card.php --> <article class="blog-preview-card card"> <img src="<?= $blogPost->blog_image->url; ?>" alt="<?= $blogPost->blog_image->description; ?>"> <h2><?= $blogPost->title; ?></h2> <div class="blog-summary"> <?= $blogPost->blog_summary; ?> </div> <span class="blog-read-time"><?= $blogPost->readTime(); ?></span> <a href="<?= $blogPost->url; ?>">Read More</a> </article> I am liking how well this is working out! Page classes have done a ton of heavy lifting here: We're using our renderComponent method to it's maximum potential and it's payed off in spades Our template couldn't be cleaner or more easily maintainable BlogPostPage.php has taken care of all of our needs as far as delivering the PageArray of posts and all our template does is output the data as needed Our "card" component will render the same thing everywhere and we can update how that looks globally with changes to one file If you don't think this can get more awesome, or think this post is already too long, I have bad news for you and you should stop reading now. Still here? Let's create a BlogPostPage class and add a method: <?php namespace ProcessWire; // /site/classes/BlogPostPage.php class BlogPostPage extends DefaultPage { // ... Other page methods, like readTime() public function relatedPosts(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts( limit: $limit, includePinnedPost: false, categoryId: $this->blog_category?->id ); } } The BlogPage::latestPosts method is really flexing it's muscle here. We've used it in two different places for different purposes but requesting similar data, blog posts. If you noticed, we're also specifying a category for this blog since we have a page select field that references a blog category. That was a parameter that we included back in our BlogPage::latestPosts() method. So, blog posts have a "You might also be interested in..." section with posts in the same category as the that one that a visitor has just read. With page classes this couldn't be easier to add, so let's use the relatedPosts method we just created in BlogPostPage.php in our blog-post.php template: <?php namespace ProcessWire; // /site/templates/blog-post.php echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-post', metaDescription: $sanitizer->truncate($page->blog_summary, 160) ); ?> <!-- The page hero, blog content, stuff, etc. --> <section class="related-posts"> <?php foreach ($page->relatedPosts() as $post): ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> We've just added a related posts feature in *checks stopwatch* seconds. This project is going so fast that you can already hear the crack of the cold beer after a job well done. One more example, and I'll let you decide if this is too much, or feels just right. We want a blog feed on our home page. So before we go to the home.php template, let's do what we do (surprise, it's a page class): <?php namespace ProcessWire; // /site/classes/HomePage.php class HomePage extends DefaultPage { // Other HomePage methods... public function blogFeed(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts(limit: $limit); } } At first glance, this might seem like a bit much. Why create a method in our page class that is so short and simple? We could just call $pages->get('template=blog')->latestPosts(3) inside our template and get away with it just fine. Here's why I think it's worth creating a dedicated page class method: This creates yet another example of predictability between templates It promotes thie philosophy I like of page classes talking to page classes It's likely that we'll have other more complex methods in the HomePage class, keeping them together feels right and helps with uniformity- we always know where to look when seeing custom methods If we need to make a change to the BlogPage::latestPosts() method that affect how other page classes call that method, we don't have to root around in our templates to make any changes. It's pretty nice to see that thus far we have only ever referenced the $page object in our templates! There aren't any calls to $pages because our template is really about doing one thing- rendering the current page without caring about other pages. That's not to say that we can't, or shouldn't, use other ProcessWire objects in our templates, but it's still impressive how much we've been able to scope the data that we're working with in each template. I truly love the page classes feature in ProcessWire and even though this is a deeper dive than my other post, this really is still just scratching the surface because we can imagine having more complex behavior and other benefits. Here are some from my experience: Handling AJAX calls to the same page. Choosing how and what type of content is returned when the page is loaded is really nice. This can be done with hooks, but I really like page classes handling things like this in-context. Working with custom sessions between one or more pages. Creating a trait that shares session behavior between page classes is a great way to extend functionality while keeping it scoped. Adding external API calls and whatever complexity that may entail Provides the ability to add significant complexity to our templates in the admin yet little to no additional complexity in template files. The basic "pinned post" and form embed features are just the start As you can imagine, this makes replicating code you use often between projects trivial Page classes provide a home for logic that otherwise would be stuck in a template, or relegated to a messy functions.php file. No more functions.php ever. We met every single goal I listed here at the beginning Every single template thus far only uses if statements and loops DefaultPage could have a "bootApplication" method if needed that does things for every page and is called in ready.php. This is also a great way to create a "bootable" method in all of your page classes where bootApplication could call a bootPage method in your page classes if it exists. Thanks for coming to my TED talk. Hope you find this useful! If you have any questions, corrections, use cases, or things to add, the comments below are open and I'd love to hear them!
  9. That sounds pretty great. This is probably the most annoying part, all of that is rendered on the client side in JS. I wish I rendered it client side by hooking into the render methods for the inputfields, but I didn't think about that at the time. So the challenge would be waiting until the translation buttons are rendered first, which is pretty fast because it doesn't involve any requests to initialize. The only delay is for the js file itself to load and the elements to be appended in the DOM. I really want to make some internal improvements to Fluency, especially as nifty things like this come about so the module can be more easily worked with and extended. If I can be of any help or if there's any questions you have while slogging through my code, I'm here to help haha. PRs are open if you end up finding some things that can be improved anyway. Looking forward to seeing what you come up with!
  10. @Martin1 Great idea! Thinking out loud, I'm considering a couple of things by way of how to implement. At it's core, Fluency is intended to provide like-kind behavior that is similar enough between current and future translation services/engines so that the module overall doesn't stray into having over-specificity for functionality. Like having general module methods that reach too far into features or behavior of specific engines. So my first thought is an add-on module to supplement DeepL's abilities- but this is open source so contributors should definitely have input that can make it better. Some thoughts below on addressing the issues you listed- I think that Google Cloud translate may have a similar feature but it would come down to how well the behavior could be abstracted to provide a common enough interface at the engine level where a common Data Transfer Object, and thus a feature interface, can be shared. Final classes- the DataTransfer Objects are definitely a "hard" final class where each should inherit from the base abstract DTO. There is some extra flexibility built in with the ability to add a 'meta' property that can hold some engine-specific behavior, like "formality" which DeepL supports but others like Google don't. Other instances of removing final are probably on the table, I'm not sure I should have made the Fluency module class final. A glossary would definitely warrant it's own DTO in the same spirit as the EngineApiUsageData, EngineTranslationData, EngineLanguagesData, etc. represent data passed between domains for individual features. Otherwise it's probably just a matter of following the Principle of Least Privilege. Unhookable methods- I don't good excuse for this one. Basically left out because hookable methods don't allow for named arguments. This is 100% open to change and was pretty short sighted on my part given that this is a module after all... I should just push a release that does adds hooks regardless. Private methods- This pretty much just follows the Principle of Least Privilege again and, like the final classes notes above, many are open for changing that. All said, I'm kind of open here as far as integrating it into Fluency or opening up class extension, public-ifying methods, and adding hooks. All said, is there one approach that feels better to you?
  11. @gornycreative I've uploaded a new version of the module to the dev branch in the repository and it would be great if you can take it for a spin. The UI is fixed so that the field renders with the correct styling. Working with the values introduced some new opportunities and I took some notes from the feedback on the FormBuilderHtmx module on return values. The new rendering options are: Form ID or null when no form is selected (int) Form name or null when no form is selected FormBuilder form object or null when no form is selected Adding the ability to work with the FormBuilder object directly affords some extra utility, it's a direct passthrough in that FieldtypeFormSelect retrieves and returns the form that $forms would have had you done that manually. This was in lieu of using a Page select field since Pages and Forms are retrieved differently. Examples are included on form config pages. I kept the Form ID option as default to help prevent a breaking change, but I haven't directly tested this version against the last version so let me know if you run into any issues. Download from dev, I'll push a new version if everything works out well.
  12. @netcarver Figured you may be familiar but added some recommendations here for the Linux curious!
  13. @gornycreative Sorry for the delay, have been buried with work. I'm a fan of strict typing and comparisons wherever possible, I'd be inclined to cast. Can't remember if I considered storing it as a page reference rather than ID. The ID worked for this situation since the use case is either rendering via FieldtypeFormSelect or passing the value to FormBuilder to render. Page reference makes more sense though, it's finding the form page anyway so formatting the name for output doesn't rely on it. What I should have done is render the input select using InputfieldSelect which would let ProcessWire manage theming. Unfortunately I think the answers you seek are all due to how fast I put this module together for use in a current project. I think your suggestions are good and I'll take another look at my approach.
  14. @netcarver I keep a backup drive connected when I'm at my desk and also use Timeshift as well as Back In Time. Timeshift, as @Andi recommends, is set up to snapshot the system as often as hourly when needed and every time I execute a package command including install/update/uninstall with timeshift autsnap. I keep snapshots for X number of hours/days/weeks/months. If you're running EXT4 it's all done easily with an external drive. Back in time for user file snapshots at 30min intervals. Being able to grab a file at an earlier version with pretty granular diffs is great. Samsung 990 Pro internal + T9 external, fastest snapshots ever ?
  15. Best Gnome I've ever used for sure. NICE. Glad you got it working!
  16. Active developers, passionate team, can self-host. Entirely happy with it! I don't remember it being difficult, everything works as well as it did when I was on Ubuntu. Odd that it gave you trouble. I installed Docker then DDEV then done.
  17. I'll share my experience as full-time 100% happy Linux user since 2018. I used Ubuntu up until a few months ago when I got my new laptop (Framework, btw). I just switched to Manjaro and have had a flawless experience. The pain point in the beginning was the common losing Photoshop conundrum, but I've found better solutions for web design since then and only use Gimp when I need to clean up/crop/make web-ready bitmap images. Moving primarily to vector for UI makes bitmap design feel very un-web. For web and layout design I've switched over to using the FOSS Penpot web app. I've traditionally be hesitant to embrace a web platform, but really with today's state of web technology I feel at home with it now and don't miss a desktop app. I've also come to appreciate that a web platform allows developers to iterate, bugfix, and deploy new features much more quickly without having to wait for an entire full local app update. I get that this can be a pain point for those looking to make the switch. In my case my work is UI/UX based and don't consider myself, nor have the talent for, a graphic designer. I, like pretty much everyone here, absolutely have to have a dependable machine to do work. My ability to earn an income stops dead without a usable laptop and having a backup machine just isn't a viable option considering how much it would take to fall back on. I'll back-to-back my experiences. I'm not trying to sell anyone on these distros, but the experience has broadened some horizons. I didn't switch for 5 years. Do what you want, it's still *checks watch* a free country. Linux in general: Free and open source. I've become much more aware and appreciative of how great this is. I don't feel "screwed around with". The "innovations" coming out of Microsoft and MacOS updates tend to be disruptive and entirely subjective as far as their utility and value are concerned. There hasn't been one single feature that they've released that I feel like I'm missing out on, and in some cases already have. Plenty of stuff I'm grateful not to have or deal with. Apple has been long-term recognized for it's decline in the "pro" experience. I feel more at home on servers and have stronger skills on the command line. Even if the distros are different, you still get a shared directory and OS behavior between server and desktop. I always appreciated Mac for the *nix-like environment, but switching to Linux still felt a lot more awesome because at the end of the day I wasn't jumping between OS' that have true distinctions. Docker just runs better both in ease of installing/managing and in raw performance- which Mac really struggles with. The fact that Windows now at least has WSL is a nod to the needs of developers. Mac specifically has had known performance issues for years. For me, security and privacy is a big deal. Win is completely out of the question, and Apple's claims are becoming less trustworthy on this front. When I boot my computer and connect to the internet my network up/down activity is literally a flat line. Aside from having automatic update checking for the OS, it don't talk to nobody nowhere about nothin'. Not possible with Win/Mac and I am also not constantly accepting new terms of service and opting out of things left and right. I think Windows Recall and Apple Intelligence are really dumb and whatever promises are made today about "security and privacy" can be cancelled via new Terms and Conditions you're forced to agree to so you can continue using your computer after a decision made based on what side of the bed a CEO woke up on or earnings report demanding more profits be made or else the CEO will be fired and replaced with a new CEO who will force it upon everyone anyway. A run-on sentence for a run-on problem. NVIDIA support is spotty, and that's on NVIDIA (linus_middle_finger_nvidia.jpg). My previous machine did have an NVIDIA gpu but it was as old as the 2018 laptop and I never had real problems with it since it had long been stable. If you're someone who needs new NVIDIA, it'll come down to some research about your gpu and compatibility. Aside from the graphics app situation, I didn't feel like I lost anything. My code editor (Sublime Text, I'm switching to Neovim) is available and there's a lot of availability great alternatives without compromise. If you like games, Steam's compatibility layer and continually widening native game selection has been a massive improvement. I'm an extremely casual game player though and others would have a better opinion on more serious gaming. Linux is a beacon of hope when faced with the depressing and accelerating trend known as "the ens**ttification of everything" Obvs, full customization, many desktop environment/window tiling options, it's like exploring the limitless frontier of space. You can do anything you want, including getting sucked into a black hole of customizing your machine and realizing at some point you just have to stop or you'll never stop. Never been there, no idea what that's like, totally hypothetical on my part, really, I would never do that, I see that none of you believe me. Ubuntu positives: Debian based means wide availability in software and a ton of support. Easy to find help online when needed. Everything can pretty much be relied upon to be ready out of the box Stable. Very. This is why I used it for so long, for reasons mentioned above Well tested before releases so you know upgrades are far less likely to break something save for the most deeply customized and tweaked systems Ready to go out of the box and does well maintaining availability for things like NVIDIA drivers where stable. It's why my NVIDIA gpu never had issues which was highly appreciated. The softest landing for someone making the jump from Win/Apple I never borked my machine to a point where it needed serious effort to get working again. This does not include typing "sudo rm -rf --force" into a terminal within a VM to see what would actually happen only to realize the VM wasn't in focus and it was actually a real terminal and find out it does exactly what you think it would do Ubuntu not-so-positives: I'll address the elephant in the room: Snaps. I've never liked them, but having had the choice of how I installed/managed my apps made it less of an issue. My tolerance ran dry once they started replacing system apps with Snaps at a direct cost of integrations due to sandboxing. Most simple example would be the calculator- they Snap'd it and you couldn't quickly type math into the Gnome search input, that's a micro-example but there are others that were problematic. Choosing to replace apps by default while removing features is just not great, especially when Snap hadn't been widely well received. Fresh installs meant a lot of work uninstalling and replacing- to a point where I kept a "checklist" so I didn't have to look them up. Snaps part II: The latest version of Ubuntu has now forced Snaps and added a lot of difficulty even forcing it to not install them by default. When I type "sudo apt install firefox", I expect a deb. What I didn't type was "snap install firefox", and Ubuntu choosing to ignore and deceptively replace what you asked for is unacceptable. I didn't know this happened until a) Firefox took longer to open, and b) I went to "About Firefox" to check. Not not not cool and it made me lose trust in Canonical. It involves extra steps that never existed before Feels bloated, I got a performance boost on the same machine switching to another distro. This means different things to different people though. Bi-annual stable release means that it's generally behind versions in things like Gnome and the kernel. This isn't 100% bad because that's a tradeoff made for peace-of-mind stability. I'm not sure if this is widespread, but out of the box APT fails because it hangs using ipv6 and doesn't fall back to ipv4. No idea why but I have to fix this on every install. Again, like the Snap calculator example, this is not a huge issue but things add up. Manjaro positives: Arch based without Arch stability anxiety. I am completely capable of managing an Arch distro but I just don't have the time to do it. As much as I want to use Arch, btw. I need to sit down at my computer and work without some concern that I might have to take a detour to manage the OS because it could cause me trouble with deadlines and confidently delivering for clients. Arch updates are tested and have a healthy delay between bleeding edge release and Manjaro updates. Personally, I wait an extra week- unless it's a notable security update, and just check in on the message boards to see if there are any known issues, then update myself. The maintainers are responsive and communicative. Absolutely faster and leaner than Ubuntu. Some of this comes from having more recent updates sooner. Gnome was a big difference because Ubuntu doesn't update as often and known performance issues take a lot longer to make it into the OS by comparison Truly clean install. Example- the dock on the left side of the screen was mentioned- not a problem on vanilla Gnome that hasn't been modified by Canonical Still has a wide selection of software. I lost nothing when switching. AUR (Arch User Repository) provides a lot of software that you are looking for available on Deb but repackaged for Arch. This falls into both positives and considerations Sane defaults, 100% worked out of the box, dip your toes into Arch without falling in. I may switch to it down the line. Manjaro considerations: Need to be mindful about updates, this is mitigated by what I mentioned above, but you have to be aware and make more decisions than Ubuntu. AUR is wide open and community maintained. Installing apps willy-nilly opens up possibilities for bad or unmaintained software. I've adopted an "install it only if you need it" and read up before I install. With Ubuntu I noticed I cared a little less about this because a lot of stuff is available via Ubuntu and trusted repositories. I'd also chalk that up to a potential false sense of security because of how beginner friendly it is. Everyone should keep this in mind regardless of distro. Involves extra documentation. If I have to look something up I check Manjaro specific info, then fall back to Arch. No something I ever needed with Ubuntu The reason one has not-so-positives, and the other has considerations is because there hasn't been anything that I don't like or been forced to deal with, only decisions I had to make when switching. Different experience. Others do not like what I like about Manjaro, and that's cool. All said- I'm not making a case for Manjaro specifically- just sharing what a compare/contrast looks like between distros and specifically back to back with Ubuntu. There are plenty of alternatives out there. Find one you like, or distro hop for fun and research. I like Gnome, others like KDE, others like i3, knock yourself out- install more than one. The world is your oyster, in space, next to the black holes. Why I mentioned my laptop... I bought a Framework 16 and love everything about it. Modularity, repairability, upgradeablity, company ethos, etc. On top of all that they have first-class Linux support and an internal team that actively works on this. Ubuntu is an officially supported distro, Manjaro is not but I've had zero issues whatsoever. Worth considering if you're in the market for a new machine. I also considered the companies mentioned by @gebeer and in the past I had a System76 as well. There are more great options these days than ever IMHO. I also want to mention- little to no fan noise at all except under real load that are directly related to what I'm doing. Maybe a little during automatic disk backups but those are fast and only when I'm connected to my external. I think the specs on a fresh machine probably help that, but battery life is also great. I also don't have the optional dedicated GPU, the AMD chip does very well with a lot of games. This has happened once for me last month and it was for a completely aesthetic and totally unnecessary it's-okay-to-laugh-at-me-for-installing-this "neat thing". I try to keep things simple but agree that this is an issue with much-customized DEs. My issue stemmed from distro updates coming out faster than the extension. Overall agree and YMMV. Happens less with Ubuntu due to the delayed Gnome releases that can be multiple versions behind current but are stable. Mad respect. I wish I could live on this level of bleeding edge. Feels like I'm watching a someone drive by in a Porsche 911 GT2 from behind the fence at a daycare playground ? GrapheneOS here. You have to like not having a lot of things though haha. Bravo. Did this years ago and it's liberating for many reasons- this forum is pretty much the only "social" account I have. As for privacy, I'll send you the dossier I purchased from a data broker with a map of your activities on and offline for the past 6 months, no need to send me your address, I know where to mail it ?
  18. Hello all, FieldtypeFormSelect has been added to the modules directory and is also now installable via Composer. Thanks, and enjoy!
  19. FormBuilderHtmx is now available in the modules directory and can be installed via Composer. Thanks to all for the feedback and testing! If there are improvements or edge case issues, feel free to file a bug report on the GitHub repo or check in here for support.
  20. ? <- me, pretty much all the time with ProcessWire. Fantastic stuff. Thanks as always, and happy Friday everyone!
  21. This is really great! I want to be able to shift some priority in featured content around websites based on visibility/exposure to visitors, like "featured pages" or blog posts and this info will help provide the information needed to build that more robustly. I've tinkered in the past with $page->meta() but this looks very robust. Thank you for putting this together!
  22. @ryan just opened a PR on Github that should resolve the issues above. Module appears to be working as expected now.
  23. 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.
  24. 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!
×
×
  • Create New...