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.
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:
Be the first one to write a response :(
{{ reply.member.name }} - {{ reply.created_at_human_readable }}