Applying Hexagonal Architecture to a Symfony project

Share This Post

Hexagonal 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 in The Clean Architecture to ensure a clean, scalable and adaptable architecture to change:

  • 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 adulterate the functionality of this one.
  • 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 enter to modify it by completely external factors to the business rules.
  • 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 and a future problem 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 part of the so-called Clean Architectures, which are based on the following scheme:

hexagonalHexagonal 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. If you want to know more about Hexagonal Architecture you can find more information in the blog of Apiumhub.

  Continuous Architecture principles and goals

Practical exercise: applying Hexagonal Architecture

We will perform a simple exercise that allows us to see in a practical way, the application of the concepts named above. 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 of 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 a 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 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 layer named as domain and the application layer, which often tend to mix, and we are only going to allow interaction with our domain if the structure of 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.

  B-Wom case study: Pure MVP application

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. In this layer also, it 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 ProductRepositoryInterface and not the adapter, why? Very simple, 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 change the injection of the dependency that we have defined in a yml 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 last 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 the CreateProductResponseDto, which assures us a centralized way of deciding which data we expose from our domain abroad.

  Software Architecture Journey: Interview with Sonya Natanzon

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:

Screenshot 2018 10 30 at 08.45.49Structure 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.

If you are working on a Symfony project and you need software development help, let us know! We would be happy to know more about your project!

And if you would like to know more about applying Hexagonal Architecture, I highly recommend you to subscribe to our monthly newsletter by clicking here.

CTA Software

And if you found this article about applying Hexagonal Architecture to a Symfony project interesting, you might like…

Scala generics I: Scala type bounds

Scala generics II: covariance and contravariance

Scala generics III: Generalized type constraints

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability

iOS Objective-C app: sucessful case study

Mobile app development trends of the year

Banco Falabella wearable case study 

Mobile development projects

Viper architecture advantages for iOS apps

Why Kotlin ? 

Software architecture meetups

Pure MVP in Android

Be more functional in Java ith Vavr

Author

6 Comments

  1. Iosif

    If ProductRepositoryInterface will reside inside of the Application layer, then where’s the place of the implementations of this interface? Are those implementations belong to the Infrastructure layer? If this is true, then we have a leaking of information from the Domain layer to the Infrastructure layer because the implementations of the ProductRepositoryInterface will return instances of Product entity, which resides inside of the Domain layer. Isn’t something wrong here?

    • Javier Gómez

      Hi!
      The ProductRepositoryInterface resides on the Domain layer, but I’m inyecting it in the Aplication layer because in this simple example we don’t have a Domain Service. If we had a Domain service, the ProductRepositoryInterface should be inyected there.
      As you said, these implementations belong to Infrastructure layer. On this example we’re aplying the Dependency Inversion principle. We’re abstracting our Domain from external dependencies, because we’re defining the “contract” on the Domain layer through the Interface. So, the responsibility of adapting itself to work with the other one relies on the Infrastructure Adapter to work with the Domain.
      Regarding your last point, I showed a pragmatic and most commonly used example. But you’re right, if we want to be purist, we should create a kind of InfrastructureEntity to work with the repository implementation and transform it into our DomainEntity when we come back to Domain Layer.
      I hope that it helps and thanks for your comment 🙂

      • Iosif

        Thank you for your response! It helps me to better understand the Hexagonal architecture.

  2. Luis Diego Flores

    Hi guys,
    I think ProductRepositoryInterface can live inside the Application Layer, I create the interface where the client consume it(YAGNI).
    The main reason about Domain Services is orchestrate invariants that belongs to diferent entities, so if this invariants must access to repository the ProductRepositoryInterface must live inside the Domain.
    We can use the memento pattern to isolate domain entities from infrastructure layer.
    What do you think?

  3. Javier Gómez

    Hi Luis,
    You’re right, I created the ProductRepositoryInterface where I usually create it, because it’s a simple example and there isn’t a Domain service, but I assume that in the future it will be. So, the most common place to allocate the ProductRepositoryInterface is the Domain Layer, because is where you’re working with Domain entities, as you defined in the interface. In Application layer you still can work with DTOs or another kind of value objects.
    So, it’s true that we maybe need to declare a Domain service and inject the ProductRepositoryInterface there, but I think that the ProductRepositoryInterface should be in the Domain layer, because is returning Domain entities, that should be uses in the Domain layer.
    But I agree that the maybe my example could isn’t clear enough, because it’s too much simple. I wanted to do really simple to emphasize on the flow between layers, and maybe I left some important points as the Domain Service.
    Hope that it helps for you, and thanks for your comment!

  4. Matheus Dall'Rosa

    What about reports? Say i want to build a Doctrine Query object and inject it on a paginator, should i make the entire report on the Application Layer?
    About the DTOs i make: DTO extends DomainClass and put the validation rules on methods, but there are validations that come from exceptions which are thrown by the doctrine when there is a FK violation, should i catch the exceptions thrown by Doctrine and throw exceptions that are present on the domain?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange