Child Entities lifecycle, making the implicit explicit

Child Entities should not have its own lifecycle. You should not blindly create entities without clearly thinking about its relationships. Thus making implicit relationships. We should make relationships between entities and its lifecycle explicit.

This means the relationships between entities should be clear in a blink of an eye. And should not require someone new or less experience in the code base much time to read and reason about the domain model.
The act of clean code and a good code base is making it easy to read and reason about.

First, before we can do that, let us take a look at what exactly defines a child entity. What kind of relationships we have and what this means for the lifecycle of these entities. And then we look at how to refactor a child entity to make the implicit explicit.

A Child Entity?

What exactly makes an entity a child entity?  Before we can answer that question, we need to understand that there are 2 kinds of relationships. We have a so-called root entity. And this entity can have 2 types of relationships; child entities and related entities. The one is a strong relationship while the other is weak.

An entity itself is an object in your domain model that has an identity and lifecycle. Another name for an entity is a domain object and this is part of your domain model.

Lifecycle means that this an object that can be created, changed and deleted. Through this lifecycle, we need to be able to identify this object. For this, we have an identity or id field.

A root entity is an entity which lifecycle can be directly manipulated by using that entity explicitly. A related entity is an entity that is related to the root entity. If for example, we have a Ticket entity. A Ticket can be assigned to a User entity. If the User entity gets changed, then the Ticket can still exist and be assigned to another User. It has its own lifecycle. This is also the case when a Ticket gets deleted, the User will not be affected by this. This is a weak relationship. In this case, User is a related entity for Ticket.

But if we have comments on our Ticket. These comments cannot exist without a Ticket. By deleting the Ticket, all the related Comments should also be deleted. This is a strong relationship. Comment is a child to the Ticket entity. It has no use to manipulate its lifecycle outside the Ticket entity. It should be a child entity whose life cycle is dependant on the root entity and thus should only be manipulated through the Ticket entity. This is a child entity.

Refactoring a child entity

Let us start from a Ticket entity that has a child entity called TicketComment. This is, in reality, a child entity, but it’s not as explicit as it should. Prefixing it with Ticket is not the way to go…

<?php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

class Ticket
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $title;

    /**
     * @var Collection
     */
    private $comments;

    public function __construct(string $title)
    {
        $this->title = $title;
        $this->comments = new ArrayCollection();
    }

    public function addComment(TicketComment $comment): void
    {
        $this->comments->add($comment);
    }
}
<?php
namespace App\Entity;

class TicketComment
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $user;

    /**
     * @var string
     */
    private $text;

    public function __construct(string $user, string $text)
    {
        $this->user = $user;
        $this->text = $text;
    }
}

The codebase where we got this from, creates a ticket with a comment in its code. A dedicated repository for each entity is used to persist.

$ticket = new Ticket('My Ticket');

$comment = new TicketComment('Jeffrey', 'A comment for my ticket.');
$this->ticketCommentRepository->save($comment);

$ticket->addComment($comment);
$this->ticketRepository->save($ticket);

You will notice. TicketComments should only exist if there is a ticket, and is always part of a collection of comments inside the Ticket entity.

The solution we have now works. But it’s not as explicit or clean as we want it to be.

No repository for child entities

There is no reason in the previous example to save the child entity through its own repository. There is no reason to have a repository for this entity. Because a comment should only exist as part of a ticket. This means that the comments lifecycle is dependent on the Ticket entity. This is what defines this entity as a child entity of Ticket.

If you add a new Comment object to the comments collection of a ticket. Doctrine will persist all the entities in the collections of the persisted entity.

This means that this will have the same result as before:

$comment = new TicketComment('Jeffrey', 'A comment for my ticket.');
$ticket->addComment($comment);
$this->ticketRepository->save($ticket);

But this has less code, but still, we can work on readability.

Increasing readability

Now, another way to increase readability is to not construct the child entity and adding it to the collection of the root entity. But directly create it from the root entity. Like this:

$ticket = new Ticket('My Ticket');
$ticket->addComment('Jeffrey', 'A comment for my ticket.');
public function addComment(string $user, string $text): void
{
    $comment = new TicketComment($user, $text);
    $this->comments->add($comment);
}

This explicitly makes your child entities lifecycle dependant on the root entity. A child entity should not be exposed to the outside world. It does not exist outside Ticket. If we delete ticket, its comments should also disappear. We can not have comments without a ticket.

Subdirectory for children

Another trick to make it more clear which entities are the children of which root entity is to create a sub-namespace for the children and to name them more appropriate.

<?php
namespace App\Entity;

class Ticket

namespace App\Entity\Ticket;

class Comment

You are of course free to choose how exactly you do this. You could also create a namespace for each root entity and also place the root entity itself in it. This solution is a first step in the right direction. If you apply a good architecture then most of the time you might also work with aggregates, which is the same idea.

Conclusion

The goal of this article is to make you think more about the relationships of your entities. Which entities can be defined as root and child. What are the relationships between each entity?

It’s always a good idea to take on concepts from DDD and aggregates. Even when not applying DDD or any variation on it.

Not taking the time to think about your entities relationships is a recipe for disaster in the long term. It will lead to unnecessary repositories, reducing readability and increasing complexity.

I keep repeating this over and over. But you should have a good grasp of different ideas and solutions. And then combine these to solve your problem. You should always strive to make the implicit explicit.