Jump to content

How do I transition from procedural to OOP?


Alpina
 Share

Recommended Posts

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!

  • Like 2
Link to comment
Share on other sites

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!

  • Like 18
  • Thanks 5
Link to comment
Share on other sites

9 hours ago, FireWire said:

Wasn't sure of your overall exposure to OOP but hopefully this helps and inspires!

Wow. And the award for the greatest answer of the year goes to...

  • Like 6
  • Thanks 1
Link to comment
Share on other sites

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

  • Like 5
  • Thanks 1
Link to comment
Share on other sites

59 minutes ago, wbmnfktr said:

a prime example why PW and its API is so awesome

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.

7 hours ago, Alpina said:

Wow. And the award for the greatest answer of the year goes to...

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.

  • Like 7
Link to comment
Share on other sites

  • 3 weeks later...

Transitioning from procedural development to object-oriented development is primarily a matter of approach and key principles to follow. It involves learning to code differently, but the code itself is not the core issue; it is the way the application is structured that is important.

It's not enough to create a class to do OOP; for example, if you put 5000 lines of code in a class, you are probably not adhering to OOP principles. 🙂

I would advise you to look into the KISS principle, read articles that compare inheritance with composition, study the role of interfaces and abstract classes, study Design Patterns, and represent your programs with UML class diagrams before you start coding (at least during your learning period).

A book that definitively gave me a leap forward is "Design Patterns" from the Head First series. This book is absolutely magnificent for understanding the OOP approach in record time while learning the essential Design Patterns and the reason for their existence.

enseignement-iut-m3105-conception-avancee/patterns/references_patterns.md  at master · iblasquez/enseignement-iut-m3105-conception-avancee · GitHub

  • Like 4
Link to comment
Share on other sites

My recommendations for those who might be wondering...:

I can also recommend even the free and public content of this site: https://refactoring.guru/design-patterns/php

And if you have an actual task to solve, ask chat ChatGTP 4+ what it can recommend. And always ask it for more design pattern "ideas" since the random first one it recommends might not be the one that solves a particular problem best. After you have discovered the possibilities then sites like refactoring.guru and others can help with concrete implementation examples.

  • Like 2
Link to comment
Share on other sites

20 hours ago, da² said:

Transitioning from procedural development to object-oriented development is primarily a matter of approach and key principles to follow

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.

18 hours ago, szabesz said:

I can also recommend even the free and public content of this site: https://refactoring.guru/design-patterns/php

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.

18 hours ago, szabesz said:

ask chat ChatGTP 4+ what it can recommend. And always ask it for more design pattern "ideas" since the random first one it recommends might not be the one that solves a particular problem best.

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.

  • Like 2
Link to comment
Share on other sites

9 minutes ago, FireWire said:

It requires starting to work with encapsulation

Yes, encapsulation is a very important concept to read about, I forgot to mention it in my message.

9 minutes ago, FireWire said:

The Head First series really does explain things in ways that I have seldom seen elsewhere.

Yes, the book is written based on neuroscientific knowledge that facilitates learning, and it works incredibly well. Before I read this book, I had been trying to understand Design Patterns for weeks or months through discussions on forums with good developers, web tutorials... But I couldn't understand anything, I saw no point in coding DPs. Then I received this book, read 2 or 3 chapters the first evening, it was very exciting to read, I dreamed all night of boxes connecting together (classes/objects), and the next morning I understood everything! 😃👍

  • Like 2
Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...