Simplifying object-oriented approach with Singleton

Learn how to use functions and modules created using an object-oriented approach in WordPress plugin development in a way that will give you simplicity like in the procedural approach.

tl;drGitHub

Ah, this whole object-oriented approach seems like an overengineering. In procedural programming, I just write function, use it wherever I want, and that's it. But here? Classes, namespaces, encapsulation, blah blah. I don't have time for that.

I understand such a perspective. I agree that the object-oriented approach may seem more complex, but I assure you that it doesn't have to be that way with a little effort. So today, I'll try to explain how to make using functions in this approach as simple as in procedural. Let's get started!


What are the requirements?

Last time, I made some basic architectural decisions to implement internal links after the post content. Today I'll step further and implement the more challenging solution.

Project

My website is currently small and doesn't have much content or authors yet, so I would like to search for some kind of external boost. I will try to implement simple news hub features like displaying links to external articles. To simplify things, I will use a publicly available ESPN RSS feed as a post repository. So the business requirements are:

  • Displaying a widget with links to 5 random articles from ESPN in the sidebar.
  • Displaying list with links to 5 random articles from ESPN after post content.

How to create ESPN repository?

Let's start by implementing the simplest ESPN repository that I'll use for querying the data. I just need to utilize the XMLReader object provided in PHP by default, load XML from the URL, iterate through the elements in the document, create a collection of data and a function for retrieving 5 random posts from this collection.

namespace FM\Integrations;

use XMLReader;
use SimpleXMLElement;

class ESPN
{
    public function get(): array
    {
        if (empty($data = $this->read())) {
            return [];
        }

        if (empty($keys = array_rand($data, 5))) {
            return [];
        }

        return array_values(array_filter($data, fn(int $key) => in_array($key, $keys), ARRAY_FILTER_USE_KEY));
    }

    private function read(): array
    {
        $data = [];

        $reader = new XMLReader();
        $reader->open('https://www.espn.co.uk/espn/rss/football/news');

        while ($reader->read()) {
            if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'item') {
                $item = new SimpleXMLElement($reader->readOuterXML(), LIBXML_NOCDATA);

                if (! empty($item->title) && ! empty($item->link)) {
                    $data[] = [
                        'title' => (string) $item->title,
                        'url' => (string) $item->link,
                        'description' => ! empty($item->description) ? (string) $item->description : '',
                    ];
                }
            }
        }

        $reader->close();

        return $data;
    }
}

It's important to note that I'm not striving for perfection here. It's a prototype intended to show the basic idea, which can be refined over time so for now I don't address security issues related to loading external resources. However, if I were developing a production-ready solution, it would be essential to pay more attention to that aspect.

Now, I just need to create a new instance and use get function for querying the data.

$espn = new FM/Integrations/ESPN();
$espn->get();

How to create a widgets?

The data repository is ready so it's time to utilize it and implement the first business requirement which is creating a widget that displays five random articles in the sidebar.

As discussed in the previous article, I don't do this in some random place. I use the separation of concerns and create a new module called Widgets, which will take care of handling features related to the this context. It's placed in the Core group because for now, it's small, and once it becomes bigger, I can easily move it to a separate group.

namespace FM\Core;

use FM\Core\Widgets;

class Core
{
    public function __construct()
    {
        new Widgets();
    }
}

Once I have this, I hook into the get_sidebar action which I've added to the template, create a new instance of the ESPN class there, call its get method, and prepare the HTML based on the data. This is the first way of utilizing modules in the object-oriented approach. Creating class instances and performing public operations.

namespace FM\Core;

class Widgets
{
    public function __construct()
    {
        add_action('get_sidebar', [$this, 'addLinks']);
    }

    public function addLinks(): void
    {
        $espn = new \FM\Integrations\ESPN();

        if (empty($items = $espn->get())) {
            return;
        }

        $html = '';

        foreach ($items as $item) {
            $html .= "<div><a hre=\"{$item['url']}\">{$item['title']}</a></div>";
        }

        echo $html;
    }
}

The next step is modifying the code responsible for links below the post. It's pretty simple because I just need to switch the previous data source and use ESPN class.

namespace FM\Posts;

class Posts
{
    public function __construct()
    {
        add_filter('the_content', [$this, 'addLinks']);
    }

    public function addLinks(string $content): string
    {
        if (! is_singular('post')) {
            return $content;
        }

        $espn = new \FM\Integrations\ESPN();

        if (empty($items = $espn->get())) {
            return $content;
        }

        $html = '';

        foreach ($items as $item) {
            $html .= "<li><a hre=\"{$item['url']}\">{$item['title']}</a></li>";
        }

        return $content .= "<ul>{$html}</ul>";
    }
}

Note: Advantages of OOP

The ESPN module task is just to provide data to the client (an object using service). The client doesn't need to know HOW the data is created (read). All that matters is THAT the data is provided in the agreed format (get). If the client would like to have some impact on the process (e.g. try to use forbidden methods), encapsulation prevents this.

It's similar to real-life situations. You use a bakery service for getting the bread, but you can't enter the kitchen and bake the bread yourself using their internal tools. You also don't have access to their perfect recipes, because that's private internal detail of the bakery. This is how encapsulation work.


What are the problems?

Links are now present in the sidebar, links in the footer as well – everything seems to be working, so the task is complete, right? Not quite.

The system is using local instances of the ESPN class in two different places and each of them performs external requests every time I use the get function (#1, #2). Let's check out now the $finish variable in the debugger that measures querying time for each request. Assuming that one request takes about 0.5 seconds, the server needs about one second for a full response, and this time will increase with each subsequent request.

I'm working with an RSS feed, where the data can be treated as static. Okay, there is a chance that it might change between the two calls, but even if it occurs, it won't cause significant issues – the next request will simply display the newer data.

It all points me to the fact that making multiple requests for external data in the given business context doesn't make any sense because I generally work with the same dataset which I should share, not create every time.

Furthermore, let's not hide the fact that this approach may be less appealing to frontend developers who value the simplicity of utilizing backend elements. It's easier to just use a function to accomplish something rather than creating objects, worrying about namespaces, and so on. What is your opinion? Let me know in the comments!

// easier
do_something();

// harder
$espn = new \FM\Integrations\ESPN();
$espn->do_something();

So I have two problems to solve: reducing unnecessary operations by providing data-sharing methods, and the making the object-oriented approach easier to work with.


What is Singleton pattern?

I can make use of the Singleton design pattern that lets me ensure that a class has only one instance to help ma manage data sharing and provide entry point to my app.

refacoring.guru has already an excellent article about this which should help you understand everything better, so I'll just provide some basic concepts and examples.

Implementation

  • Make __constructor private, to prevent other objects from using the new operator with the Singleton class. It needs to be done also to __clone magic method to avoid cloning. __wakeup can't be private so I just throw an exception.
  • Create a static get method that calls the private constructor to create an object and save it in a static field which is then served when needed.

I've decided to make my app facade a singleton to simplify the code and module usage.

namespace FM;

use FM\Core\Core;
use FM\Integrations\Integrations;
use FM\Posts\Posts;
use FM\Teams\Teams;

class App
{
    private static ?App $instance = null;

    private function __construct()
    {
        new Core();
        new Posts();
        new Teams();
    }

    private function __clone()
    {
    }

    public function __wakeup()
    {
        throw new \Exception('Cannot unserialize a singleton.');
    }

    public static function get(): App
    {
        if (empty(self::$instance)) {
            self::$instance = new App();
        }

        return self::$instance;
    }
}

Usage

To get the facade object I just need to call static get method and nothing more 🤷‍♂️

\FM\App::get()

Once the static method is called first time it initializes the class instance and stores it in the private property. Every next call omits instance creation, by checking if the property is already set. So whenever that method is called, the same object is always returned.

public static function get(): App
{
    if (empty(self::$instance)) {
        self::$instance = new App();
    }

    return self::$instance;
}

Tests

Let's try to check if it works by creating a property that stores some random number generated during the class initialization and firing the static method three times. Now, as it is visible in th debugger on the left side, no matter how many times get function is used, the same object is always returned. So it works 🎉

Modules Usage

Everything sounds good so far, but what's next? What should I do with this object? How do I utilize the modules I have created? All I need to do is initialise the modules and assign them as facade attributes to easily access them using \FM\App::get()->teams;

\FM\App::get()->teams;

However, there is an important consideration: I must ensure proper encapsulation. Since the object is shared, I don't want every client to modify the properties freely.

$app = \FM\App::get();
$app->integrations = 'asdasdasd';

I need to prevent clients from managing properties incorrectly by making access modifiers private and creating read-only methods for accessing submodules. This way, the facade exposes only things that should be exposed, making everything more secure.

class App
{
    private Core $core;

    private Integrations $integrations;

    private Posts $posts;

    private Teams $teams;

    private function __construct()
    {
        $this->core = new Core();
        $this->integrations = new Integrations();
        $this->posts = new Posts();
        $this->teams = new Teams();
    }

    public function core(): Core
    {
        return $this->core;
    }

    public function integrations(): Integrations
    {
        return $this->integrations;
    }

    public function posts(): Posts
    {
        return $this->posts;
    }

    public function teams(): Teams
    {
        return $this->teams;
    }
}

For example, If I don't want the module teams to be accessible by clients, I just remove teams() access method.


Adding some Sugar!

Let me try to give you some energy and try to add some sugar to this process 🥳

Instead of calling the static method of my facade every time I want to use it, it would be better to create a global function that simply returns the object I need. I put this function in the inc/bootstrap.php file and instead of \FM\App::get()->teams() I'm able to use fm()->teams() now.

if (! function_exists('fm')) {
    function fm(): FM\App
    {
        return FM\App::get();
    }
}

Tip: Use plugin initials as the name of your function to distinguish the function better. For example: nf() for NinjaForms plugin.

I need to remember about initializing all the system components! It can be done just by firing fm() function in the functions.php file and that's all.

define('FM_VERSION', '0.0.1');
define('FM_PATH', dirname(__FILE__));
define('FM_FILE', FM_PATH . '/functions.php');

require_once(FM_PATH . '/inc/bootstrap.php');

fm();

What are the results?

How have I managed to solve my problems? Let's try to test data-sharing methods first.

I've modified ESPN integration and added some kind of local caching. Now, the post collection is not set until the first query is executed. After that, the received data is stored in a local attribute and then reused by other calls.

namespace FM\Integrations;

use XMLReader;
use SimpleXMLElement;

class ESPN
{
    private ?array $data = null;

    public function get(): array
    {
        if (empty($data = $this->read())) {
            return [];
        }

        if (empty($keys = array_rand($data, 5))) {
            return [];
        }

        return array_values(array_filter($data, fn(int $key) => in_array($key, $keys), ARRAY_FILTER_USE_KEY));
    }

    private function read(): array
    {
        if (! empty($this->data)) {
            return $this->data;
        }

        $this->data = [];

        $reader = new XMLReader();
        $reader->open('https://www.espn.co.uk/espn/rss/football/news');

        while ($reader->read()) {
            if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'item') {
                $item = new SimpleXMLElement($reader->readOuterXML(), LIBXML_NOCDATA);

                if (! empty($item->title) && ! empty($item->link)) {
                    $this->data[] = [
                        'title' => (string) $item->title,
                        'url' => (string) $item->link,
                        'description' => ! empty($item->description) ? (string) $item->description : '',
                    ];
                }
            }
        }

        $reader->close();

        return $this->data;
    }
}

What about response times? The data is shared now so the server needs about 0.5s for querying no matter how many times I'll use get the function. That's a significant improvement.

What about code simplicity? I'm probably talking about tastes here, but for me, fetching random posts looks much simpler than before. Of course, using the static method directly is not really demanding, but it just looks better and easier for others. What do you think? Let me know in the comments.

fm()->integrations()->espn()->get();

I've also reduced code complexity in other ways. With PHP Inteliphence, I'm able to view all the available methods on the fly, when writing code. It makes the whole process so much easier!

So I thing that the results I've managed are really promising 😎


What are the concerns?

Is Singleton anti-pattern?

Using Singleton raises a lot of controversies and leads to complaints that it is an anti-pattern and that we should avoid it at all costs, but I don't agree with this strict point. It might be anti-pattern when overused or used incorrectly, but it doesn't need to be so. It has pros and cons as everything, and we should carefully consider its usage, rather than not using or using it because someone said so.

I'm aware of the problems it entails like testability, violating the SRP and a few others, but it gives me flexibility, makes my code easier to use and seriously never caused any problems in MY approach. So if it serves its purpose, solves problems and doesn't cause others - it's just not anti-pattern for me.

We can solve data-sharing problems other way.

That's right. We always can do things differently. In this case, I have plenty of ways to solve those data problems rather than using Singleton, but I use it now because I know that there will be many more valuable cases for this pattern in the next articles. Stay up with me to check out them!


Summary

The Singleton design pattern has been a subject of discussion among developers for a long time. For some it is salvation, for others, it is a terrible thing. I'm in the first group.

It improved my and the team's efficiency and simplified the more complex approach that I try to seems that it present here. If it aligns with your needs and use case, it is worth exploring further. However, it may not be suitable for every scenario. If you you have a strict automated testing policy, you work with multithreaded applications, you should carefully consider its usage.

To summarize, its implementation should be approached judiciously, considering the specific requirements of your project. Be mindful of the potential drawbacks associated with this pattern and make informed decisions accordingly.

If you managed to get to this point, I insist you at least try it and let me know what your impressions are 🙏

avatar

Looking for a developer who
truly cares about your business?

My team and I provide expert consultations, top-notch coding, and comprehensive audits to elevate your success.

Feedback

How satisfied you are after reading this article?