Solid PHP - SOLID principles in PHP
SOLID, this acronym was coined by Michael Feathers, it represents the five basic principles of object-oriented programming developed by Uncle Bob.
Most programmers probably know this acronym. But it seems to me that a minority can decode it.
Is it wrong? Not exactly, I think that writing clean and simple code is more important than knowing the theory. But don't completely ignore the theory. How else will you pass on knowledge to someone? How do you justify your code in a discussion during the code review? Your code has to be based on theory and generally accepted standards.
It's essential to know the basics of what clean and simple code looks like. SOLID principles can be used in any object-oriented programming language. I work in Symfony on a daily basis, so I will show some principles in PHP.
So let's go through the five SOLID principles together.
Single responsibility principle (SRP)
I think this is the most famous rule (probably because it is the first and some people did not read on). But seriously, I think it is very important.
Uncle Bob describes it as “A class should have one, and only one, reason to change”. What does it mean? For me, this sentence is not helpful :).
Other explanations say that a class or a function should do one thing.
But what is this “one thing”? Is user registration can be considered “one thing”? Or maybe it's more "things", because registrations include some other smaller things, password encryption, saving to the database and sending an e-mail.
Perhaps, sending an email can be considered a “one thing”? After all, it consists of many steps such as preparing the e-mail content and subject, extracting the user's e-mail address and handling the response. Should we set up a separate class for each of these activities? How far do we go with this “single responsibility”?!
I think we need to use a different object-oriented programming principle to answer this question. In 1974, the "high cohesion and low coupling" principle was described for the first time in the article Structured Design in the IBM journal.
I will try to present it in simple-to-understand examples.
Cohesion determines how much a function or class is responsible for. A simple example here would be Bob and Alice, the cook's helpers. Alice is making desserts. She needs to make a sponge cake, cream, glaze, cut the fruit and put it all together. Each of these steps consists of several others. It's an example of low cohesion. Bob's job is to peel potatoes, nothing else, it’s an example of high cohesion. Your method/class should be like Bob, do one thing.
Coupling is about how easy it is to reuse a given module or class. Puzzles and Lego blocks are good examples of that. The puzzles are characterized by high coupling. One puzzle fits only in one place, it cannot be combined with other puzzles. The opposite of Lego bricks, they have a low coupling, they can be combined freely and each one can be used anywhere. Your code should be like Lego blocks, easy to use in different places.
The single responsibility principle should be used together with the "high cohesion and low coupling" principle. Both of these principles, in my opinion, try to say the same.
Now, an example in the PHP code. Imagine a BlogPost class:
class BlogPost
{
private Author $author;
private string $title;
private string $content;
private \DateTime $date;
// ..
public function getData(): array
{
return [
'author' => $this->author->fullName(),
'title' => $this->title,
'content' => $this->content,
'timestamp' => $this->date->getTimestamp(),
];
}
public function printJson(): string
{
return json_encode($this->getData());
}
public function printHtml(): string
{
return `<article>
<h1>{$this->title}</h1>
<article>
<p>{$this->date->format('Y-m-d H:i:s')}</p>
<p>{$this->author->fullName()}</p>
<p>{$this->content}</p>
</article>
</article>`;
}
}
What's wrong here? The BlogPost class does too many things, and as we know, it should only do one thing. The main problem is that it is responsible for printing to various formats, JSON, HTML and more if needed. Let's see how this could be improved.
We remove printing methods from the BlogPost class, the rest remains unchanged. And we're adding a new PrintableBlogPost interface. With a method that can print a blog post.
interface PrintableBlogPost
{
public function print(BlogPost $blogPost);
}
Now we can implement this interface in as many ways as we need:
class JsonBlogPostPrinter implements PrintableBlogPost
{
public function print(BlogPost $blogPost) {
return json_encode($blogPost->getData());
}
}
class HtmlBlogPostPrinter implements PrintableBlogPost
{
public function print(BlogPost $blogPost) {
return `<article>
<h1>{$blogPost->getTitle()}</h1>
<article>
<p>{$blogPost->getDate()->format('Y-m-d H:i:s')}</p>
<p>{$blogPost->getAuthor()->fullName()}</p>
<p>{$blogPost->getContent()}</p>
</article>
</article>`;
}
}
You can see a whole example of bad and good implementation here
I've seen projects where classes only have one public method with a few lines of code (usually call to a different method from a different class). Completely illegible and terrible to maintain. In my opinion, this is an example of going too far.
To sum up. Your classes and methods shouldn't be responsible for a few things. But the point here is not to go to extremes and exude absolutely everything. Just to make them easy to understand, but they also have to be consistent. So that you don't have to read them cover to cover to understand what they are doing.
Open/closed principle (OCP)
Second, from SOLID principles. The general explanation is that “code should be open for extension, but closed for modification”. It is not obvious to me what this means in practice. Perhaps it is better explained by the consequence of not following this rule. Changing the declaration of a method may cause it to malfunction somewhere it is used. The main point is that the changes have to be backwards compatible. Of course, it's best to write code that works perfectly from the beginning and you don't have to change it, but we don't live in a perfect world.
I will try to present it with some examples:
a) open/closed API
This will be an example of the open/closed principle not on a single class but on the entire API. It's a large SaaS, it is an accounting system written in PHP, Symfony Framework. Your API is used by several hundred customers who use it to issue invoices. Your API has a method to retrieve invoices as PDFs. Let's say it is an endpoint like “GET /invoice/{id}/print”. Everything is fine, but one day customers demand the option to download CSV (everyone from business loves tables).
So you implement this capability quickly and change the endpoint from:
"GET /invoice/{id}/print"
to
"GET /invoice/{id}/{format}"
where the format can be PDF or CSV.
Now only hundreds of programmers using your API have to change how they download the report in PDF. Well, no, it shouldn't be done that way. How to do it correctly? Unfortunately, it is sometimes necessary to see potential problems and anticipate possible future changes. From the beginning, your endpoint did not follow the open/closed principle because it was not closed for modification. Your endpoint should assume that the need for other formats may arise someday.
b) open/closed animals
Another example is a more classic one. Let's say we have several different animal classes:
class Dog
{
public function bark(): string
{
return 'woof woof';
}
}
class Duck
{
public function quack(): string
{
return 'quack quack';
}
}
class Fox
{
public function whatDoesTheFoxSay(): string
{
return 'ring-ding-ding-ding-dingeringeding!, wa-pa-pa-pa-pa-pa-pow!';
}
}
And a class that allows animals to communicate:
class Communication
{
public function communicate($animal): string
{
switch (true) {
case $animal instanceof Dog:
return $animal->bark();
case $animal instanceof Duck:
return $animal->quack();
case $animal instanceof Fox:
return $animal->whatDoesTheFoxSay();
default:
throw new \InvalidArgumentException('Unknown animal');
}
}
}
Is the Communication class open for extension and closed for modification? To answer this question, we can ask it differently. Are we able to add a new animal class without changing the existing code? No. Adding a new animal class would necessitate the modification of the switch in the communicate() function. So what should our code look like to comply with our principle? Let's try to improve our classes a bit.
We can start by adding an interface Communicative and using it in our classes.
interface Communicative
{
public function speak(): string;
}
class Dog implements Communicative
{
public function speak(): string
{
return 'woof woof';
}
}
class Duck implements Communicative
{
public function speak(): string
{
return 'quack quack';
}
}
class Fox implements Communicative
{
public function speak(): string
{
return 'ring-ding-ding-ding-dingeringeding!, Wa-pa-pa-pa-pa-pa-pow!';
}
}
After that, we can change the Communication class so that it complies with the open/close principle.
class Communication
{
public function communicate(Communicative $animal): string
{
return $animal->speak();
}
}
How to code according to the opened/closed principle?
In code, it is worth using interfaces and sticking to them. However, if you need to change something, consider the decorator pattern.
A class or method should be small enough and have one specific task so that no future event can necessitate modification (single responsibility principle). But you also need to consider whether there may be a need for changes in the future, such as a new response format or an additional parameter, your code should be closed for modification.
Liskov substitution principle (LSP)
The substitution principle applies to well-designed class inheritance. The author of this principle is Barbara Liskov. The principle says that we can use any inheriting class in place of the base class. If we implement a subclass, we must also be able to use it instead of the main class. Otherwise, it means that inheritance has been implemented incorrectly.
There are some popular examples of the Liskov substitution principle in PHP:
a) rectangle-square
The first example. We already have a Rectangle PHP class. Now we're adding a Square PHP class that inherits the Rectangle class. Because every square is also a rectangle :). They have the same properties, height and width.
The height of the square is the same as the width. So, setHeight() and setWidth() will set both (what about single responsibility?) of these values:
class Square extends Rectangle
{
public function setWidth(int $width): void {
$this->width = $width;
$this->height = $width;
}
public function setHeight(int $height): void {
$this->width = $height;
$this->height = $height;
}
}
Is that a good solution? Unfortunately, it does not follow the Liskov substitution principle. Let's say there is a test that computes the area of a rectangle, and it looks like this:
public function testCalculateArea()
{
$shape = new Rectangle();
$shape->setWidth(10);
$shape->setHeight(2);
$this->assertEquals($shape->calculateArea(), 20);
$shape->setWidth(5);
$this->assertEquals($shape->calculateArea(), 10);
}
According to the Liskov substitution principle, we should be able to replace the Rectangle class with the Square class. But if we replace it, it turns out that the test does not pass (100 != 20). Overriding the setWidth() and setHight() methods broke the Liskov substitution rule. We should not change how the parent class's methods work.
So what is the correct solution? Not every idea from "reality" should be implemented 1:1 in code. The Square class should not inherit from the Rectangle class. If both of these classes can have a computed area, let them implement a common interface, and not inherit one from the other since they are quite different.
You can see an example solution here
b) live duck vs toy duck
Imagine a living duck and a toy duck and their representations in the code (PHP classes). Both of these classes implement the TheDuck interface.
interface TheDuck
{
public function swim(): void;
}
We also have a controller with the action swim().
class SomeController
{
public function swim(): void
{
$this->releaseDucks([
new LiveDuck(),
new ToyDuck()
]);
}
private function releaseDucks(array $ducks): void
{
/** @var TheDuck $duck */
foreach ($ducks as $duck) {
$duck->swim();
}
}
}
But after calling this action ToyDuck doesn't swim. Why? Because to make it swim, you must first call the "turnOn()" method.
class ToyDuck implements TheDuck
{
private bool $isTurnedOn = false;
public function swim(): void
{
if (!$this->isTurnedOn) {
return;
}
// ...
}
}
We could modify the controller action and add a condition that we call turnOn() on the ToyDuck instance before swim().
private function releaseDucks(array $ducks): void
{
/** @var TheDuck $duck */
foreach ($ducks as $duck) {
if ($duck instanceof ToyDuck) {
$duck->turnOn();
}
$duck->swim();
}
}
It violates the Liskov substitution principle because we should be able to use a subclass without knowing the object, so we cannot condition by subclasses (it also violates the open/close principle - because we need to change the implementation).
Handling a collection of objects of a given base class may not require checking whether the given object is an instance of subclass X and should be treated differently.
What should it look like correctly? A common interface for both of these ducks is not a good idea, their operation is completely different, even though we think they both work similarly because they are swimming, it is not.
c) ReadOnlyFile
And the last example. We have a File class with methods read() and write().
class File
{
public function read()
{
// ...
}
public function write()
{
// ...
}
}
We're adding a new class - ReadOnlyFile.
class ReadOnlyFile extends File
{
public function write()
{
throw new ItsReadOnlyFileException();
}
}
The ReadOnlyFile class inherits from the File class. In the ReadOnlyFile class, the write() method will throw an Exception, because you cannot write to a read-only file.
This is a poorly designed abstraction, the Liskov rule has been broken because we are unable to use the ReadOnlyFile class instead of File.
Interface segregation principle (ISP)
Uncle Bob introduced this principle when he collaborated with Xerox. They couldn't cope with the ever-long process of implementing changes to their code. The rule is: “No client should be forced to depend on methods it does not use”. The user of the interface should not be forced to rely on methods he does not use. We should not use “fat interfaces” that declare multiple methods if any of them could be left unused. Better to have a few dedicated small interfaces than one that is too general. It is also in line with the single responsibility principle.
So let's see a badly written code, not following the interface segregation principle. I present to you the Exportable, PHP Interface. An interface that allows you to export something to PDF and export something to CSV. We also have an Invoice and a CreditNote class.
interface Exportable
{
public function getPDF();
public function getCSV();
}
class Invoice implements Exportable
{
public function getPDF() {
// ...
}
public function getCSV() {
// ...
}
}
class CreditNote implements Exportable
{
public function getPDF() {
throw new \NotUsedFeatureException();
}
public function getCSV() {
// ...
}
}
We can download the Invoice in PDF and CSV. We can download a CSV of the CreditNote. But downloading the PDF of the CreditNote was a useless functionality and was not implemented (it’s throwing an exception right now).
We shouldn't force our interface implementations to implement methods they don't use. In the above case, we forced the CreditNote class to do so, it implements the getPDF() method even though it does not need it at all.
So how should it look to be good?
According to the interface segregation principle, we have to separate the interfaces. We divide Exportable and create an interface ExportablePdf and create an interface ExportableCSV.
interface ExportablePdf
{
public function getPDF();
}
interface ExportableCSV
{
public function getCSV();
}
class Invoice implements ExportablePdf, ExportableCSV
{
public function getPDF() {
//
}
public function getCSV() {
//
}
}
class CreditNote implements ExportableCSV
{
public function getCSV() {
//
}
}
This way, CreditNote no longer has to worry about implementing not used getPDF() public function. If necessary in the future, just need to use a separate interface and implement it. As you can see here, specific interfaces are better.
The example of ReadOnlyFile related to the Liskov principle is also a good example of the Interface segregation principle. There, the File class has been doing too many things, it's better to have separate interfaces for each action.
That's interface segregation, easy.
Dependency inversion principle (DIP)
Last from SOLID principles, this rule is:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
What does it mean? We should reduce dependencies to specific implementations but rely on interfaces. If we make any change to the interface (it violates the open/close principle), this change necessitates changes in the implementations of this interface. But if we need to change a specific implementation, we probably don't need to change our interface.
To illustrate the problem, let's go over this PHP example.
class DatabaseLogger
{
public function logError(string $message)
{
// ..
}
}
Here we have a class that logs some information to the database. Now use this class.
class MailerService
{
private DatabaseLogger $logger;
public function __construct(DatabaseLogger $logger)
{
$this->logger = $logger;
}
public function sendEmail()
{
try {
// ..
} catch (SomeException $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
Here is the PHP class that sends e-mails, in case of an error, error details are logged to the database using the logger we have just seen above.
It breaks the principle of dependency inversion. Our e-mail-sending service uses a specific logger implementation. What if we want to log information about errors to a file or Sentry? We will have to change MailerService. This is not a flexible solution, such a replacement becomes problematic.
So what should it look like?
According to this principle, MailerService should rely on abstraction rather than detailed implementation. Therefore, we are adding the LoggerInterface interface.
interface LoggerInterface
{
public function logError(string $message): void;
}
And we use it in our DatabaseLogger:
class DatabaseLogger implements LoggerInterface
{
public function logError(string $message): void
{
// ..
}
}
Now, we can take advantage of Symfony Dependency Injection.
class MailerService
{
private LoggerInterface $logger;
public function sendEmail()
{
try {
// ..
} catch (SomeException $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
In this way, we can freely replace the logs in the database with logs wherever we want, as long as the detailed implementation implements the LoggerInterface. This change will not require modifying MailerService, because it does not depend on it, it depends only on the interface.
SOLID Principles PHP
All these principles come together as one, they often overlap. It's nice when you know the theory like SOLID principles because it makes it easier to make good code. Then you also have strong arguments behind your code, for example in code review. All the rules are aimed at making the code easy to understand and maintain.
SOLID is one of the many good practices that help us write clean code. I've written about the Boy Scout Rule before. But that's not all, there are many other rules and standards to follow. Let me just mention them:
- PSR (PHP Standards Recommendations) — PHP Framework Interop Group (PHP-FIG) is a group of people associated with the largest PHP projects who jointly develop PSR. I think every PHP programmer should know coding styles standards PSR-1 and PSR-12 (formerly PSR-2). You can find all the current sets of standards here
- KISS (Keep It Simple Stupid) — Don't complicate the code. The code should be its documentation itself. Any new programmer on the team should be able to get into the project quickly.
- DRY (Don’t Repeat Yourself) — Do not code using the Copy-Paste principle (there is no such rule). See that the same code repeats in several places? Extract code for a separate function.
- YAGNI (You Aren’t Gonna Need It) — 17th-century German philosopher Johannes Clauberg formulated a principle called Occam's Razor (I was also surprised Ockham was not its author ;) ) “entities should not be multiplied beyond necessity". I think this sentence expresses the YAGNI principle well. We should not write code “for the future”. Such code is not needed at the moment.
- GRASP (General Responsibility Assignment Software Patterns) — is a large set of rules about which I could write a separate article. These are the basic principles that we should follow when creating object design and responsibility assignments. It consists of Information Expert, Controller, Creator, High Cohesion, Low Coupling, Pure Fabrication, Polymorphism, Protected Variations and Indirection.
Applying the SOLID principles in our daily work helps us not get into technical debt. What are the consequences of incurring technical debt, you can find out in the article written by our CEO Piotr.
If you have problems understanding your project. Write to us, we have experience in dealing with difficult cases and PHP refactoring.