SOLID Principles: Part 2, Open-Closed
Part 2 of SOLID Principles: Writing Maintainable Object-Oriented Code
#OOP #PHP #Symfony
In this second article of the SOLID series, we'll be diving into the Open-Closed Principle (OCP).
This principle encourages developers to design software modules that are open for extension, but closed for modification.

Introduction to OCP
At its core, the Open-Closed Principle encourages developers to:
"design software modules that are open for extension, but closed for modification."
This is one of the SOLID principles that is not that easy to understand and follow in real world applications.
This means that once a module is developed, it should not be modified directly, but rather extended or built upon in a way that preserves its original functionality.
Project Setup
As in the first article of this series, we will use a simple blog demo application built with Symfony. If possible it would be better to read all the articles in this series in order as each of them is starting where the previous one finished.
You can find the code for the example in this article in this Github repository. We will start on the "SRP_right" branch which is where we left off in the previous article.
To install the application, follow the steps in the README file. For more details on this application please have a look at the first article of this series.
Breaking the OCP
As with the other articles in this series we will begin by providing an example of code that does not follow the OCP.
In our application we have the App\Service\AdminNotifier service class that is responsible for notifying the admin when a new article is created.
App\Service\AdminNotifier
namespace App\Service;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class AdminNotifier
{
public function __construct(
private readonly MailerInterface $mailer,
)
{
}
public function notifyNewArticle(string $title): void
{
$email = (new Email())
->from('my_blog@example.com')
->to('admin@example.com')
->subject('New article created')
->text('A new article has been created: ' . $title)
;
$this->mailer->send($email);
}
}
Let's say that we now want to add a new way to notify that a new article was created.
For simplicity purpose we will display an alert on the user interface.
A simple way to do this is to use flash messages. We inject the RequestStack in the constructor of our service and then use it to get the Session, then the FlashBag and add a success message in the FlashBag.
App\Service\AdminNotifier
namespace App\Service;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class AdminNotifier
{
public function __construct(
private readonly MailerInterface $mailer,
private readonly RequestStack $requestStack,
)
{
}
public function notifyNewArticle(string $title): void
{
$email = (new Email())
->from('my_blog@example.com')
->to('admin@example.com')
->subject('New article created')
->text('A new article has been created: ' . $title)
;
$this->mailer->send($email);
$this->requestStack->getSession()->getFlashBag()->add('success', 'A new article has been created: ' . $title);
}
}
We can then display this flash message in our Twig template.
templates/base.html.twig
<main class="mt-6 mb-6 flex-1 container mx-auto max-w-6xl px-2 sm:px-6 lg:px-8">
{% for message in app.flashes('success') %}
<div class="bg-teal-100 border-t-4 border-teal-500 rounded-b text-teal-900 px-4 py-3 shadow-md">
{{ message }}
</div>
{% endfor %}
{% block body %}{% endblock %}
</main>
You can find the whole code on the open_closed_wrong branch.
If we test adding a new article, we can see that it works just fine 🎉

Refactoring to Follow the OCP
So what's wrong with our code?
In order to add a new way to notify about article created we had to modify the AdminNotifier class.
If we want to follow the OCP, we would like to be able to add new notifications channels without having to modify this class.
In order to do this we will have some refactoring to do.
First we will create a NewArticleNotifierInterface interface
App\Contract\NewArticleNotifierInterface
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('new_article_notifier')]
interface NewArticleNotifierInterface
{
public function notifyNewArticle(string $title): void;
}
The "AutoconfigureTag" attribute will automatically tag the services that implement this interface with the tag "new_article_notifier". Have a look at the Symfony documentation if you want to learn more about service tags in Symfony. This will be useful to inject them into our AdminNotifier.
Then we will create two different services that implement this interface, App\Service\EmailNotifier will be used to send the email and App\Service\FlashNotifier will be used to display the flash message.
App\Service\EmailNotifier
namespace App\Service;
use App\Contract\NewArticleNotifierInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class EmailNotifier implements NewArticleNotifierInterface
{
public function __construct(
private readonly MailerInterface $mailer,
)
{
}
public function notifyNewArticle(string $title): void
{
$email = (new Email())
->from('my_blog@example.com')
->to('admin@example.com')
->subject('New article created')
->text('A new article has been created: ' . $title)
;
$this->mailer->send($email);
}
}
App\Service\FlashNotifier
namespace App\Service;
use App\Contract\NewArticleNotifierInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FlashNotifier implements NewArticleNotifierInterface
{
public function __construct(
private readonly RequestStack $requestStack,
)
{
}
public function notifyNewArticle(string $title): void
{
$this->requestStack->getSession()->getFlashBag()->add('success', 'A new article has been created: ' . $title);
}
}
Lastly we will modify the AdminNotifier class so that it can receive an array of services that implement the NewArticleNotifierInterface interface. Then in the "notifyNewArticle" method we will loop over this array and foreach of them call their "notifyNewArticle" method.
App\Service\AdminNotifier
namespace App\Service;
use App\Contract\NewArticleNotifierInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class AdminNotifier
{
/**
* @param NewArticleNotifierInterface[] $newArticleNotifiers
*/
public function __construct(
#[TaggedIterator(tag: 'new_article_notifier')]
private readonly iterable $newArticleNotifiers,
)
{
}
public function notifyNewArticle(string $title): void
{
foreach ($this->newArticleNotifiers as $notifier) {
$notifier->notifyNewArticle($title);
}
}
}
Here the "TaggedIterator" attribute creates a collection of the services with the tag "new_article_notifier".
This way if we want to add new notifications way that should be used by the AdminNotifier class, we only have to create a new class that implements NewArticleNotifierInterface interface, we do not have to modify the AdminNotifier class, it is now open for extension but closed for modification.
You can find the whole refactored code on the open_closed_right branch.
This is of course a very simple example of following OCP, there are other ways to write code that can be extended, for example using event listeners.
In real applications this is not that easy to write code that follows OCP but keeping this principle in mind might help us create softwares that are more modular and more scalable.
See you in the next article which will be about the Liskov Substitution Principle.
Articles in this series
Last updated 7 months ago