Hexagonal Architechure (Ports and Adapters) with PHP
Since I started my studies about software architecture, I have seen a lot of content about Clean Architecture, Microservices Architecture and Domain Driven Design. Of course, this content is very important, but these architectures could be overengineering for small projects. So, the question is: Do I not need to think about architecture for simple projects? In my opinion, the answer is: You should always think about architecture because your application may grow, and likely will grow, requiring maintenance.
That’s where the Hexagonal Architecture comes into play, defining patterns for your application and also serving as an entry point for you to implement more robust designs in your application in the future.
The Hexagonal Architecture, described by Alistair Cockburn, can be a good solution for these cases. Cockburn describes an architecture that “allows an application to equally be driven by users, programs, automated tests, or batch scripts, and to be developed and tested in isolation from its eventual runtime devices and databases.”
Why I believe that Hexagonal Architecture can be a good entry point for those seeking knowledge about software architecture? Cockburn defines “just” how the boundaries of your application with its drivers and resources should be, but does not provide a discipline on how you should organize the internal part of the application. With this, you can have an interesting separation within an application that is starting, and thus, evolve the application design over time.
The name “Hexagonal Architecture” can be a bit misleading, as it’s not directly related to the hexagon itself. Cockburn’s choice of this name was merely illustrative, representing the idea of “Ports and Adapters,” which divides the application between its drivers (such as user interfaces, tests, and APIs) and its resources (such as databases and SMTP servers). This approach facilitates the understanding and implementation of the architecture, making it an attractive choice for those looking to start exploring software design concepts.
Before we dive into the practical part, it’s definitely worth checking out Cockburn’s content to gain a more comprehensive understanding of the architecture. This will help to solidify your understanding of the principles and concepts behind Hexagonal Architecture, better preparing you to apply this knowledge in practice.
Now, let’s dive into the practical part of a project.
The application we’re going to use as an example here will be a kind of list of movies the user has watched, along with a rating they can assign to the movie. The user’s movie catalog will consist of the name, the rating assigned, and the IMDb, which will be dynamically fetched.
Specific details of the business rule can be ignored. The idea here is to illustrate how the application will communicate with its drivers and resources.
<?php
declare(strict_types=1);
namespace HexagonalArch;
use HexagonalArch\Repository\WatchedMovie;
use HexagonalArch\Repository\WatchedMoviesRepository;
class WatchedMoviesCatalog
{
public function __construct(
private readonly WatchedMoviesRepository $repository,
private readonly IMDBGateway $IMDBGateway
){}
public function execute(Filters $filters): WatchedMoviesCatalogOutputCollection
{
$watchedMovies = $this->repository->getWatchedMovies($filters);
$output = new WatchedMoviesCatalogOutputCollection();
/* @var WatchedMovie $watchedMovie */
foreach ($watchedMovies as $watchedMovie) {
$imdb = $this->IMDBGateway->getImdb($watchedMovie->imdbId);
$output->add(new WatchedMoviesCatalogOutput($watchedMovie->name, $watchedMovie->watchedAt, $imdb));
}
return $output;
}
}
The WatchedMoviesCatalog class is responsible for returning the catalog of movies watched by the user. Note that in the constructor, we have dependency inversion, which gives our application complete control over the resources we are using.
This dependency inversion allows us to use Test Patterns such as Stubs, Mocks, Spies, and Fakes in our tests. Below is a test case for the WatchedMoviesCatalog
public function test(): void
{
$repositoryStub = $this->createMock(WatchedMoviesRepository::class);
$watchedMoviesCollection = new WatchedMoviesCollection();
$watchedMoviesCollection->add(new WatchedMovie('Fast and Furious', '2024-02-1', 'tt123456'));
$repositoryStub->expects($this->once())
->method('getWatchedMovies')
->willReturn($watchedMoviesCollection);
$imdbGatewayStub = $this->createMock(IMDbGateway::class);
$imdbGatewayStub->expects($this->once())
->method('getImdb')
->willReturn(7.4);
$watchedMoviesCatalog = new WatchedMoviesCatalog($repositoryStub, $imdbGatewayStub);
$catalog = $watchedMoviesCatalog->execute(new Filters());
/** @var WatchedMoviesCatalogOutput $firstMovie */
$firstMovie = $catalog->getIterator()[0];
$this->assertEquals('Fast and Furious', $firstMovie->movieName);
$this->assertEquals('2024-02-1', $firstMovie->watchedAt);
$this->assertEquals(7.4, $firstMovie->imdbRate);
}
Imagine a scenario where our dependency control flow is not inverted. Perhaps it would be impossible to test the result of IMDB, as this can be a variable value. Considering this, controlling our resources allows us to have more robust, independent, and repeatable tests.
In the example above, we have a driver (test case) consuming our application. Other drivers, such as APIs, messaging systems, or even CLI, could also consume our application.
Some other less important information:
As this is an example application, I did not use any dependency injection service to control our dependency inversion.
We could easily add an HTTP layer using a microframework like SLIM.
To have more control over the returns of application methods that returned arrays, I used the concept of First Class Collections.
You can find the application code on my GitHub: https://github.com/leosteil/hexagonal-arch-php. This project will be continuously improved, with the addition of new business rules, the incorporation of a framework for API creation, among other enhancements.