Clean Code Architecture and Test Driven Development in PHP

Share this article

The Clean Code Architecture was introduced by Robert C. Martin on the 8light blog. The idea was to create an architecture which is independent of any external agency. Your business logic should not be coupled to a framework, a database, or to the web itself. With the independence, you have several advantages. For example, you have the ability to defer technical decisions to a later point during development (e.g. choosing a framework and choosing a database engine/provider). You can also easily switch the implementations or compare different implementations, but the biggest advantage is that your tests will run fast.

Just think about it. Do you really have to run through a router, load a database abstract layer or some ORM magic, or execute some other code just to assert one or more results?

I started to learn and practice this architecture because of my old favorite framework Kohana. At some point, the core developer stopped maintaining the code, which also meant that my projects would not get any further updates or security fixes. This meant that I had to either move to another framework and rewrite the entire project or trust the community development version.

Clean Code Architecture

I could have chosen to go with another framework. Maybe it would have been better to go with Symfony 1 or Zend 1, but by now that framework would have also changed.

Frameworks will continue to change and evolve. With composer, it is easy to install and replace packages, but it is also easy to abandon a package (composer even has the option to mark a package as abandoned), so it is easy to make “the wrong choice”.

In this tutorial, I will show you how we can implement the Clean Code Architecture in PHP, in order to be in control of our own logic, without being dependent on external providers, but while still using them. We will create a simple guestbook application.


The image above shows the different layers of the application. The inner layers do not know anything about the outer layers and they all communicate via interfaces.

The most interesting part is in the bottom-right corner of the image: Flow of control. The image explains how the framework communicates with our business logic. The Controller passes its data to the input port, which is processed by an interactor to produce an output port which contains data for the presenter.

We will start with the UseCase layer, since this is the layer which contains our application-specific-logic. The Controller layer and other outer layers belong to a Framework.

Note that all the various stages described below can be cloned and tested from this repo, which was neatly arranged into steps with the help of Git tags. Just download the corresponding step if you’d like to see it in action.

First test

We usually begin from the UI point of view. What should we expect to see if we visit a guestbook? There should be some kind of input form, the entries from other visitors, and maybe a navigation panel to search through pages of entries. If the guestbook is empty, we might see a message like “No entries found”.

In our first test we want to assert an empty list of entries, it looks like this:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
}

In this test, I used a slightly different notation than Uncle Bob. The Interactors are UseCases, Input Ports are Requests, and Output Ports are Responses.

The UseCases always contain the method process which has a type hint to its specific Request and Response interface.

According to the Red, Green, and Refactor cycles in TDD, this test should and will fail, because the classes do not exist.

After creating the class files, methods, and properties, the test passes.
Since the classes are empty, we do not need to use the Refactor cycle at this point.

Next, we want to assert that we can actually see some entries.

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
    public function testCanSeeEntries()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertNotEmpty($response->entries);
    }
}

As we can see, the test fails, and we are in the red section of the TDD cycle. To make the test pass we have to add some logic into our UseCases.

Sketch out the UseCase logic

Before we start with the logic, we apply the parameter type hints and create the interfaces.

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;

class ViewEntriesUseCase
{
    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){

    }
}

This is similar to how graphic artists often work. Instead of drawing the entire picture from beginning to end, they usually draw some shapes and lines to have an idea of what the finished picture might be. Afterwards, they use the shapes as guides and add more details. This process is called “Sketching”.

Instead of shapes and lines, we use, for example, Repositories and Factories as our guides.

The Repository is an abstract layer for retrieving data from a storage. The storage could be a database, it could be a file, an external API or even in memory.

To view the guestbook entries, we have to find the entities in our repository, convert them to views, and add them to the response.

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;

class ViewEntriesUseCase
{
    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
		$entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());

        if(!$entries){
            return;
        }

        foreach($entries as $entry){
            $entryView = $this->entryViewFactory->create($entry);
            $response->addEntry($entryView);
        }
    }
}

You might ask, why do we convert the entry Entity to a View?

The reason is that the Entity should not go outside the UseCases layer. We can only find an Entity with the help of the repository, so we modify/copy it if necessary and then put it back into the repository (when modified).

When we begin to move the Entity into the outer layer, it is best to add some additional methods for communication purposes, but the Entity should only contain core business logic.

As we are not sure of how we want to format the entries, we can defer this step.

Another question might be “Why a factory?”

If we create a new instance inside the loop such as

$entryView = new EntryView($entry);
$response->addEntry($entryView);

we would violate the dependency inversion principle. If, later on, we require another view object in the same UseCase logic, we would have to change the code. With the factory, we have an easy way to implement different views, which might contain different formatting logic, while still using the same UseCase.

Implementing external dependencies

At this point, we already know the dependencies of the UseCase: $entryViewFactory and $entryRepository. We also know the methods of the dependencies. The EntryViewFactory has a create method which accepts the EntryEntity, and the EntryRepository has a findAll() method which returns an array of EntryEntities. Now we can create the interfaces with the methods and apply them to the UseCase.

The EntryRepository will looks like this:

<?php
namespace BlackScorp\GuestBook\Repository;

interface EntryRepository {
    public function findAllPaginated($offset,$limit);
}

And the UseCase like so

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Repository\EntryRepository;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;

class ViewEntriesUseCase
{
    /**
     * @var EntryRepository
     */
    private $entryRepository;
    /**
     * @var EntryViewFactory
     */
    private $entryViewFactory;

    /**
     * ViewEntriesUseCase constructor.
     * @param EntryRepository $entryRepository
     * @param EntryViewFactory $entryViewFactory
     */
    public function __construct(EntryRepository $entryRepository, EntryViewFactory $entryViewFactory)
    {
        $this->entryRepository = $entryRepository;
        $this->entryViewFactory = $entryViewFactory;
    }


    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response)
    {
        $entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
        if (!$entries) {
            return;
        }

        foreach ($entries as $entry) {
            $entryView = $this->entryViewFactory->create($entry);
            $response->addEntry($entryView);
        }
    }
}

As you can see, the tests still fail because of the missing dependency implementation. So here we just create some fake objects.

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $entryRepository = new FakeEntryRepository();
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
    public function testCanSeeEntries()
    {
		$entities = [];
        $entities[] = new EntryEntity();
        $entryRepository = new FakeEntryRepository($entities);
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        $this->assertNotEmpty($response->entries);
    }
}

Because we already created the interfaces for the repository and view factory, we can implement them in the fake classes, and also implement the request/response interfaces.

The repository now looks like this:

<?php
namespace BlackScorp\GuestBook\Fake\Repository;

use BlackScorp\GuestBook\Repository\EntryRepository;

class FakeEntryRepository implements EntryRepository
{
    private $entries = [];

    public function __construct(array $entries = [])
    {
        $this->entries = $entries;
    }

    public function findAllPaginated($offset, $limit)
    {
        return array_splice($this->entries, $offset, $limit);
    }
}

and the view factory like this:

<?php
namespace BlackScorp\GuestBook\Fake\ViewFactory;

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\View\FakeEntryView;
use BlackScorp\GuestBook\View\EntryView;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;

class FakeEntryViewFactory implements EntryViewFactory
{
    /**
     * @param EntryEntity $entity
     * @return EntryView
     */
    public function create(EntryEntity $entity)
    {

        $view = new FakeEntryView();
        $view->author = $entity->getAuthor();
        $view->text = $entity->getText();
        return $view;
    }
}

You may wonder, why don’t we just use mocking frameworks to create the dependencies? There are two reasons:

  1. Because it is easy to create an actual class with the editor. So there is no need to use them.
  2. When we start to create the implementation for the framework, we can use these fake classes in the DI Container and fiddle with the template without having to make a real implementation.

The tests now pass, and we can go to refactoring. There is in fact nothing that can be refactored in the UseCase class, only in the test class.

Refactoring the test

The execution is actually the same, we just have a different setup and assertion. We can thus extract the initialization of the fake classes and processing the UseCase to a private function processUseCase.

The test class should now look like this

<?php
require_once __DIR__ . '/../../vendor/autoload.php';

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;

class ListEntriesTest extends PHPUnit_Framework_TestCase
{

    public function testCanSeeEntries()
    {
 		$entries = [
            new EntryEntity('testAuthor','test text')
        ];
        $response = $this->processUseCase($entries);
        $this->assertNotEmpty($response->entries);
    }

    public function testEntriesNotExists()
    {
        $entities = [];
        $response = $this->processUseCase($entities);
        $this->assertEmpty($response->entries);
    }

    /**
     * @param $entities
     * @return FakeViewEntriesResponse
     */
    private function processUseCase($entities)
    {
        $entryRepository = new FakeEntryRepository($entities);
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        return $response;
    }
}

Independence

Now, for example, we can easily create new test cases with invalid entities, and we can move the repository and factory to the setup method and run the tests with real implementations.

As you can see, we can implement a ready to use UseCase into the DI Container and use it inside any framework. The logic is framework agnostic.

We could create another repository implementation which speaks to an external API, for example, and pass it to the UseCase. The logic is database agnostic.

We could create CLI request/response objects and pass them to the same UseCase which is used inside a controller, so the logic is independent of the platform.

We could even execute different UseCases in a row where every UseCase might modify the actual response object.

class MainController extends BaseController
{
    public function indexAction(Request $httpRequest)
    {
        $indexActionRequest = new IndexActionRequest($httpRequest);
        $indexActionResponse = new IndexActionResponse();
        $this->getContainer('ViewNavigation')->process($indexActionRequest, $indexActionResponse);
        $this->getContainer('ViewNewsEntries')->process($indexActionRequest, $indexActionResponse);
        $this->getContainer('ViewUserAvatar')->process($indexActionRequest, $indexActionResponse);
        $this->render($indexActionResponse);

    }
}

Pagination

Now we want to add pagination. The test may look like this:

   public function testCanSeeFiveEntries(){
        $entities = [];
        for($i = 0;$i<10;$i++){
             $entities[] = new EntryEntity('Author '.$i,'Text '.$i);
        }

        $response = $this->processUseCase($entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
    }

This test will fail, so we have to modify the process method of the UseCase and also rename the method findAll to findAllPaginated.

 public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
        $entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
//....
    }

Now we apply the new parameters to the interface and the fake repository and add the new methods to the request interface.

The repository’s findAllPaginated method changes a little:

    public function findAllPaginated($offset, $limit)
    {
        return array_splice($this->entries, $offset, $limit);
    }

and we have to move the request object in the tests; also, the limit parameter will be required in the constructor of our request object. This way, we will force the setup to create the limit with a new instance.

  public function testCanSeeFiveEntries(){
        $entities = [];
        for($i = 0;$i<10;$i++){
            $entities[] = new EntryEntity();
        }
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
    }

The test passes, but we have to test if we can see the next five entries. Therefore, we have to extend the request object with a setPage method.

<?php
namespace BlackScorp\GuestBook\Fake\Request;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
class FakeViewEntriesRequest implements ViewEntriesRequest{
    private $offset = 0;
    private $limit = 0;

    /**
     * FakeViewEntriesRequest constructor.
     * @param int $limit
     */
    public function __construct($limit)
    {
        $this->limit = $limit;
    }

    public function setPage($page = 1){
        $this->offset = ($page-1) * $this->limit;
    }
    public function getOffset()
    {
        return $this->offset;
    }

    public function getLimit()
    {
        return $this->limit;
    }

}

With this method, we can test if we can see the next five entries.

   public function testCanSeeFiveEntriesOnSecondPage(){
        $entities = [];
        $expectedEntries = [];
        $entryViewFactory = new FakeEntryViewFactory();
        for($i = 0;$i<10;$i++){
            $entryEntity = new EntryEntity();
            if($i >= 5){
                $expectedEntries[]=$entryViewFactory->create($entryEntity);
            }
            $entities[] =$entryEntity;
        }
        $request = new FakeViewEntriesRequest(5);
        $request->setPage(2);
        $response = $this->processUseCase($request,$entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
        $this->assertEquals($expectedEntries,$response->entries);
    }

Now the tests pass and we can refactor. We move the FakeEntryViewFactory to the setup method and we are done with this feature. The final test class looks like this:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;

class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $entries = [];
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entries);
        $this->assertEmpty($response->entries);
    }

    public function testCanSeeEntries()
    {
        $entries = [
            new EntryEntity('testAuthor', 'test text')
        ];
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entries);
        $this->assertNotEmpty($response->entries);
    }

    public function testCanSeeFiveEntries()
    {
        $entities = [];
        for ($i = 0; $i < 10; $i++) {
            $entities[] = new EntryEntity('Author ' . $i, 'Text ' . $i);
        }
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5, count($response->entries));
    }

    public function testCanSeeFiveEntriesOnSecondPage()
    {
        $entities = [];
        $expectedEntries = [];
        $entryViewFactory = new FakeEntryViewFactory();
        for ($i = 0; $i < 10; $i++) {
            $entryEntity = new EntryEntity('Author ' . $i, 'Text ' . $i);
            if ($i >= 5) {
                $expectedEntries[] = $entryViewFactory->create($entryEntity);
            }
            $entities[] = $entryEntity;
        }
        $request = new FakeViewEntriesRequest(5);
        $request->setPage(2);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5, count($response->entries));
        $this->assertEquals($expectedEntries, $response->entries);
    }

    /**
     * @param $request
     * @param $entries
     * @return FakeViewEntriesResponse
     */
    private function processUseCase($request, $entries)
    {
        $repository = new FakeEntryRepository($entries);
        $factory = new FakeEntryViewFactory();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($repository, $factory);
        $useCase->process($request, $response);
        return $response;
    }
}

Conclusion

In this tutorial, we have seen how the test leads us to the UseCase, which leads us to interfaces which, in turn, lead us to fake implementations.

Again, the source code for this article can be found on Github – check the tags for all the various source code stages of this post.

This tutorial demonstrates that it is not that difficult to apply Test Driven Development and Clean Code Architecture to any newly created project. The great advantage is that while the logic is completely independent, we can still use third-party libraries.

Questions? Comments? Please leave them in the comment section right below the like button!

Frequently Asked Questions (FAQs) about Clean Code Architecture and Test-Driven Development in PHP

What is the main difference between Clean Code Architecture and traditional PHP coding?

Clean Code Architecture, also known as Clean Architecture, is a software design philosophy that prioritizes the separation of concerns, making the system easy to understand, develop, test, and maintain. Traditional PHP coding, on the other hand, often involves mixing business logic with presentation and data access code, which can lead to a system that is difficult to understand and maintain. Clean Architecture separates the software into layers, with each layer having a specific role and responsibility. This separation makes the system more flexible, easier to test, and less dependent on external agencies such as databases or frameworks.

How does Test-Driven Development (TDD) fit into Clean Code Architecture?

Test-Driven Development (TDD) is a software development approach where tests are written before the actual code. In the context of Clean Code Architecture, TDD plays a crucial role in ensuring that each component of the system behaves as expected. By writing tests first, developers are forced to think about the design of their code and how it will be used, which often leads to better-designed, more maintainable code. Furthermore, having a comprehensive suite of tests makes it easier to refactor the code and add new features, as developers can be confident that they haven’t broken existing functionality.

Can Clean Code Architecture be used with any PHP framework?

Yes, Clean Code Architecture can be used with any PHP framework. The key is to ensure that the framework is used as a tool, not as the foundation of the application. The business logic should be framework-agnostic and reside in the inner layers of the architecture, while the framework should be used primarily for handling HTTP requests and responses, routing, and interacting with the database.

How does Clean Code Architecture improve the maintainability of a PHP application?

Clean Code Architecture improves the maintainability of a PHP application by separating concerns into different layers. This separation makes it easier to understand and modify each part of the system without affecting others. For example, changes to the database schema should not require changes to the business logic or the user interface. Furthermore, by adhering to the Dependency Rule, which states that dependencies should point inwards, the architecture ensures that high-level modules are not dependent on low-level details, making the system more robust and less prone to bugs.

Is it more time-consuming to implement Clean Code Architecture compared to traditional PHP coding?

Initially, it might take more time to implement Clean Code Architecture because it requires careful planning and design. However, in the long run, it can save time and effort. The clear separation of concerns makes the code easier to understand, test, and debug. It also makes it easier to add new features or make changes to existing ones, as the impact of changes is localized to specific layers or components. Therefore, while the upfront investment might be higher, the long-term benefits in terms of maintainability and flexibility can more than make up for it.

How does Clean Code Architecture handle external dependencies like databases or third-party services?

In Clean Code Architecture, external dependencies like databases or third-party services are treated as details that should not influence the business logic. These dependencies are encapsulated in the outer layers of the architecture, and they communicate with the inner layers through interfaces defined by the inner layers. This approach, known as Dependency Inversion, ensures that the business logic is not tied to specific technologies or services, making it easier to swap out these dependencies if needed.

What are the main layers in Clean Code Architecture?

Clean Code Architecture typically consists of four main layers: Entities, Use Cases, Interface Adapters, and Frameworks and Drivers. Entities encapsulate the business rules at the highest level of abstraction. Use Cases define the specific business operations that can be performed. Interface Adapters convert data between the format most convenient for Use Cases and Entities and the format needed by the external world (like the web or a database). Frameworks and Drivers are the outermost layer and handle communication with the external world.

How does Clean Code Architecture support scalability?

Clean Code Architecture supports scalability by promoting a modular design where each component is independent and can be modified or scaled without affecting others. This modularity makes it easier to scale the system horizontally by adding more instances of specific components as needed. Furthermore, because the business logic is decoupled from external dependencies, it’s easier to make changes to the system to support increased load, such as switching to a more scalable database or using a more performant third-party service.

Can I migrate an existing PHP application to Clean Code Architecture?

Yes, it’s possible to migrate an existing PHP application to Clean Code Architecture. The process typically involves refactoring the existing codebase to separate concerns into different layers, starting with the most critical or complex parts of the system. It’s important to note that this process can be time-consuming and risky, especially for large codebases, so it should be done incrementally and with thorough testing at each step.

What are some common challenges when implementing Clean Code Architecture in PHP?

Some common challenges when implementing Clean Code Architecture in PHP include understanding and applying the principles of the architecture, especially for developers used to traditional PHP coding. It can also be challenging to design the system in a way that truly separates concerns and adheres to the Dependency Rule. Furthermore, because PHP is a dynamically typed language, it can be more difficult to enforce the boundaries between layers compared to statically typed languages. However, these challenges can be overcome with careful design, thorough testing, and the use of tools like static analyzers.

Vitalij MikVitalij Mik
View Author

German PHP developer since 2011

BrunoSclean codeOOPHPPHPtddTestingunit testunit testingUnit Tests
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week