When I first started programming, I instantly got addicted to it. I felt so excited about the thought of coming up with a solution to a problem using programs and my own imagination. Back in school, there was a time when the instructor gave us the task of solving some simple algebraic challenges using Turbo-C. I had goosebumps and felt very excited as I quickly realized I could just write programs to solve these types of challenges repeatedly. Write the program once, pass different arguments, and get different results. I loved it. I remember a challenge to compute the height of a bridge if someone is standing on it, drops a ball, and hears a sound after several seconds. Easy! Now, I can just use my program to compute the height of the bridge for me repeatedly. Now, I don’t have to keep remembering that the Earth’s gravitational acceleration is at around 9.8 m/s2 – I can just declare it in the program! I learned that in programming, I can follow my own rules to get from point A to point B. Give me a task, and I can come up with a solution using my own imagination to finish the task. This, for me, is the best thing about programming. I was one proud spaghetti-code-writing machine. I didn’t care about how clean my code was – I just needed to solve problems using code! Learning about other programming languages made me even more excited, and I thought the possibilities were endless – if the task or challenge did not defy the laws of physics, I thought it could be solved using programming! I did not pay attention to code cleanliness or maintainability. What are those? I don’t need those!
When I started working professionally as a software developer, I continued with my mindset of just enjoying solving problems using programming. I didn’t care how disorganized my solutions were – they solved the problems, and my employers and clients were happy. Done, I’m out of here. Too easy. I thought I knew everything and that I was unstoppable. Oh boy, I was so wrong. The more I learned, the more I realized how little I knew how to program.
As I continued working on more complex projects with other developers while having to maintain these projects, I learned the hard way how difficult I had made my life by writing code I couldn’t easily maintain myself. I’m probably not the only developer on the planet to have experienced this problem. I was sure other people had encountered these issues before, and I was sure there were solutions out there. One of the solutions that helped make my life a lot easier was by trying to follow the SOLID principles by Robert C. Martin. They really helped change my programming life, and using these principles with Test-Driven Development (TDD) made my programming life even easier! There are more principles and architectural design patterns out there to help make your application more maintainable, but in this chapter, we will be focusing on the SOLID principles one by one while doing TDD.
We’ll go through the process of interpreting a Jira ticket into a BDD test, which, in turn, will help us in creating our integration tests, down to the development of the solution code. Then, one by one, we will go through each of the SOLID principles by using TDD as you would do in a real project.
In this chapter, we will go through the following topics:
In this chapter, the reader needs to use the base code from the repository found at https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/tree/main/Chapter%208.
First, get the base code for this chapter found at https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/tree/main/Chapter%206/base/phptdd or simply run the following command:
curl -Lo phptdd.zip "https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/raw/main/Chapter%208/base.zip" && unzip -o phptdd.zip && cd base && ./demoSetup.sh
To run the containers and execute the commands in this chapter, you should be inside the docker-server-web-1 container.
Run the following command to confirm the container name for our web server:
docker ps
To run the containers, run the following command from the /docker directory from the repository in your host machine:
docker-compose build && docker-compose up -d docker exec -it docker-server-web-1 /bin/bash
Once inside the container, run the following commands to install the libraries required through composer:
/var/www/html/symfony# ./setup.sh /var/www/html/behat# ./setup.sh
The SOLID principles, as defined by Robert C. Martin, are a set of coding guidelines or standards that help developers write more organized, decoupled, maintainable, extensible software. In this chapter, we’ll go through them one by one, but we will try to simulate the process by working on a real project and then implementing each of the principles.
In this chapter, we will be writing solution code that will try to adhere to the SOLID principles, but before that, we need an example problem to solve. As we did in Chapter 7, Building Solution Code with BDD and TDD, we’ll start with a Jira ticket, write some Gherkin features, write Behat tests, write integration and unit tests, and then write the SOLID-adhering solution code as depicted in the following flowchart:
Figure 8.1 – Development flow
Let’s use one of the Jira tickets we created in Chapter 2, Understanding and Organizing the Business Requirements for Our Project. We created a story to let a logged-in user input and save some toy car model data. This will be a nice simple feature to use to demonstrate the SOLID principles:
Figure 8.2 – Ticket for creating toy model data
As we did in Chapter 7, Building Solution Code with BDD and TDD, create a new git branch for your Jira ticket. Check out the git branch from the repository you set up in Chapter 2, Understanding and Organizing the Business Requirements for Our Project, and let’s start writing some tests and programs!
Before we start learning about the SOLID principles, first, we need to work on the BDD tests that will drive us to write the solution code while trying to follow the SOLID principles. Remember, we always need to start with failing tests. Next, to start with BDD, we need to write a Gherkin feature first.
Let’s start by writing a Gherkin feature to describe what behavior we expect to build. Create the following feature file with the following content inside the behat directory:
codebase/behat/features/create_toy_car_record.feature
Feature: Clerk creates new toy car record In order to have a collection of toy car model records As an Inventory Clerk I need to be able to create a single record Scenario: Create new record Given I am in the inventory system page When I submit the form with correct details Then I should see a success message
Now that we have our feature, let’s generate the Behat PHP context class for it.
We will now take the Gherkin feature and make a PHP context class for it. Follow these steps:
codebase/behat/behat.yml
default: suites: default: contexts: - FeatureContext - HomeContext suite_a: contexts: - InventoryClerkRegistrationContext suite_create: contexts: - CreateToyCarRecordContext
/var/www/html/behat# ./vendor/bin/behat --init
/var/www/html/behat# ./vendor/bin/behat features/
create_toy_car_record.feature --append-snippets –
suite=suite_create
/var/www/html/behat# ./vendor/bin/behat features/
create_toy_car_record.feature --suite=suite_create
You should then see the following test result:
Figure 8.3 – Failed test
Good – now, we know that the Behat test for this feature can be executed and fails as expected, so let’s move on to the Symfony application.
The Behat test we created is already a functional test – do we still have to create a functional test inside the Symfony directory? I think this is optional, but it will help us to quickly run basic smoke tests – for example, if we want to quickly check whether our controller loads and doesn’t encounter a fatal error. We don’t need to run the bigger and slower Behat test to find that out:
codebase/symfony/tests/Functional/Controller/InventoryAdminControllerTest.php
<?php namespace AppTestsFunctionalController; use SymfonyBundleFrameworkBundleTestWebTestCase; class InventoryControllerTest extends WebTestCase { public function testCanLoadIndex(): void { $client = static::createClient(); $client->request(‘GET’, ‘/inventory-admin’); $this->assertResponseIsSuccessful(); } }
/var/www/html/symfony# ./vendor/bin/phpunit --filter
InventoryAdminControllerTest
After running the test, make sure that you get a test failure. Remember the red phase?
Great – we can forget about creating the controller for now. Let’s move on to the integration tests. These tests will be used to develop the mechanism to persist the toy car model in the database.
We will now need to start writing integration tests that will help us write the code to persist or create a new toy car model. After passing these tests, then we can go back to the Behat tests we created earlier and make sure they pass:
codebase/symfony/tests/Integration/Processor/ToyCarProcessorTest.php
<?php namespace AppTestsIntegrationRepository; use SymfonyBundleFrameworkBundleTest KernelTestCase; class ToyCarProcessorTest extends KernelTestCase { public function testCanCreate() { $this->fail(“--- RED ---”); } }
/var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarRepositoryTest
Figure 8.4 – Failing processor test
Now that we have a failing integration test, let’s build the code to pass it. We want to be able to persist a new toy car model into the persistence layer which is our database. Do we even have a DB table for it? Nope, not yet. But we don’t care. We can continue working on the solution code. Next, we will be trying to follow the Single-Responsibility Principle (SRP) to write our solution code.
Let’s start with what I think is one of the most important principles in the SOLID principles. Are you familiar with god classes or objects – where one class can do almost everything? A single class for login, registration, displaying registered users, and so on? If there are two developers working on the same god class, can you already imagine how challenging that can be? And what happens after you deploy it to production and then an issue is found in the part where you display a list of registered users? You will have to change or fix that god class, but now the same class for login and registration has been modified and these processes may be compromised too. You run a bigger risk of introducing regressions to your login and registration functionalities by just trying to fix the list of registered users. You fix one feature, and there’s a greater risk of breaking other features.
This is where the SRP will start to make sense. The SRP mandates that a class should only have one main responsibility, and one reason to be changed. Is it that simple? Sometimes not. A Login class should only know about letting a user log in, and not have the program responsible for displaying a list of registered users or checking out a shopping cart, but sometimes where to draw the line can become very subjective.
Next, we’ll start writing the actual solution code while trying to implement the SRP.
We have a failing test that tests whether our application can create a toy car model and persist it in the database, but we don’t even have a database table for it yet. It’s okay – we will only focus on the PHP side of things for now.
It’s better for our processor PHP class to deal with objects and not to directly know about database table rows and so on. Let’s create a Plain Old PHP Object (POPO) that will represent what a toy car model is, without caring about the database structure:
codebase/symfony/src/Model/ToyCar.php
<?php namespace AppModel; class ToyCar { /** * @var int */ private $id; /** * @var string */ private $name; /** * @var CarManufacturer */ private $manufacturer; /** * @var ToyColor */ private $colour; /** * @var int */ private $year; }
After declaring the properties, it’s best to generate the accessors and mutators for all of these properties, rather than accessing them directly.
As you can see, this is just a POPO class. Nothing fancy. No information whatsoever about how to persist it in our database. Its responsibility is just to be a model that represents what a toy car is.
codebase/symfony/src/Model/ToyColor.php
<?php namespace AppModel; class ToyColor { /** * @var int */ private $id; /** * @var string */ private $name; }
After declaring the properties, generate the accessors and mutators for the class.
codebase/symfony/src/Model/CarManufacturer.php
<?php namespace AppModel; class CarManufacturer { /** * @var int */ private $id; /** * @var string */ private $name; }
Now, generate the accessors and mutators for this class as well.
Now, we have the main ToyCar model, which is also using the ToyColor and CarManufacturer models. As you can see, as with the ToyCar model, these two classes are not responsible for persisting or reading data either.
As you remember, we are using the Doctrine ORM as a tool to interact with our database. We can also use Doctrine entities directly in our processor class if we want to, but that would mean that our processor class would now be using a class that has a dependency on Doctrine. What if we need to use a different ORM? To keep things a little bit less coupled, we will just use codebase/symfony/src/Model/ToyCar.php in the processor class we will be creating next.
For us to create and persist a toy car model, we will need a class that will need to process it for us. The thing is, we still don’t have a database at this stage – where do we persist the toy car model? For now, nowhere, but we can still pass the test:
codebase/symfony/src/DAL/Writer/WriterInterface.php
<?php namespace AppDALWriter; interface WriterInterface { /** * @param $model * @return bool */ public function write($model): bool; }
We created a very simple interface that our data-writer objects can implement. We’ll then use this interface for our processor class.
codebase/symfony/src/Processor/ToyCarProcessor.php
<?php namespace AppProcessor; use AppDALWriterWriterInterface; use AppModelToyCar; use AppValidatorToyCarValidationException; class ToyCarProcessor { /** * @var WriterInterface */ private $dataWriter; /** * @param ToyCar $toyCar * @return bool * @throws ToyCarValidationException */ public function create(ToyCar $toyCar) { // Do some validation here $this->validate($toyCar); // Write the data $result = $this->getDataWriter()-> write($toyCar); // Do other stuff. return $result; } /** * @param ToyCar $toyCar * @throws ToyCarValidationException */ public function validate(ToyCar $toyCar) { if (is_null($toyCar->getName())) { throw new ToyCarValidationException (‘Invalid Toy Car Data’); } } /** * @return WriterInterface */ public function getDataWriter(): WriterInterface { return $this->dataWriter; } /** * @param WriterInterface $dataWriter */ public function setDataWriter(WriterInterface $dataWriter): void { $this->dataWriter = $dataWriter; } }
We have created a processor class that has a create method that accepts the toy car model we created previously, and then tries to write the model using an instance of a writer class that doesn’t exist. What if another developer in your company is working on the data-writer class and it will take him 2 weeks to complete it? Do you wait for 2 weeks to pass your integration test?
If your processor class must validate the data and do other things after the data has been written into the database, should those programs be delayed too just because you are waiting for the other developer to complete their work? Probably not! We can use test doubles to replace the missing dependencies for now.
Most of the time, it’s just difficult or impractical to be able to run a test against a feature with all its dependencies already built. Sometimes, we need to have a solution to be able to test the specific feature we want, even if we have not built the other dependencies yet, or simply want to isolate or only focus our test on a certain feature. Here, we can use test doubles. You can read more about test doubles for PHPUnit at https://phpunit.readthedocs.io/en/9.5/test-doubles.html.
The processor class we just created needs a concrete instance of ToyValidatorInterface and WriterInterface. Since we have not created those classes yet, we can still proceed in passing the test just by using a Mock object. In PHPUnit, the Mock object is an interface that extends the Stub interface. This means that in the code, a Mock object is an implementation of a Stub interface. The process of replacing the instances of ToyValidatorInterface and WriterInterface with a Mock object and setting a return value when a specific method is executed is called stubbing. Let’s try it for real:
codebase/symfony/tests/Integration/Processor/ToyCarProcessorTest.php
<?php namespace AppTestsIntegrationRepository; use AppDALWriterWriterInterface; use AppModelCarManufacturer; use AppModelToyCar; use AppModelToyColor; use AppProcessorToyCarProcessor; use SymfonyBundleFrameworkBundleTest KernelTestCase; class ToyCarProcessorTest extends KernelTestCase { /** * @param ToyCar $toyCarModel * @throws AppValidator ToyCarValidationException * @dataProvider provideToyCarModel */ public function testCanCreate (ToyCar $toyCarModel): void { // Mock: Data writer $toyWriterStub = $this->createMock (WriterInterface::class); $toyWriterStub ->method(‘write’) ->willReturn(true); // Processor Class $processor = new ToyCarProcessor(); $processor->setDataWriter($toyWriterStub); // Execute $result = $processor->create($toyCarModel); $this->assertTrue($result); } public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘Black’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Ford’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘Mustang’); $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer ($carManufacturer); $toyCarModel->setYear(1968); return [ [$toyCarModel], ]; } }
In the testCanCreate function here, we are creating mock objects for the ValidationModel, ToyCarValidator, and ToyCarWriter classes. We then instantiate the main ToyCarCreator class while passing the mock ToyCarValidator and ToyCarWriter classes into its constructor. This is called dependency injection, which will be discussed further later in the chapter. Lastly, we then run the ToyCarCreator’s create method to simulate a developer trying to create a new toy car record:
/var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarProcessorTest
You should then see the following result:
Figure 8.5 – Passed the test using a stub
We passed the test, even though we have not really persisted anything in the database yet. It’s very common in bigger and more complex projects that you’ll have to rely on test doubles just to isolate and focus on your test even if other dependencies are either not built yet or are too cumbersome to include as a part of your test.
Now going back to the SRP, our ToyCarProcessor now has two responsibilities – to validate and create a toy car model. Equally, other developers are using your class’s validate method. Let’s refactor our code to redefine the focus and responsibility of our ToyCarProcessor class:
codebase/symfony/tests/Integration/Processor/ToyCarCreatorTest.php
<?php namespace AppTestsIntegrationRepository; use AppDALWriterWriterInterface; use AppModelCarManufacturer; use AppModelToyCar; use AppModelToyColor; use AppProcessorToyCarCreator; use AppValidatorValidatorInterface; use SymfonyBundleFrameworkBundleTestKernelTestCase; class ToyCarCreatorTest extends KernelTestCase { /** * @param ToyCar $toyCarModel * @throws AppValidator ToyCarValidationException * @dataProvider provideToyCarModel */ public function testCanCreate (ToyCar $toyCarModel): void { // Mock 1: Validator $validatorStub = $this->createMock (ValidatorInterface::class); $validatorStub ->method(‘validate’) ->willReturn(true); // Mock 2: Data writer $toyWriterStub = $this->createMock (WriterInterface::class); $toyWriterStub ->method(‘write’) ->willReturn(true); // Processor Class $processor = new ToyCarCreator(); $processor->setValidator($validatorStub); $processor->setDataWriter($toyWriterStub); // Execute $result = $processor->create($toyCarModel); $this->assertTrue($result); } public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘Black’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Ford’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘Mustang’); $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer ($carManufacturer); $toyCarModel->setYear(1968); return [ [$toyCarModel], ]; } }
As you can see, we added a new Mock object for the validation. I will explain why we must do that after we refactor the content of the ToyCarCreator.php class. Let’s create a validator interface, and then refactor the ToyCarCreator class.
codebase/symfony/src/Validator/ValidatorInterface.php
<?php namespace AppValidator; interface ValidatorInterface { /** * @param $input * @return bool * @throws ToyCarValidationException */ public function validate($input): bool; }
<?php
namespace AppProcessor;
use AppDALWriterWriterInterface;
use AppModelToyCar;
use AppValidatorToyCarValidationException;
use AppValidatorValidatorInterface;
class ToyCarCreator
{
/**
* @var ValidatorInterface
*/
private $validator;
/**
* @var WriterInterface
*/
private $dataWriter;
/**
* @param ToyCar $toyCar
* @return bool
* @throws ToyCarValidationException
*/
public function create(ToyCar $toyCar): bool
{
// Do some validation here and so on...
$this->getValidator()->validate($toyCar);
// Write the data
$result = $this->getDataWriter()->write
($toyCar);
// Do other stuff.
return $result;
}
}
Next, add the necessary accessors and mutators for the private properties we have declared in the class.
We renamed the class just to give it a more specific name. Sometimes, just naming the class to something else helps you clean up your code. Also, you will notice that we have removed the publicly visible validate class. This class will no longer contain any validation logic – it only knows that it will run a validation routine before it tries to persist the data. This is the class’s main responsibility.
We still have not written any validation and data persistence code, but let’s see whether we can still pass the test to test the main responsibility of the class, which is to do the following:
/var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarCreatorTest
Now, you should see the following result:
Figure 8.6 – Passing the test using two stubs
In this section, we used BDD and TDD to direct us into writing the solution code. We have created POPOs with a single responsibility. We have also created a ToyCarCreator class that does not contain the validation logic, nor the persistence mechanism. It knows it needs to do some validation and some persistence, but it does not have the concrete implementation of those programs. Each class will have its own specialization or a specific job, or a specific single responsibility.
Great – now, we can pass the test again even after refactoring. Next, let’s continue writing the solution code by following the O in the SOLID principle, which is the Open-Closed Principle (OCP).
The OCP was first defined by Bertrand Meyer, but in this chapter, we will follow the later version defined by Robert C. Martin, which is also called the polymorphic OCP.
The OCP states that objects should be open to extension and closed to modification. The aim is that we should be able to modify the behaviour or a feature by extending the original code instead of directly refactoring the original code. That’s great because that will help us developers and testers be more confident about the ticket we’re working on, as we haven’t touched the original code that might be used somewhere else – less risk of regression.
In our ToyCarCreateTest class, we are stubbing a validator object because we have not written a concrete validator class yet. There are a lot of different ways of implementing validation, but for this example, we’ll try to make it very simple. Let’s go back to the code and create a validator:
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
<?php namespace AppTestsUnitValidator; use AppModelCarManufacturer; use AppModelToyCar; use AppModelToyColor; use AppValidatorToyCarValidator; use PHPUnitFrameworkTestCase; class ToyCarValidatorTest extends TestCase { /** * @param ToyCar $toyCar * @param bool $expected * @dataProvider provideToyCarModel */ public function testCanValidate(ToyCar $toyCar, bool $expected): void { $validator = new ToyCarValidator(); $result = $validator->validate($toyCar); $this->assertEquals($expected, $result); } public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘White’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Williams’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘’); // Should fail. $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer ($carManufacturer); $toyCarModel->setYear(2004); return [ [$toyCarModel, false], ]; } }
After creating the test class, as usual, we need to run the test to make sure that PHPUnit recognizes your test.
/var/www/html/symfony# ./vendor/bin/phpunit --testsuite=Unit --filter ToyCarValidatorTest
Make sure that you get an error, as we have not created the validator class yet. Remember the red phase? You’ll notice that in the data provider, we have set an empty string for the name. We will make the validator class return false whenever it sees an empty string for the toy car name.
codebase/symfony/src/Validator/ToyCarValidator.php
<?php namespace AppValidator; use AppModelToyCar; class ToyCarValidator { public function validate(ToyCar $toyCar): bool { if (!$toyCar->getName()) { return false; } return true; } }
We have created a very simple validation logic where we only check for the toy car’s name if it’s not an empty string. Now, let’s run the test again.
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter ToyCarValidatorTest
You should now see a passing test.
Okay, so for now, we can make sure that the toy car model’s name should always be a string that is not empty – but here’s the thing, what if we want to add more validation logic? We will have to keep on modifying the ToyCarValidator class. That’s not wrong. It’s just that it’s arguably better to follow the OCP so that we don’t keep modifying our code – less class modification, less risk of breaking things. Let’s refactor our solution code to pass the test again:
codebase/symfony/src/Validator/ToyCarValidatorInterface.php
<?php namespace AppValidator; use AppModelToyCar; use AppModelValidationModel; interface ToyCarValidatorInterface { public function validate(ToyCar $toyCar): ValidationModel; }
codebase/symfony/src/Model/ValidationModel.php
<?php namespace AppModel; class ValidationModel { /** * @var bool */ private $valid = false; /** * @var array */ private $report = []; }
After creating the class, generate the accessors and mutators for the properties.
Instead of simply returning true or false on our validation program, we can now return an array containing the field name and validation result for that field name as well. Let’s continue coding.
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
<?php namespace AppTestsUnitValidator; use PHPUnitFrameworkTestCase; use AppValidatorYearValidator; class YearValidatorTest extends TestCase { /** * @param $data * @param $expected * @dataProvider provideYear */ public function testCanValidateYear(int $year, bool $expected): void { $validator = new YearValidator(); $isValid = $validator->validate($year); $this->assertEquals($expected, $isValid); } /** * @return array */ public function provideYear(): array { return [ [1, false], [2005, true], [1955, true], [312, false], ]; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter YearValidatorTest --debug
If the test fails, that’s good. Let’s proceed with the solution code:
codebase/symfony/src/Validator/YearValidator.php
<?php namespace AppValidator; class YearValidator implements ValidatorInterface { /** * @param $input * @return bool */ public function validate($input): bool { if (preg_match(“/^(d{4})$/”, $input, $matches)) { return true; } return false; } }
Now, we have a simple validation class for checking whether a year is acceptable for our car. If we want to add more logic here, such as checking for the minimum and maximum acceptable value, we can put all that logic here.
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter YearValidatorTest --debug
You should see the following result:
Figure 8.7 – Simple date validation test
Now that we have passed the very simple test for the year validator, next, let’s move on to the name validator:
codebase/symfony/tests/Unit/Validator/NameValidatorTest.php
<?php namespace AppTestsUnitValidator; use AppValidatorNameValidator; use PHPUnitFrameworkTestCase; class NameValidatorTest extends TestCase { /** * @param $data * @param $expected * @dataProvider provideNames */ public function testCanValidateName(string $name, bool $expected): void { $validator = new NameValidator(); $isValid = $validator->validate($name); $this->assertEquals($expected, $isValid); } /** * @return array */ public function provideNames(): array { return [ [‘’, false], [‘$50’, false], [‘Mercedes’, true], [‘RedBull’, true], [‘Williams’, true], ]; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter NameValidatorTest
codebase/symfony/src/Validator/NameValidator.php
<?php namespace AppValidator; class NameValidator implements ValidatorInterface { public function validate($input): bool { if (preg_match(“/^([a-zA-Z’ ]+)$/”, $input)) { return true; } return false; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter NameValidatorTest
You should now see five passing tests.
Let’s summarize what we have added so far. We created two new validation classes, and both are working as expected based on our unit tests – but how is this better than the first solution we created? How is this relevant to the OCP? Well, first we need to tie things together and pass the bigger ToyCarValidatorTest.
codebase/symfony/src/Validator/ToyCarValidator.php
<?php namespace AppValidator; use AppModelToyCar; use AppModelValidationModel as ValidationResult; class ToyCarValidator implements ToyCarValidatorInterface { /** * @var array */ private $validators = []; public function __construct() { $this->setValidators([ ‘year’ => new YearValidator(), ‘name’ => new NameValidator(), ]); } /** * @param ToyCar $toyCar * @return ValidationResult */ public function validate(ToyCar $toyCar ValidationResult { $result = new ValidationResult(); $allValid = true; foreach ($this->getValidators() as $key => $validator) { $accessor = ‘get’ . ucfirst(strtolower ($key)); $value = $toyCar->$accessor(); $isValid = false; try { $isValid = $validator->validate ($value); $results[$key][‘message’] = ‘’; } catch (ToyCarValidationException $ex) { $results[$key][‘message’] = $ex-> getMessage(); } finally { $results[$key][‘is_valid’] = $isValid; } if (!$isValid) { $allValid = false; } } $result->setValid($allValid); $result->setReport($results); return $result; } }
Then, generate the accessors and mutators for the $validators property.
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
<?php namespace AppTestsUnitValidator; use AppModelCarManufacturer; use AppModelToyCar; use AppModelToyColor; use AppValidatorToyCarValidator; use PHPUnitFrameworkTestCase; class ToyCarValidatorTest extends TestCase { /** * @param ToyCar $toyCar * @param array $expected * @dataProvider provideToyCarModel */ public function testCanValidate(ToyCar $toyCar, array $expected): void { $validator = new ToyCarValidator(); $result = $validator->validate($toyCar); $this->assertEquals($expected[‘is_valid’], $result->isValid()); $this->assertEquals($expected[‘name’], $result->getReport()[‘name’][‘is_valid’]); $this->assertEquals($expected[‘year’], $result->getReport()[‘year’][‘is_valid’]); } public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘White’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Williams’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘’); // Should fail. $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer ($carManufacturer); $toyCarModel->setYear(2004); return [ [$toyCarModel, [‘is_valid’ => false, ‘name’ => false, ‘year’ => true]], ]; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter ToyCarValidatorTest
Now, you should see the following result:
Figure 8.8 – Passing toy car validation test
You will notice that we passed three assertions. It looks like we are starting to get a test with more responsibilities. It’s still better to do one assertion per test, just so that we don’t end up having a god test class! For now, we’ll move on.
Now, what have we achieved by refactoring? Well, first, we no longer have the validation logic for checking the validity of the toy name inside the ToyCarValidatorTest class. Second, we can now check for the validity of the year. If we want to improve the date and name validation logic, we won’t have to do it in the main ToyCarValidator class – but what if we want to add more validator classes? Such as a ToyColorValidator class? Well, we can still do that without even touching the main class! We’ll refactor ToyCarValidator and discuss how to do so later in the chapter in the TDD with the Dependency Inversion Principle section.
But what if we want to change the entire behavior of the ToyCarValidator.php class we created and change the logic entirely? Well, there’s no need to modify it – we can just replace the entire ToyCarValidator.php class with a different concrete implementation of the ToyCarValidatorInterface interface!
Next, we’ll talk about the Liskov Substitution Principle (LSP).
The LSP was introduced by Barbara Liskov. The way that I use it is that an implementation of an interface should be replaceable with another implementation of that interface without changing the behavior. If you are extending a superclass, the child class must be able to substitute the superclass without breaking the behavior.
In this example, let’s try adding a business rule to reject toy car models that were built on or before 1950.
As usual, let’s start with a test:
codebase/symfony/tests/Unit/Validator/YearValidatorTest.php
<?php namespace AppTestsUnitValidator; use AppValidatorToyCarTooOldException; use PHPUnitFrameworkTestCase; use AppValidatorYearValidator; class YearValidatorTest extends TestCase { /** * @param $data * @param $expected * @dataProvider provideYear */ public function testCanValidateYear(int $year, bool $expected): void { $validator = new YearValidator(); $isValid = $validator->validate($year); $this->assertEquals($expected, $isValid); } /** * @return array */ public function provideYear(): array { return [ [1, false], [2005, true], [1955, true], [312, false], ]; } /** * @param int $year * @dataProvider provideOldYears */ public function testCanRejectVeryOldCar(int $year): void { $this->expectException (ToyCarTooOldException::class); $validator = new YearValidator(); $validator->validate($year); } /** * @return array */ public function provideOldYears(): array { return [ [1944], [1933], [1922], [1911], ]; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
codebase/symfony/src/Validator/ToyCarTooOldException.php
<?php namespace AppValidator; class ToyCarTooOldException extends Exception { }
As you can see, it’s just a simple exception class that extends the main PHP Exception class.
If we run the test again, we should now pass the test, as we have told PHPUnit that we are expecting exceptions for this test by using the $this->expectException() method.
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
Now, we should be able to pass the test – you should see the following result:
Figure 8.9 – Passing the old car rejection test
This means that we are correctly throwing the ToyCarTooOldException object whenever we submit a year that is less than or equal to 1950 – but what will happen to our ToyCarValidatorTest?
Let’s modify the test data with a year less than 1950 and see what happens:
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘White’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Williams’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘’); // Should fail. $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer($carManufacturer); $toyCarModel->setYear(1935); return [ [$toyCarModel, [‘is_valid’ => false, ‘name’ => false, ‘year’ => false]], ]; }
/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
You will notice that we have failed the test with the following message:
Figure 8.10 – Failed toy car validation
Now, we can see that we have an uncaught exception. Our ToyCarValidator program is not programmed to handle this exception object. Why is that? Well, the interface in this example is the codebase/symfony/src/Validator/ValidatorInterface.php interface. This interface throws a ToyCarValidationException object. The problem now is that our implementing class, the YearValidator.php class, throws a different exception compared to its contract or interface. Therefore, it breaks the behavior. To fix this problem, we simply need to throw the correct exception as declared in the interface.
codebase/symfony/src/Validator/ToyCarTooOldException.php
<?php namespace AppValidator; class ToyCarTooOldException extends ToyCarValidationException { }
As you can see, we simply replaced the class it extends to ToyCarValidationException. The ToyCarValidator.php class is designed to catch this exception.
/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
We should now pass the test and see the following result:
Figure 8.11 – Passing the toy car validator test, with old car validation
/var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
Figure 8.12 – Validation model
You can see that our ToyCarValidator’s validate method returns a ValidationModel object. It gives a summary of the fields we validated for, as well as the exception message for the year field.
We’ve seen how interfaces can be useful, but sometimes they become too powerful. Next, we’ll talk about the Interface Segregation Principle (ISP) to help stop this from happening.
Interfaces are very helpful, but sometimes it can be very easy to pollute them with capabilities that are not really supposed to be a part of the interface. I used to encounter this violation a lot. I was asking myself how I kept on creating empty methods with to-do comments, only to find classes a few months or years later, still with those to-do comments and the methods still empty.
I used to touch my interfaces first and stuff them with all the methods I thought I needed. Then, when I finally wrote the concrete implementations, these concrete classes mostly had empty methods in them.
An interface should only have methods that are specific to that interface. If there’s a method in there that is not entirely related to that interface, you need to segregate it into a different interface.
Let’s see that in action. Again, let’s start with a – you guessed it right – test:
/**
* @param $data
* @param $expected
* @dataProvider provideLongNames
*/
public function testCanValidateNameLength(string
$name, bool $expected): void
{
$validator = new NameValidator();
$isValid = $validator->validateLength($name);
$this->assertEquals($expected, $isValid);
}
/**
* @return array
*/
public function provideLongNames(): array
{
return [
[‘TheQuickBrownFoxJumpsOverTheLazyDog’,
false],
];
}
We introduced a new function in the test called validateLength, which is common for strings. We also added a very long name, and we set false to be expected to be returned in the data provider.
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength
You should get an error, as we have not created the new method yet.
codebase/symfony/src/Validator/ValidatorInterface.php
<?php namespace AppValidator; interface ValidatorInterface { /** * @param $input * @return bool * @throws ToyCarValidationException */ public function validate($input): bool; /** * @param string $input * @return bool */ public function validateLength(string $input): bool; }
Figure 8.13 – Must implement the method
Obviously, we need to implement the validateLength method for the NameValidator.php class, which is okay, as we want to validate the string length – but what would happen if we also wanted to create a validator for the ToyCar model’s color? The ToyCar model’s color property expects a ToyColor.php object, not a string! Therefore, the solution is to delete the validateLength method from ValidatorInterface. Certain classes will implement ValidatorInterface without the need to implement this logic. What we can do instead is create a new interface called the StringValidator interface that can have the validateLength method.
codebase/symfony/src/Validator/StringValidatorInterface.php
<?php namespace AppValidator; interface StringValidatorInterface { /** * @param string $input * @return bool */ public function validateLength(string $input): bool; }
At this stage, we have segregated the validateLength method into a separate interface, removing it from the ValidatorInterface.php interface.
codebase/symfony/src/Validator/NameValidator.php
<?php namespace AppValidator; class NameValidator implements ValidatorInterface, StringValidatorInterface { const MAX_LENGTH = 10; /** * @param $input * @return bool */ public function validate($input): bool { $isValid = false; if (preg_match(“/^([a-zA-Z’ ]+)$/”, $input)) { $isValid = true; } if ($isValid) { $isValid = $this->validateLength($input); } return $isValid; } /** * @param string $input * @return bool */ public function validateLength(string $input): bool { if (strlen($input) > self::MAX_LENGTH) { return false; } return true; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength
Now, you should see the following result:
Figure 8.14 – Passing the string length validation test
What we did is instead of combining different methods into ValidatorInterface, we segregated them into two different interfaces. Then, we only implement the StringValidator interface for the validator objects that will need this validateLength method. That’s basically what the ISP is all about. This is a very basic example, but it is very easy to fall victim to these very powerful interfaces if you don’t watch out.
Next, we will go back to the ToyCarValidator class and see how we can improve what we had earlier in the TDD with the Open-Closed Principle example, using the Dependency Inversion Principle (DIP).
In terms of making a class more testable, the DIP is probably the most important principle on the list for me. The DIP suggests that details should depend on abstractions. To me, this means that the specifics of a program that does not really belong to a class should be abstracted. The DIP allows us as developers to remove a concrete implementation of a routine or program and put it in a different object altogether. We can then use the DIP to inject the object that we need, whenever we need it. We can inject the object that we need in the constructor, passed as an argument upon class instantiation, or simply expose a mutator function.
Let’s revisit the ToyCarValidator class that we created earlier in this chapter to see how we can implement the DIP.
How will this look in our code?
Going back to the ToyCarValidator.php class, you will notice that in the __constructor method, we have instantiated two classes:
Figure 8.15 – Hardcoded dependencies
How can we improve this? Well, this program works – as you have seen, we are passing ToyCarValidatorTest. The only problem is that our ToyCarValidator class is now hardcoded to its dependencies – the YearValidator and NameValidator classes. What if we want to replace these classes – or what if we want to add more validators? Well, what we can do is remove the dependency from inside of the class. Follow these steps:
codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
/** * @param ToyCar $toyCar * @param array $expected * @dataProvider provideToyCarModel */ public function testCanValidate(ToyCar $toyCar, array $expected): void { $validators = [ ‘year’ => new YearValidator(), ‘name’ => new NameValidator(), ]; // Inject the validators $validator = new ToyCarValidator(); $validator->setValidators($validators); $result = $validator->validate($toyCar); $this->assertEquals($expected[‘is_valid’], $result->isValid()); $this->assertEquals($expected[‘name’], $result->getReport()[‘name’][‘is_valid’]); $this->assertEquals($expected[‘year’], $result->getReport()[‘year’][‘is_valid’]); }
You will notice that the objects that ToyCarValidator depends on are now being instantiated outside the ToyCarValidator class – and we then set the validators using the setValidators mutator.
codebase/symfony/src/Validator/ToyCarValidator.php
<?php namespace AppValidator; use AppModelToyCar; use AppModelValidationModel as ValidationResult; class ToyCarValidator implements ToyCarValidatorInterface { /** * @var array */ private $validators = []; /** * @param ToyCar $toyCar * @return ValidationResult */ public function validate(ToyCar $toyCar): ValidationResult { $result = new ValidationResult(); $allValid = true; $results = []; foreach ($this->getValidators() as $key => $validator) { $accessor = ‘get’ . ucfirst(strtolower ($key)); $value = $toyCar->$accessor(); $isValid = false; try { $isValid = $validator->validate ($value); $results[$key][‘message’] = ‘’; } catch (ToyCarValidationException $ex) { $results[$key][‘message’] = $ex->getMessage(); } finally { $results[$key][‘is_valid’] = $isValid; } if (!$isValid) { $allValid = false; } } $result->setValid($allValid); $result->setReport($results); return $result; } /** * @return array */ public function getValidators(): array { return $this->validators; } /** * @param array $validators */ public function setValidators(array $validators): void { $this->validators = $validators; } }
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength
After running the command, you should see that the tests still pass. At this point, we can keep creating new validators and just add them to the array of validators we want to inject into the ToyCarValidator.php class.
codebase/symfony/tests/Integration/Processor/ToyCarCreatorTest.php
<?php namespace AppTestsIntegrationRepository; use AppDALWriterWriterInterface; use AppModelCarManufacturer; use AppModelToyCar; use AppModelToyColor; use AppModelValidationModel; use AppProcessorToyCarCreator; use AppValidatorToyCarValidatorInterface; use SymfonyBundleFrameworkBundleTestKernelTestCase; class ToyCarCreatorTest extends KernelTestCase { /** * @param ToyCar $toyCarModel * @throws AppValidator ToyCarValidationException * @dataProvider provideToyCarModel */ public function testCanCreate(ToyCar $toyCarModel): void { $validationResultStub = $this->createMock (ValidationModel::class); $validationResultStub ->method(‘isValid’) ->willReturn(true); // Mock 1: Validator $validatorStub = $this->createMock (ToyCarValidatorInterface::class); $validatorStub ->method(‘validate’) ->willReturn($validationResultStub); // Mock 2: Data writer $toyWriterStub = $this->createMock (WriterInterface::class); $toyWriterStub ->method(‘write’) ->willReturn(true); // Processor Class $processor = new ToyCarCreator($validatorStub, $toyWriterStub); // Execute $result = $processor->create($toyCarModel); $this->assertTrue($result); } public function provideToyCarModel(): array { // Toy Car Color $toyColor = new ToyColor(); $toyColor->setName(‘Black’); // Car Manufacturer $carManufacturer = new CarManufacturer(); $carManufacturer->setName(‘Ford’); // Toy Car $toyCarModel = new ToyCar(); $toyCarModel->setName(‘Mustang’); $toyCarModel->setColour($toyColor); $toyCarModel->setManufacturer ($carManufacturer); $toyCarModel->setYear(1968); return [ [$toyCarModel], ]; } }
As you can see, we have instantiated the dependencies of the ToyCarCreator.php class and then injected them as a parameter when we instantiated the class in ToyCarCreator($validatorStub, $toyWriterStub);.
codebase/symfony/src/Processor/ToyCarCreator.php
<?php namespace AppProcessor; use AppDALWriterWriterInterface; use AppModelToyCar; use AppValidatorToyCarValidationException; use AppValidatorToyCarValidatorInterface; class ToyCarCreator { /** * @var ToyCarValidatorInterface */ private $validator; /** * @var WriterInterface */ private $dataWriter; public function __construct (ToyCarValidatorInterface $validator, WriterInterface $dataWriter) { $this->setValidator($validator); $this->setDataWriter($dataWriter); } /** * @param ToyCar $toyCar * @return bool * @throws ToyCarValidationException */ public function create(ToyCar $toyCar): bool { // Do some validation here and so on... $this->getValidator()->validate($toyCar); // Write the data $result = $this->getDataWriter()->write ($toyCar); // Do other stuff. return $result; } /** * @return WriterInterface */ public function getDataWriter(): WriterInterface { return $this->dataWriter; } /** * @param WriterInterface $dataWriter */ public function setDataWriter(WriterInterface $dataWriter): void { $this->dataWriter = $dataWriter; } /** * @return ToyCarValidatorInterface */ public function getValidator(): ToyCarValidatorInterface { return $this->validator; } /** * @param ToyCarValidatorInterface $validator */ public function setValidator (ToyCarValidatorInterface $validator): void { $this->validator = $validator; } }
Upon instantiation, both the validator and writer dependencies are set through the constructor.
If we run the test, it should still pass:
/var/www/html/symfony# ./runDebug.sh --testsuite=Integration --filter ToyCarCreatorTest
After running the command, you should still see a passing test.
The most obvious thing that you will notice with this approach is that you will have to manage all the dependencies yourself and then inject them into the object that needs them. Luckily, we are not the first people to encounter this headache. There are a lot of service containers out there that help manage the dependencies that your application needs, but the most important thing when selecting a service container for PHP is that it should follow the PSR-11 standards. You can read more about PSR-11 at https://www.php-fig.org/psr/psr-11/.
In this chapter, we’ve gone through the SOLID principles one by one. We used our tests to kickstart the development of our solution code so that we can use them as examples for implementing the SOLID principles in real life.
We have covered the SRP, which helped us make a PHP class’s responsibility or capability more focused. The OCP helped us avoid the need for touching or modifying a class in some instances when we want to change its behavior. The LSP helped us be stricter about the behavior of an interface, making it easier for us to switch concrete objects implementing that interface without breaking the parent class’s behavior. The ISP helped us make the responsibility of an interface more focused – classes that implement this interface will no longer have empty methods just because they were declared by the interface. The DIP helped us quickly test our ToyCarCreator class even without creating a concrete implementation of its dependencies, such as the ToyCarValidator class.
When working on real-life projects, some principles are hard to strictly follow, and sometimes the boundaries are vague. Add the pressure of real-life deadlines and it gets even more interesting. One thing is for sure, using BDD and TDD will help you be more confident about the features you are developing, especially when you are already a few months deep into a project. Adding SOLID principles on top of that makes your solution even better!
In the next chapter, we will try to utilize automated tests to help us make sure that any code changes that any developer in your team pushes into your code repository will not break the expected behavior of your software. We will try to automate this process by using Continuous Integration.