Applying Hexagonal Architecture to a Symfony Project
We look at how the hexagonal architecture promotes maintainable, clean, and testable code before showing how to use it in a PHP-based project.
Join the DZone community and get the full member experience.
Join For FreeHexagonal Architecture, also known as Architecture of Ports and Adapters, is one of the most used architectures nowadays. The aim of this article is to list down the main characteristics, applying hexagonal architecture to a Symfony project.
Applying Hexagonal Architecture: Main Principles
Hexagonal Architecture is a type of architecture that is based on the principle of Ports and Adapters. This architecture follows the premise of guaranteeing the total abstraction of the domain with respect to all the dependencies we may have (repositories, frameworks, third-party libraries, etc.).
The principle of Ports and Adapters, as its name suggests, is based on complete isolation of our domain from any external dependency and managing the interaction of these dependencies with our domain by using:
- Ports that allow us to standardize the functionalities/actions that we use from these dependencies and in what way or with what data structures they will do it.
- Adapters that will be responsible for adapting the dependencies to the data structures that we need within our domain.
In addition to these basic principles, hexagonal architecture follows the principles defined by Robert C. Martin The Clean Architecture to ensure a clean, scalable, and adaptable architecture:
- Independence of the framework: As we have defined previously, the hexagonal architecture guarantees total independence of any framework or external dependence.
- Testable: By having the business rules isolated in our domain, we can unitarily test all the requirements of our application without any external factor that could alter the functionality of the one under test.
- Independent of the UI: The user interface is a continuously changing ecosystem, which does not mean that the business rules do it equally. UI independence allows us to maintain a more stable domain, avoiding having to modify it to the business rules by completely external factors.
- Independent of the database: Our domain should not know the way we decided to structure or store our data in a repository. It doesn’t influence it, except coupling or if we decide to change our database at some point.
- Independent of any external agent: Our domain is a representation of our business rules, therefore it does not concern the knowledge of any external agency. Each external library will have its own adapter, which will be responsible for the interaction with the domain.
The Hexagonal Architecture is a so-called Clean Architecture, which are based on the following scheme:
Hexagonal Architecture Scheme. Taken from Novoda
We can see that the concepts explained above fit perfectly into this scheme. And, in addition to applying these principles, in our practical example, we will always try to apply the SOLID principles in the best possible way.
Practical Exercise: Applying the Hexagonal Architecture
We will perform a simple exercise that allows us to see the application of the concepts named above in a practical way. Let’s imagine that we have to develop an application for a warehouse in PHP 7 using Symfony 3. In this first article, we will develop the introduction of products in the system.
To do this we will develop a POST-type access point that we will define as “/ products”.
As we can see, the statement can already give us some clues as to the clearest external dependencies that we will need:
- Symfony, as an application framework.
- A database to store the introduced products: in this case, we will use Doctrine as an ORM to interact with MySQL.
In this exercise, we are going to apply an inside-out strategy, therefore, let’s start by defining our domain. We are going to implement a very basic Product entity with four fields: id, name, reference, and date of creation.
To ensure that the data that will pass through our domain is as expected, we have declared our entity with a private constructor and we can only instantiate our entity through the static function fromDto
which is waiting for a specific data structure based on a Data Transfer Object that we have defined as CreateProductRequestDto
, as we can see below:
class Product
{
private $id;
private $name;
private $reference;
private $createdAt;
private function __construct(
string $name,
string $reference
)
{
$this->name = $name;
$this->reference = $reference;
$this->createdAt = new \DateTime();
}
public static function fromDto(CreateProductRequestDto $createProductResponseDto): Product
{
return new Product(
$createProductResponseDto->name(),
$createProductResponseDto->reference()
);
}
public function toDto()
{
return new CreateProductResponseDto(
$this->id,
$this->name, $this->reference, $this->createdAt
);
}
}
As we can see, the parameters of our entity are private, and will only be accessible to the outside through the data structure defined as CreateProductResponseDto
, thus ensuring that we are only going to expose the data that we really want to show. With this methodology, we wanted to go one step further, and we are going to differentiate between the domain layer and the application layer, which often tend to mix, and we are only going to allow interaction with our domain if the structure of the data is adequate.
Next, we will proceed to implement the interaction of a database with our domain. To do this, we will need to define a port that defines the functionalities that our domain can perform with the external dependency and an adapter that implements the relationship between the external dependency and our domain.
In this case, as a port, we will define a repository interface in which we will define the methods that we will need from our domain. For this we have created the ProductRepositoryInterface class, which will perform the port function:
interface ProductRepositoryInterface
{
public function find(string $id): Product;
public function save (Product $product): void;
}
And, as an adapter, we have created the ProductRepositoryDoctrineAdapter
class that will implement the functions defined in the previous interface. These will be impure functions, since they interact directly with the external dependency and adapt the data to be used in our domain, for example:
class ProductRepositoryDoctrineAdapter implements ProductRepositoryInterface
{
/** @var EntityRepository */
private $productRepository;
/** @var EntityManager */
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
$this->productRepository = $entityManager->getRepository(Product::class);;
}
public function find(string $id): Product
{
$this->productRepository->find($id);
}
public function save(Product $product): void
{
$this->entityManager->persist($product);
$this->entityManager->flush();
}
}
In this case, we can see how the external dependency is injected into the constructor, and how the functions defined in the interface that we use as a port are implemented.
Now let’s see how all this will interact with our domain. For this, we have created an application layer, in which we will have the actions that our system can perform. Tjos layer will be where we will introduce the calls to the adapters, to which we will have access by means of dependency injection.
class ProductApplicationService
{
private $productRepository;
public function __construct(ProductRepositoryInterface $productRepository)
{
$this->productRepository = $productRepository;
}
(...)
}
As we can see, in the constructor we have defined the port as ProductRepositoryInterface
and not the adapter, why? Very simply, if, in the future, instead of using Doctrine with MySQL we just want to persist our data in a Redis cache, for example, it would be as simple as creating a new adapter that complies with the contract defined in ProductRepositoryInterface
and changing the injection of the dependency that we have defined in a YAML file:
services:
product.application.product_service:
class: ProductBundle\Application\ProductApplicationService
arguments: ['@product.repository.product.doctrine_adapter']
product.repository.product.doctrine_adapter:
class: ProductBundle\Repository\ProductRepositoryDoctrineAdapter
arguments: ['@doctrine.orm.entity_manager']
It will be as simple as declaring the new adapter, and replacing the dependency of the application service. This shows us that we have built a completely agnostic domain of its dependencies, since we will not have to touch a single line of productive code to change the dependency.
Now let’s see the use of the adapter in the application service:
class ProductApplicationService
{
(...)
public function createProduct(
CreateProductRequestDto $createProductRequestDto
): CreateProductResponseDto {
$product = Product::fromDto($createProductRequestDto);
$this->productRepository->save($product);
return $product->toDto();
}
}
As can be seen, in the application service we are conducting the orchestration between the pure domain (the creation of a domain entity, based on the data structure CreateProductRequestDto
) and the call to the adapter to perform the persistence of the created object. In this way we guarantee that the domain layer will never become contaminated or interact with adapters of external dependencies, thus focusing only on the business rules that the application has.
As a final point, and as you may have noticed previously, we have created two Data Transfer Objects (DTOs): CreateProductRequestDto
and CreateProductResponseDto
. This is to ensure the correct structure of the input and output data of our domain. If we force a specific DTO to be sent to us from the controller, we are abstracting from how the data is being sent to our application and guaranteeing that they enter our domain with the appropriate data structure. The same happens with CreateProductResponseDto
, which assures us a centralized way of deciding which data we expose from our domain abroad.
If we try to extrapolate this practical exercise, applying Hexagonal Architecture, to the image that appears in the previous section, we can see how the definition of our layers fits perfectly into the theoretical scheme:
Structure of our example applied to the Hexagonal Architecture scheme.
Conclusions: Applying Hexagonal Architecture
Reviewing the concepts that we have enumerated in the first part of our article together with the developed example, these are the outcomes of the application of these concepts in our application:
- The persistence of our domain data has been completely abstracted.
- An example of a port and an adapter has been successfully implemented.
- DTOs have been implemented to see an example of how to transfer the data between the different layers of abstraction (Controller and Application layer, which separates our domain from the framework).
- The separation by layers gives us a system that is easily tested in a unitary way, allowing us to mock the dependencies of our application service.
- A completely agnostic domain of the dependencies, the framework, and the UI has been created.
I am aware that this has been a very simple example of applying Hexagonal Architecture, but I wanted to emphasize the development of the architecture without getting lost in the details of implementation of the application itself. We have to understand the theories applied in this exercise and what they bring to our day-to-day lives as developers, helping us to implement clean, maintainable, and easily testable code.
Published at DZone with permission of Javier Gomez. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments