Hexagonal architecture in a Symfony project: Working with Domain Identifiers

Share This Post

When we talk about Domain Identifiers, we are talking about the ID of an entity in our domain. Many times, it may seem like a simple procedure to assign an ID to an instance of a domain entity, and therefore in many cases this repetitive task is assigned to the ORM that we are using (Doctrine in our case).

In this article we want to emphasize the importance of assigning the ID from our own domain, demonstrating the benefits that it brings to us and understanding the problems that can designate this task to an external agent such as an ORM ( Doctrine).

Before continuing, I recommend you, if you have not already done so, that you look at the first article on hexagonal architecture in the blog of Apiumhub since some of the concepts that are going to be discussed below are defined and explained there.  

 

Hexagonal architecture in a Symfony project: Working with Domain Identifiers

 

Why should we assign the ID to Domain?

Based on my experience, many of the PHP projects with Symfony in which I have worked, delegates the task of assigning the ID to the entity in the ORM. This a priori can give us certain problems, for example it makes it very difficult to test, since we delegate a part of the construction of our entity to an external agent.

To better understand what I am saying, we will see it more clearly in an example. Let’s have an an example of the product entity we had in the first article in which we talk about Hexagonal Architecture:


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
        );
    }
}

 

This class currently has a configuration file in yml format in which the mapping to the database is declared, and the automatic assignment of the ID has been configured incrementally:


ProductBundle\Domain\Product:
  type: entity
  fields:
    id:
      type: integer
      id: true
      generator:
        strategy: AUTO
    name:
      type: string
    reference:
      type: string
  lifecycleCallbacks: {}

 

As we can see, an ID of type integer is being assigned automatically and incrementally. Here at first glance, we can observe 2 problems:

Difficult testing: Performing unit tests, the ID of our entity will always have a NULL value, since we will not have an ID in our entity until it is persisted in the database, which by definition should never be so.

  Sacrificial architecture

Dependency of external agents: At the moment that an instance of the entity is built, said instance makes sense in itself within our domain, therefore it should have an assigned ID from the moment of its creation, regardless of whether it has been persisted or not in the database (That would be the moment in which the assignment of the ID with the current configuration would be carried out). An instance of a class should be correct at the same time it is created, since in the constructor itself we will have all the necessary checks to allow its creation. In the initial scenario, this does not happen, because until we persist the entity, it will not be complete, meaning that despite having completed its construction, the entity is still incomplete, and therefore, incorrect.

Besides breaking with these two principles of hexagonal architecture best practices, we have a clear security problem. In case we want to expose the data of our product by accessing through its ID, we are exposing an id of autogerremental integer type. So any user of our API could access all the data of our products. We will try to solve this problem as well. Lastly, by not using ids with simple integers, we would also solve a possible collision problem if we worked with several servers at the same time on the same database.

So, we will try to improve our code, applying iteratively the concepts defined in the previous article about our domain entity.

 

Remove external agents

First of all we must abstract our domain from any external agent or dependency, in this case, we are going to move the generation of the ID of the entity to the constructor of the entity, instead of doing it from the configuration in the yaml file:


private function __construct(
    string $name,
    string $reference
) {
    $this->id = Uuid::uuid4();
    $this->name = $name;
    $this->reference = $reference;
    $this->createdAt = new DateTime();
}

ProductBundle\Domain\Product:
  type: entity
  fields:
    id:
      type: string
      id: true
    name:
      type: string
    reference:
      type: string
  lifecycleCallbacks: {}

In this way, we manage to abstract from the external dependence that the ORM gives us. And the id field is defined as a unique identifier of type string, but its generation is given from the domain.

In addition, when using id UUID type, we eliminate access to products with ids sequentially, and we guarantee that it is almost impossible to access other products that we do not want to expose the user, as well as possible collisions between id’s.

 

Improving the Testability

Still, at first glance we can recognize that the generation of the ID, despite being within our domain, is not in the best possible place. At this time, we could not test the entity correctly, since we have no way to precede the final state of the object once created, since the ID is generated randomly within the constructor of the entity itself.

  DASA DevOps Awards 2022: Nominations Open

In addition, we could only create new objects, but not hydrate objects given from the database, this failure indicates that the generation of the ID is not in the appropriate place.

First of all, we will perform the test to verify that the behavior of the construction of the entity is as expected:


public function testProductDomainEntity()
{
    $createRequestDto = new CreateProductRequestDto('nameTest', 'reference-124');
    $product = Product::fromDto($createRequestDto);
    $result = $product->toDto();
    $expected = new CreateProductResponseDto(
                    Uuid::uuid4(),
                    'nameTest',
                    'reference-124',
                    new DateTime()
                );
    self::assertEquals($expected, $result);
}

As we can see, as the private constructor is defined, the entity is built through the static method fromDto, with the DTO CreateProductRequestDto. Finally, to access the data we want to show from our entity, we must perform the transformation using the method toDto () to access the data we want to expose our entity. As you can guess, the test does not pass because both the id and the createdAt do not match.

This makes us think that to improve the test we must extract the creation of both fields of our entity:


private function __construct(
    Uuid $id,
    string $name,
    string $reference,
    DateTime $createdAt
) {
    $this->id = $id->toString();
    $this->name = $name;
    $this->reference = $reference;
    $this->createdAt =$createdAt;
}
public static function fromDto(CreateProductRequestDto $createProductResponseDto): Product
{
    return new Product(
        $createProductResponseDto->id(),
        $createProductResponseDto->name(),
        $createProductResponseDto->reference(),
        $createProductResponseDto->createdAt()
    );
}

As you can see, the fields of ID and createdAt have been added to CreateProductRequestDto. This makes us do some small changes in our test:


public function testProductDomainEntity()
{
    $productId = Uuid::uuid4();
    $createdAt = new DateTime();
    $createRequestDto = new CreateProductRequestDto(
        $productId,
        'nameTest',
        'reference-124',
        $createdAt
    );
    $product = Product::fromDto($createRequestDto);
    $result = $product->toDto();
    $expected = new CreateProductResponseDto(
        $productId->toString(),
        'nameTest',
        'reference-124',
        $createdAt
    );
    self::assertEquals($expected, $result);
}

So we have the test in green, thanks to the extraction of the Id and createdAt fields, which allows us to define the expected result before the execution of the test.

 

Adding Domain Identifiers

As we can see, even having extracted the dependency of Doctrine in the generation of our ID, now we still have a dependency on an external library, to generate the UUID. If we want to go a little further and improve the abstraction of the dependency of our domain of external agents, we can create our own domain ID, in which we will encapsulate the dependency of the external library within this value object:


class ProductId
{
    private $id;
    private function __construct(string $id)
    {
        $this->id = $id;
    }
    public static function generate(): ProductId
    {
        return new self(Uuid::uuid4()->toString());
    }
    public function build(string $id): ProductId
    {
        if (Uuid::isValid($id)) {
            return new self($id);
        } else {
            throw new InvalidIdFormatException("Invalid ProductId format: ".$id);
        }
    }
    public function value(): string
    {
        return $this->id;
    }
}

We will have to refactor our domain entity and the createProductRequestDto to eliminate the dependency of the UUID library:


class Product
{
    private $id;
    private $name;
    private $reference;
    private $createdAt;
    private function __construct(
        ProductId $id,
        string $name,
        string $reference,
        DateTime $createdAt
    ) {
        $this->id = $id->toString();
        $this->name = $name;
        $this->reference = $reference;
        $this->createdAt =$createdAt;
    }
    public static function fromDto(CreateProductRequestDto $createProductResponseDto): Product
    {
        return new Product(
            $createProductResponseDto->id(),
            $createProductResponseDto->name(),
            $createProductResponseDto->reference(),
            $createProductResponseDto->createdAt()
        );
    }
    public function toDto()
    {
        return new CreateProductResponseDto(
            $this->id,
            $this->name,
            $this->reference,
            $this->createdAt
        );
    }
}

class CreateProductRequestDto
{
    private $id;
    private $name;
    private $reference;
    private $createdAt;
    public function __construct(
        ProductId $id,
        string $name,
        string $reference,
        DateTime $createdAt
    ) {
        $this->id = $id;
        $this->name = $name;
        $this->reference = $reference;
        $this->createdAt = $createdAt;
    }
    public function id(): ProductId
    {
        return $this->id;
    }
    public function name(): string
    {
        return $this->name;
    }
    public function reference(): string
    {
        return $this->reference;
    }
    public function createdAt(): DateTime
    {
        return $this->createdAt;
    }
}

Finally, modify our test with the new domain identifier:


class ProductTest extends TestCase
{
    public function testProductDomainEntity()
    {
        $productId = ProductId::generate();
        $createdAt = new DateTime();
        $createRequestDto = new CreateProductRequestDto(
            $productId,
            'nameTest',
            'reference-124',
            $createdAt
        );
        $product = Product::fromDto($createRequestDto);
        $result = $product->toDto();
        $expected = new CreateProductResponseDto(
            $productId,
            'nameTest',
            'reference-124',
            $createdAt
        );
        self::assertEquals($expected, $result);
    }
}

With this last iteration we have reduced the dependency of the external library UUID to a single point in our domain, within the value object ProductId. So that if in the future we want to modify the format of the identifiers of our domain, we should only change the dependency in a single point of our domain.

  JavaScript frameworks you should know

 

Conclusions: Domain Identifiers

With this very simple example, we have been able to demonstrate the problems that one of the most extended practices in PHP projects gives us. As we have seen, it is a practice that we must avoid. As we saw in the previous article, it is very important to isolate our domain from any external dependency, keeping our domain pure and decoupled.

With this practice, in addition to minimizing the dependence of our domain on an external agent to a single and controlled point, we have managed to test 100% of our entity, and to be able to foresee the final result of the construction of the entity, thus ensuring a correct behavior.

Lastly, we have improved the security of our data, since we have avoided exposing data that we do not want to expose avoiding having sequential IDs.

 

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

 

If you found this article about Domain Identifiers interesting, you might like…

 

Applying Hexagonal Architecture to a Symfony project

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

One Comment

  1. Misha

    Hello,
    I got a question, having the uuid encapsulated in a value object sounds reasonable but doesn’t reverse the dependency.
    I’m using ramsey/uuid I strictly forbid my domain layer to have any dependencies (I use deptrac). My value objects are within the domain layer where they are supposed to be.
    I thought about writing a uuid id generator interface and implement Ramsey within my infrastructure layer but on the other hand I feel like it’s over engineered for this little separation.
    How you feel about it? I’d like to hear from you! I really enjoy your article!

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