Appendix: Hexagonal Architecture with PHP
The following article was posted in php|architect magazine in June 2014 by Carlos Buenosvinos.
Introduction
With the rise of Domain-Driven Design (DDD), architectures promoting domain centric designs are becoming more popular. This is the case with Hexagonal Architecture, also known as Ports and Adapters, that seems to have being rediscovered just now by PHP developers. Invented in 2005 by Alistair Cockburn, one of the Agile Manifesto authors, the Hexagonal Architecture allows an application to be equally driven by users, programs, automated tests or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases. This results into agnostic infrastructure web applications that are easier to test, write and maintain. Let’s see how to apply it using real PHP examples.
Your company is building a brainstorming system called Idy. Users add and rate ideas so the most interesting ones can be implemented in a company. It is Monday morning, another sprint is starting and you are reviewing some user stories with your team and your Product Owner. “As a not logged in user, I want to rate an idea and the author should be notified by email”, that’s a really important one, isn’t it?
First Approach
As a good developer, you decide to divide and conquer the user story, so you’ll start with the first part, “I want to rate an idea”. After that, you will face “the author should be notified by email”. That sounds like a plan.
In terms of business rules, rating an idea is as easy as finding the idea by its identifier in the ideas repository, where all the ideas live, add the rating, recalculate the average and save the idea back. If the idea does not exist or the repository is not available we should throw an exception so we can show an error message, redirect the user or do whatever the business asks us for.
In order to execute this UseCase, we just need the idea identifier and the rating from the user. Two integers that would come from the user request.
Your company web application is dealing with a Zend Framework 1 legacy application. As most of companies, probably some parts of your app may be newer, more SOLID, and others may just be a big ball of mud. However, you know that it does not matter at all which framework you are using, it is all about writing clean code that makes maintenance a low cost task for your company.
You’re trying to apply some Agile principles you remember from your last conference, how it was, yeah, I remember “make it work, make it right, make it fast”. After some time working you get something like Listing 1.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 // Getting parameters from the request
6 $ideaId = $this->request->getParam('id');
7 $rating = $this->request->getParam('rating');
8
9 // Building database connection
10 $db = new Zend_Db_Adapter_Pdo_Mysql([
11 'host' => 'localhost',
12 'username' => 'idy',
13 'password' => '',
14 'dbname' => 'idy'
15 ]);
16
17 // Finding the idea in the database
18 $sql = 'SELECT * FROM ideas WHERE idea_id = ?';
19 $row = $db->fetchRow($sql, $ideaId);
20 if (!$row) {
21 throw new Exception('Idea does not exist');
22 }
23
24 // Building the idea from the database
25 $idea = new Idea();
26 $idea->setId($row['id']);
27 $idea->setTitle($row['title']);
28 $idea->setDescription($row['description']);
29 $idea->setRating($row['rating']);
30 $idea->setVotes($row['votes']);
31 $idea->setAuthor($row['email']);
32
33 // Add user rating
34 $idea->addRating($rating);
35
36 // Update the idea and save it to the database
37 $data = [
38 'votes' => $idea->getVotes(),
39 'rating' => $idea->getRating()
40 ];
41 $where['idea_id = ?'] = $ideaId;
42 $db->update('ideas', $data, $where);
43
44 // Redirect to view idea page
45 $this->redirect('/idea/'.$ideaId);
46 }
47 }
I know what readers are thinking: “Who is going to access data directly from the controller? This is a 90’s example!”, ok, ok, you’re right. If you are already using a framework, it is likely that you are also using an ORM. Maybe done by yourself or any of the existing ones such as Doctrine, Eloquent, ZendDB, etc. If this is the case, you are one step further from those who have some Database connection object but don’t count your chickens before they’re hatched.
For newbies, Listing 1 code just works. However, if you take a closer look at the Controller, you’ll see more than business rules, you’ll also see how your web framework routes a request into your business rules, references to the database or how to connect to it. So close, you see references to your infrastructure.
Infrastructure is the detail that makes your business rules work. Obviously, we need some way to get to them (API, web, console apps, etc.) and effectively we need some physical place to store our ideas (memory, database, NoSQL, etc.). However, we should be able to exchange any of these pieces with another that behaves in the same way but with different implementations. What about starting with the Database access?
All those Zend_DB_Adapter connections (or straight MySQL commands if that’s your case) are asking to be promoted to some sort of object that encapsulates fetching and persisting Idea objects. They are begging for being a Repository.
Repositories and the Persistence Edge
Whether there is a change in the business rules or in the infrastructure, we must edit the same piece of code. Believe me, in CS, you don’t want many people touching the same piece of code for different reasons. Try to make your functions do one and just one thing so it is less probable having people messing around with the same piece of code. You can learn more about this by having a look at the Single Responsibility Principle (SRP). For more information about this principle: http://www.objectmentor.com/resources/articles/srp.pdf
Listing 1 is clearly this case. If we want to move to Redis or add the author notification feature, you’ll have to update the rateAction method. Chances to affect aspects of the rateAction not related with the one updating are high. Listing 1 code is fragile. If it is common in your team to hear “If it works, don’t touch it”, SRP is missing.
So, we must decouple our code and encapsulate the responsibility for dealing with fetching and persisting ideas into another object. The best way, as explained before, is using a Repository. Challenged accepted! Let’s see the results in Listing 2.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $ideaRepository = new IdeaRepository();
9 $idea = $ideaRepository->find($ideaId);
10 if (!$idea) {
11 throw new Exception('Idea does not exist');
12 }
13
14 $idea->addRating($rating);
15 $ideaRepository->update($idea);
16
17 $this->redirect('/idea/'.$ideaId);
18 }
19 }
20
21 class IdeaRepository
22 {
23 private $client;
24
25 public function __construct()
26 {
27 $this->client = new Zend_Db_Adapter_Pdo_Mysql([
28 'host' => 'localhost',
29 'username' => 'idy',
30 'password' => '',
31 'dbname' => 'idy'
32 ]);
33 }
34
35 public function find($id)
36 {
37 $sql = 'SELECT * FROM ideas WHERE idea_id = ?';
38 $row = $this->client->fetchRow($sql, $id);
39 if (!$row) {
40 return null;
41 }
42
43 $idea = new Idea();
44 $idea->setId($row['id']);
45 $idea->setTitle($row['title']);
46 $idea->setDescription($row['description']);
47 $idea->setRating($row['rating']);
48 $idea->setVotes($row['votes']);
49 $idea->setAuthor($row['email']);
50
51 return $idea;
52 }
53
54 public function update(Idea $idea)
55 {
56 $data = [
57 'title' => $idea->getTitle(),
58 'description' => $idea->getDescription(),
59 'rating' => $idea->getRating(),
60 'votes' => $idea->getVotes(),
61 'email' => $idea->getAuthor(),
62 ];
63
64 $where = ['idea_id = ?' => $idea->getId()];
65 $this->client->update('ideas', $data, $where);
66 }
67 }
The result is nicer. The rateAction of the IdeaController is more understandable. When read, it talks about business rules. IdeaRepository is a business concept. When talking with business guys, they understand what an IdeaRepository is: A place where I put Ideas and get them.
A Repository “mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.” as found in Martin Fowler’s pattern catalog.
If you are already using an ORM such as Doctrine, your current repositories extend from an EntityRepository. If you need to get one of those repositories, you ask Doctrine EntityManager to do the job. The resulting code would be almost the same, with an extra access to the EntityManager in the controller action to get the IdeaRepository.
At this point, we can see in the landscape one of the edges of our hexagon, the persistence edge. However, this side is not well drawn, there is still some relationship between what an IdeaRepository is and how it is implemented.
In order to make an effective separation between our application boundary and the infrastructure boundary we need an additional step. We need to explicitly decouple behavior from implementation using some sort of interface.
Decoupling Business and Persistence
Have you ever experienced the situation when you start talking to your Product Owner, Business Analyst or Project Manager about your issues with the Database? Can you remember their faces when explaining how to persist and fetch an object? They had no idea what you were talking about.
The truth is that they don’t care, but that’s ok. If you decide to store the ideas in a MySQL server, Redis or SQLite it is your problem, not theirs. Remember, from a business standpoint, your infrastructure is a detail. Business rules are not going to change whether you use Symfony or Zend Framework, MySQL or PostgreSQL, REST or SOAP, etc.
That’s why it is important to decouple our IdeaRepository from its implementation. The easiest way is to use a proper interface. How can we achieve that? Let’s take a look at Listing 3.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $ideaRepository = new MySQLIdeaRepository();
9 $idea = $ideaRepository->find($ideaId);
10 if (!$idea) {
11 throw new Exception('Idea does not exist');
12 }
13
14 $idea->addRating($rating);
15 $ideaRepository->update($idea);
16
17 $this->redirect('/idea/'.$ideaId);
18 }
19 }
20
21 interface IdeaRepository
22 {
23 /**
24 * @param int $id
25 * @return null|Idea
26 */
27 public function find($id);
28
29 /**
30 * @param Idea $idea
31 */
32 public function update(Idea $idea);
33 }
34
35 class MySQLIdeaRepository implements IdeaRepository
36 {
37 // ...
38 }
Easy, isn’t it? We have extracted the IdeaRepository behavior into an interface, renamed the IdeaRepository into MySQLIdeaRepository and updated the rateAction to use our MySQLIdeaRepository. But what’s the benefit?
We can now exchange the repository used in the controller with any implementing the same interface. So, let’s try a different implementation.
Migrating our Persistence to Redis
During the sprint and after talking to some mates, you realize that using a NoSQL strategy could improve the performance of your feature. Redis is one of your best friends. Go for it and show me your Listing 4.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $ideaRepository = new RedisIdeaRepository();
9 $idea = $ideaRepository->find($ideaId);
10 if (!$idea) {
11 throw new Exception('Idea does not exist');
12 }
13
14 $idea->addRating($rating);
15 $ideaRepository->update($idea);
16
17 $this->redirect('/idea/'.$ideaId);
18 }
19 }
20
21 interface IdeaRepository
22 {
23 // ...
24 }
25
26 class RedisIdeaRepository implements IdeaRepository
27 {
28 private $client;
29
30 public function __construct()
31 {
32 $this->client = new \Predis\Client();
33 }
34
35 public function find($id)
36 {
37 $idea = $this->client->get($this->getKey($id));
38 if (!$idea) {
39 return null;
40 }
41
42 return unserialize($idea);
43 }
44
45 public function update(Idea $idea)
46 {
47 $this->client->set(
48 $this->getKey($idea->getId()),
49 serialize($idea)
50 );
51 }
52
53 private function getKey($id)
54 {
55 return 'idea:' . $id;
56 }
57 }
Easy again. You’ve created a RedisIdeaRepository that implements IdeaRepository interface and we have decided to use Predis as a connection manager. Code looks smaller, easier and faster. But what about the controller? It remains the same, we have just changed which repository to use, but it was just one line of code.
As an exercise for the reader, try to create the IdeaRepository for SQLite, a file or an in-memory implementation using arrays. Extra points if you think about how ORM Repositories fit with Domain Repositories and how ORM @annotations affect this architecture.
Decouple Business and Web Framework
We have already seen how easy it can be to changing from one persistence strategy to another. However, the persistence is not the only edge from our Hexagon. What about how the user interacts with the application?
Your CTO has set up in the roadmap that your team is moving to Symfony2, so when developing new features in you current ZF1 application, we would like to make the incoming migration easier. That’s tricky, show me your Listing 5.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $ideaRepository = new RedisIdeaRepository();
9 $useCase = new RateIdeaUseCase($ideaRepository);
10 $response = $useCase->execute($ideaId, $rating);
11
12 $this->redirect('/idea/'.$ideaId);
13 }
14 }
15
16 interface IdeaRepository
17 {
18 // ...
19 }
20
21 class RateIdeaUseCase
22 {
23 private $ideaRepository;
24
25 public function __construct(IdeaRepository $ideaRepository)
26 {
27 $this->ideaRepository = $ideaRepository;
28 }
29
30 public function execute($ideaId, $rating)
31 {
32 try {
33 $idea = $this->ideaRepository->find($ideaId);
34 } catch(Exception $e) {
35 throw new RepositoryNotAvailableException();
36 }
37
38 if (!$idea) {
39 throw new IdeaDoesNotExistException();
40 }
41
42 try {
43 $idea->addRating($rating);
44 $this->ideaRepository->update($idea);
45 } catch(Exception $e) {
46 throw new RepositoryNotAvailableException();
47 }
48
49 return $idea;
50 }
51 }
Let’s review the changes. Our controller is not having any business rules at all. We have pushed all the logic inside a new object called RateIdeaUseCase that encapsulates it. This object is also known as Controller, Interactor or Application Service.
The magic is done by the execute method. All the dependencies such as the RedisIdeaRepository are passed as an argument to the constructor. All the references to an IdeaRepository inside our UseCase are pointing to the interface instead of any concrete implementation.
That’s really cool. If you take a look inside RateIdeaUseCase, there is nothing talking about MySQL or Zend Framework. No references, no instances, no annotations, nothing. It is like your infrastructure does not mind. It just talks about business logic.
Additionally, we have also tuned the Exceptions we throw. Business processes also have exceptions. NotAvailableRepository and IdeaDoesNotExist are two of them. Based on the one being thrown we can react in different ways in the framework boundary.
Sometimes, the number of parameters that a UseCase receives can be too many. In order to organize them, it is quite common to build a UseCase request using a Data Transfer Object (DTO) to pass them together. Let’s see how you could solve this in Listing 6.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $ideaRepository = new RedisIdeaRepository();
9 $useCase = new RateIdeaUseCase($ideaRepository);
10 $response = $useCase->execute(
11 new RateIdeaRequest($ideaId, $rating)
12 );
13
14 $this->redirect('/idea/'.$response->idea->getId());
15 }
16 }
17
18 class RateIdeaRequest
19 {
20 public $ideaId;
21 public $rating;
22
23 public function __construct($ideaId, $rating)
24 {
25 $this->ideaId = $ideaId;
26 $this->rating = $rating;
27 }
28 }
29
30 class RateIdeaResponse
31 {
32 public $idea;
33
34 public function __construct(Idea $idea)
35 {
36 $this->idea = $idea;
37 }
38 }
39
40 class RateIdeaUseCase
41 {
42 // ...
43
44 public function execute($request)
45 {
46 $ideaId = $request->ideaId;
47 $rating = $request->rating;
48
49 // ...
50
51 return new RateIdeaResponse($idea);
52 }
53 }
The main changes here are introducing two new objects, a Request and a Response. They are not mandatory, maybe a UseCase has no request or response. Another important detail is how you build this request. In this case, we are building it getting the parameters from ZF request object.
Ok, but wait, what’s the real benefit? it is easier to change from one framework to other, or execute our UseCase from another delivery mechanism. Let’s see this point.
Rating an idea using the API
During the day, your Product Owner comes to you and says: “by the way, a user should be able to rate an idea using our mobile app. I think we will need to update the API, could you do it for this sprint?”. Here’s the PO again. “No problem!”. Business is impressed with your commitment.
As Robert C. Martin says: “The Web is a delivery mechanism […] Your system architecture should be as ignorant as possible about how it is to be delivered. You should be able to deliver it as a console app, a web app, or even a web service app, without undue complication or any change to the fundamental architecture”.
Your current API is built using Silex, the PHP micro-framework based on the Symfony2 Components. Let’s go for it in Listing 7.
1 require_once __DIR__.'/../vendor/autoload.php';
2
3 $app = new \Silex\Application();
4
5 // ... more routes
6
7 $app->get(
8 '/api/rate/idea/{ideaId}/rating/{rating}',
9 function ($ideaId, $rating) use ($app) {
10 $ideaRepository = new RedisIdeaRepository();
11 $useCase = new RateIdeaUseCase($ideaRepository);
12 $response = $useCase->execute(
13 new RateIdeaRequest($ideaId, $rating)
14 );
15
16 return $app->json($response->idea);
17 }
18 );
19
20 $app->run();
Is there anything familiar to you? Can you identify some code that you have seen before? I’ll give you a clue.
1 $ideaRepository = new RedisIdeaRepository();
2 $useCase = new RateIdeaUseCase($ideaRepository);
3 $response = $useCase->execute(
4 new RateIdeaRequest($ideaId, $rating)
5 );
“Man! I remember those 3 lines of code. They look exactly the same as the web application”. That’s right, because the UseCase encapsulates the business rules you need to prepare the request, get the response and act accordingly.
We are providing our users with another way for rating an idea; another delivery mechanism.
The main difference is where we created the RateIdeaRequest from. In the first example, it was from a ZF request and now it is from a Silex request using the parameters matched in the route.
Console app rating
Sometimes, a UseCase is going to be executed from a Cron job or the command line. As examples, batch processing or some testing command lines to accelerate the development.
While testing this feature using the web or the API, you realize that it would be nice to have a command line to do it, so you don’t have to go through the browser.
If you are using shell scripts files, I suggest you to check the Symfony Console component. What would the code look like?
1 namespace Idy\Console\Command;
2
3 use Symfony\Component\Console\Command\Command;
4 use Symfony\Component\Console\Input\InputArgument;
5 use Symfony\Component\Console\Input\InputInterface;
6 use Symfony\Component\Console\Output\OutputInterface;
7
8 class VoteIdeaCommand extends Command
9 {
10 protected function configure()
11 {
12 $this
13 ->setName('idea:rate')
14 ->setDescription('Rate an idea')
15 ->addArgument('id', InputArgument::REQUIRED)
16 ->addArgument('rating', InputArgument::REQUIRED)
17 ;
18 }
19
20 protected function execute(
21 InputInterface $input,
22 OutputInterface $output
23 ) {
24 $ideaId = $input->getArgument('id');
25 $rating = $input->getArgument('rating');
26
27 $ideaRepository = new RedisIdeaRepository();
28 $useCase = new RateIdeaUseCase($ideaRepository);
29 $response = $useCase->execute(
30 new RateIdeaRequest($ideaId, $rating)
31 );
32
33 $output->writeln('Done!');
34 }
35 }
Again those 3 lines of code. As before, the UseCase and its business logic remain untouched, we are just providing a new delivery mechanism. Congratulations, you’ve discovered the user side hexagon edge.
There is still a lot to do. As you may have heard, a real craftsman does TDD. We have already started our story so we must be ok with just testing after.
Testing Rating an Idea UseCase
Michael Feathers introduced a definition of legacy code as code without tests. You don’t want your code to be legacy just born, do you?
In order to unit test this UseCase object, you decide to start with the easiest part, what happens if the repository is not available? How can we generate such behavior? Do we stop our Redis server while running the unit tests? No. We need to have an object that has such behavior. Let’s use a mock object in Listing 9.
1 class RateIdeaUseCaseTest extends \PHPUnit_Framework_TestCase
2 {
3 /**
4 * @test
5 */
6 public function whenRepositoryNotAvailableAnExceptionShouldBeThrown()
7 {
8 $this->setExpectedException('NotAvailableRepositoryException');
9 $ideaRepository = new NotAvailableRepository();
10 $useCase = new RateIdeaUseCase($ideaRepository);
11 $useCase->execute(
12 new RateIdeaRequest(1, 5)
13 );
14 }
15 }
16
17 class NotAvailableRepository implements IdeaRepository
18 {
19 public function find($id)
20 {
21 throw new NotAvailableException();
22 }
23
24 public function update(Idea $idea)
25 {
26 throw new NotAvailableException();
27 }
28 }
Nice. NotAvailableRepository has the behavior that we need and we can use it with RateIdeaUseCase because it implements IdeaRepository interface.
Next case to test is what happens if the idea is not in the repository. Listing 10 shows the code.
1 class RateIdeaUseCaseTest extends \PHPUnit_Framework_TestCase
2 {
3 // ...
4
5 /**
6 * @test
7 */
8 public function whenIdeaDoesNotExistAnExceptionShouldBeThrown()
9 {
10 $this->setExpectedException('IdeaDoesNotExistException');
11 $ideaRepository = new EmptyIdeaRepository();
12 $useCase = new RateIdeaUseCase($ideaRepository);
13 $useCase->execute(
14 new RateIdeaRequest(1, 5)
15 );
16 }
17 }
18
19 class EmptyIdeaRepository implements IdeaRepository
20 {
21 public function find($id)
22 {
23 return null;
24 }
25
26 public function update(Idea $idea)
27 {
28
29 }
30 }
Here, we use the same strategy but with an EmptyIdeaRepository. It also implements the same interface but the implementation always returns null regardless which identifier the find method receives.
Why are we testing these cases?, remember Kent Beck’s words: “Test everything that could possibly break”.
Let’s carry on with the rest of the feature. We need to check a special case that is related with having a read available repository where we cannot write to. Solution can be found in Listing 11.
1 class RateIdeaUseCaseTest extends \PHPUnit_Framework_TestCase
2 {
3 // ...
4
5 /**
6 * @test
7 */
8 public function whenUpdatingInReadOnlyAnIdeaAnExceptionShouldBeThrown()
9 {
10 $this->setExpectedException('NotAvailableRepositoryException');
11 $ideaRepository = new WriteNotAvailableRepository();
12 $useCase = new RateIdeaUseCase($ideaRepository);
13 $response = $useCase->execute(
14 new RateIdeaRequest(1, 5)
15 );
16 }
17 }
18
19 class WriteNotAvailableRepository implements IdeaRepository
20 {
21 public function find($id)
22 {
23 $idea = new Idea();
24 $idea->setId(1);
25 $idea->setTitle('Subscribe to php[architect]');
26 $idea->setDescription('Just buy it!');
27 $idea->setRating(5);
28 $idea->setVotes(10);
29 $idea->setAuthor('hi@carlos.io');
30
31 return $idea;
32 }
33
34 public function update(Idea $idea)
35 {
36 throw new NotAvailableException();
37 }
38 }
Ok, now the key part of the feature is still remaining. We have different ways of testing this, we can write our own mock or use a mocking framework such as Mockery or Prophecy. Let’s choose the first one. Another interesting exercise would be to write this example and the previous ones using one of these frameworks.
1 class RateIdeaUseCaseTest extends \PHPUnit_Framework_TestCase
2 {
3 // ...
4
5 /**
6 * @test
7 */
8 public function whenRatingAnIdeaNewRatingShouldBeAddedAndIdeaUpdated()
9 {
10 $ideaRepository = new OneIdeaRepository();
11 $useCase = new RateIdeaUseCase($ideaRepository);
12 $response = $useCase->execute(
13 new RateIdeaRequest(1, 5)
14 );
15
16 $this->assertSame(5, $response->idea->getRating());
17 $this->assertTrue($ideaRepository->updateCalled);
18 }
19 }
20
21 class OneIdeaRepository implements IdeaRepository
22 {
23 public $updateCalled = false;
24
25 public function find($id)
26 {
27 $idea = new Idea();
28 $idea->setId(1);
29 $idea->setTitle('Subscribe to php[architect]');
30 $idea->setDescription('Just buy it!');
31 $idea->setRating(5);
32 $idea->setVotes(10);
33 $idea->setAuthor('hi@carlos.io');
34
35 return $idea;
36 }
37
38 public function update(Idea $idea)
39 {
40 $this->updateCalled = true;
41 }
42 }
Bam! 100% Coverage for the UseCase. Maybe, next time we can do it using TDD so the test will come first. However, testing this feature was really easy because of the way decoupling is promoted in this architecture.
Maybe you are wondering about this:
1 $this->updateCalled = true;
We need a way to guarantee that the update method has been called during the UseCase execution. This does the trick. This test double object is called a spy, mocks cousin.
When to use mocks? As a general rule, use mocks when crossing boundaries. In this case, we need mocks because we are crossing from the domain to the persistence boundary.
What about testing the infrastructure?
Testing Infrastructure
If you want to achieve 100% coverage for your whole application you will also have to test your infrastructure. Before doing that, you need to know that those unit tests will be more coupled to your implementation than the business ones. That means that the probability to be broken with implementation details changes is higher. So it is a trade-off you will have to consider.
So, if you want to continue, we need to do some modifications. We need to decouple even more. Let’s see the code in Listing 13.
1 class IdeaController extends Zend_Controller_Action
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $useCase = new RateIdeaUseCase(
9 new RedisIdeaRepository(
10 new \Predis\Client()
11 )
12 );
13
14 $response = $useCase->execute(
15 new RateIdeaRequest($ideaId, $rating)
16 );
17
18 $this->redirect('/idea/'.$response->idea->getId());
19 }
20 }
21
22 class RedisIdeaRepository implements IdeaRepository
23 {
24 private $client;
25
26 public function __construct($client)
27 {
28 $this->client = $client;
29 }
30
31 // ...
32
33 public function find($id)
34 {
35 $idea = $this->client->get($this->getKey($id));
36 if (!$idea) {
37 return null;
38 }
39
40 return $idea;
41 }
42 }
If we want to 100% unit test RedisIdeaRepository we need to be able to pass the Predis\Client as a parameter to the repository without specifying TypeHinting so we can pass a mock to force the code flow necessary to cover all the cases.
This forces us to update the Controller to build the Redis connection, pass it to the repository and pass the result to the UseCase.
Now, it is all about creating mocks, test cases and having fun doing asserts.
Arggg, So Many Dependencies!
Is it normal that I have to create so many dependencies by hand? No. It is common to use a Dependency Injection component or a Service Container with such capabilities. Again, Symfony comes to the rescue, however, you can also check PHP-DI 4 http://php-di.org/.
Let’s see the resulting code in Listing 14 after applying Symfony Service Container component to our application.
1 class IdeaController extends ContainerAwareController
2 {
3 public function rateAction()
4 {
5 $ideaId = $this->request->getParam('id');
6 $rating = $this->request->getParam('rating');
7
8 $useCase = $this->get('rate_idea_use_case');
9 $response = $useCase->execute(
10 new RateIdeaRequest($ideaId, $rating)
11 );
12
13 $this->redirect('/idea/'.$response->idea->getId());
14 }
15 }
The controller has been modified to have access to the container, that’s why it is inheriting from a new base controller ContainerAwareController that has a get method to retrieve each of the services contained.
1 <?xml version="1.0" ?>
2 <container xmlns="http://symfony.com/schema/dic/services"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://symfony.com/schema/dic/services
5 http://symfony.com/schema/dic/services/services-1.0.xsd">
6 <services>
7 <service
8 id="rate_idea_use_case"
9 class="RateIdeaUseCase">
10 <argument type="service" id="idea_repository" />
11 </service>
12
13 <service
14 id="idea_repository"
15 class="RedisIdeaRepository">
16 <argument type="service">
17 <service class="Predis\Client" />
18 </argument>
19 </service>
20 </services>
21 </container>
In Listing 15, you can also find the XML file used to configure the Service Container. It is really easy to understand but if you need more information, take a look to the Symfony Service Container Component site in http://symfony.com/doc/current/book/service_container.html
Domain Services and Notification Hexagon Edge
Are we forgetting something? “the author should be notified by email”, yeah! That’s true. Let’s see in Listing 16 how we have updated the UseCase for doing the job.
1 class RateIdeaUseCase
2 {
3 private $ideaRepository;
4 private $authorNotifier;
5
6 public function __construct(
7 IdeaRepository $ideaRepository,
8 AuthorNotifier $authorNotifier
9 )
10 {
11 $this->ideaRepository = $ideaRepository;
12 $this->authorNotifier = $authorNotifier;
13 }
14
15 public function execute(RateIdeaRequest $request)
16 {
17 $ideaId = $request->ideaId;
18 $rating = $request->rating;
19
20 try {
21 $idea = $this->ideaRepository->find($ideaId);
22 } catch(Exception $e) {
23 throw new RepositoryNotAvailableException();
24 }
25
26 if (!$idea) {
27 throw new IdeaDoesNotExistException();
28 }
29
30 try {
31 $idea->addRating($rating);
32 $this->ideaRepository->update($idea);
33 } catch(Exception $e) {
34 throw new RepositoryNotAvailableException();
35 }
36
37 try {
38 $this->authorNotifier->notify(
39 $idea->getAuthor()
40 );
41 } catch(Exception $e) {
42 throw new NotificationNotSentException();
43 }
44
45 return $idea;
46 }
47 }
As you realize, we have added a new parameter for passing AuthorNotifier Service that will send the email to the author. This is the port in the “Ports and Adapters” naming. We have also updated the business rules in the execute method.
Repositories are not the only objects that may access your infrastructure and should be decoupled using interfaces or abstract classes. Domain Services can too. When there is a behavior not clearly owned by just one Entity in your domain, you should create a Domain Service. A typical pattern is to write an abstract Domain Service that has some concrete implementation and some other abstract methods that the adapter will implement.
As an exercise, define the implementation details for the AuthorNotifier abstract service. Options are SwiftMailer or just plain mail calls. It is up to you.
Let’s Recap
In order to have a clean architecture that helps you create easy to write and test applications, we can use Hexagonal Architecture. To achieve that, we encapsulate user story business rules inside a UseCase or Interactor object. We build the UseCase request from our framework request, instantiate the UseCase and all its dependencies and then execute it. We get the response and act accordingly based on it. If our framework has a Dependency Injection component you can use it to simplify the code.
The same UseCase objects can be used from different delivery mechanisms in order to allow users access the features from different clients (web, API, console, etc.)
For testing, play with mocks that behave like all the interfaces defined so special cases or error flows can also be covered. Enjoy the good job done.
Hexagonal Architecture
In almost all the blogs and books you will find drawings about concentric circles representing different areas of software. As Robert C. Martin explains in his “Clean Architecture” post, the outer circle is where your infrastructure resides. The inner circle is where your Entities live. The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.
Key Points
Use this approach if 100% unit test code coverage is important to your application. Also, if you want to be able to switch your storage strategy, web framework or any other type of third-party code. The architecture is especially useful for long-lasting applications that need to keep up with changing requirements.
What’s Next?
If you are interested in learning more about Hexagonal Architecture and other near concepts you should review the related URLs provided at the beginning of the article, take a look at CQRS and Event Sourcing. Also, don’t forget to subscribe to google groups and RSS about DDD such as http://dddinphp.org and follow on Twitter people like @VaughnVernon, and @ericevans0.