Modeling an Aggregate with Eloquent

Share this article

The Aggregate pattern is an important part of Domain Driven Design. It prevents inconsistencies and is responsible for enforcing business rules within a collection of objects. For these reasons alone, it is clear to see why it is a key component of a domain model.

Architectural advice recommends that the layer containing the Domain Model be independent from infrastructural concerns. While this is good advice, the Active Record pattern on the other hand wraps a row in the database. Because of this, it is almost impossible to decouple from the persistence layer.

Mixing persistence concerns into a Domain Model can become complex and lead to a lot of bad decisions. This does not mean that it is impossible to create an Active Record Domain Model. In this article, we will work through an example of building an Aggregate which also extends Eloquent: a popular Active Record ORM.

What is an Aggregate?

An Aggregate is a collection of objects which act as a single unit – with one of these objects acting as the Aggregate’s Root. Interaction from outside of the Aggregate must only communicate through the Root object. Aggregate Roots can then manage the consistency of all the objects within its boundary.

A set of elements joined into one

Boundaries for Aggregates define the scope of a transaction. Before the transaction can be committed, manipulation to the cluster of objects must comply with the business rules. Only one Aggregate can be committed within a single transaction. Any changes required to additional Aggregates must be eventually consistent, happening within another transaction.

In his book, Implementing Domain-Driven Design, Vaughn Vernon outlines a set of guidelines in which he calls: “the rules of Aggregate design”:

  1. Protect True Invariants in Consistency Boundaries
  2. Design Small Aggregates
  3. Reference Other Aggregates Only By Identity
  4. Use Eventual Consistency Outside the Consistency Boundary

An Example Blog

Since this is technically a blog post, it only seems fitting to use a Blog as our context. We will need a Post to have its own identity. A Post will need a Title along with Copy. Posts are written by an Author and can be commented on – but not if the Post has been locked.

Post is a good candidate as an Aggregate Root, with Title and Copy Value Objects. Author however will be on the outside of our boundary and only referenced by identity. But what about Comments?

We will model Comment as an Entity within the Post Aggregate in our example. It is important to note however that clustering too many concepts within one Aggregate will result in large numbers of objects being hydrated. This can have an impact on performance, so take care and ensure you model Small Aggregates with clearly defined boundaries.

Let’s take a look at what our Post aggregate may look like first without extending an ORM:

final class Post
{
    /**
     * @var PostId
     */
    private $postId;

    /**
     * @var AuthorId
     */
    private $authorId;

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

    /**
     * @var Copy
     */
    private $copy;

    /**
     * @var Lock
     */
    private $locked;

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

    /**
     * @param PostId $postId
     * @param AuthorId $authorId
     * @param Title $title
     * @param Copy $copy
     */
    public function __construct(PostId $postId, AuthorId $authorId, Title $title, Copy $copy)
    {
        $this->postId = $postId;

        $this->authorId = $authorId;

        $this->title = $title;

        $this->copy = $copy;

        $this->locked = Lock::unlocked();

        $this->comments = [];
    }

    public function lock()
    {
        $this->locked = Lock::locked();
    }

    public function unlock()
    {
        $this->locked = Lock::unlocked();
    }

    /**
     * @param Message $message
     */
    public function comment(Message $message)
    {
        if ($this->locked->isLocked()) {
            throw new PostIsLocked;
        }

        $this->comments[] = new Comment(
            CommentId::generate(),
            $this->postId,
            $message
        );
    }
}

Here we have a simple class for our post. Within the constructor, we enforce invariants ensuring the object can not be created without essential information. Key information, such as title and copy, must be provided if a post object is to be created.

We also have behaviour for locking and unlocking a post – along with adding a comment. New comments are being prevented if the post is locked by throwing a PostIsLocked exception. This encapsulates the business rule within the Aggregate and makes it impossible to comment on locked posts.

Introducing Eloquent

As a starting point, we have been modelling our domain without extending an ORM. Let’s introduce Eloquent into our Post class and discuss the changes:

final class Post extends Eloquent
{
    public function lock()
    {
        $this->locked = Lock::locked();
    }

    public function unlock()
    {
        $this->locked = Lock::unlocked();
    }

    /**
     * @param Message $message
     */
    public function comment(Message $message)
    {
        if ($this->locked) {
            throw new PostIsLocked;
        }

        $comment = new Comment;
        $comment->postId = $this->postId;
        $comment->message = (string) $message;

        $this->comments->add($comment);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function getLockedAttribute($value)
    {
        return Lock::fromString($value);
    }

    public function setLockedAttribute(Lock $lock)
    {
        $this->attributes['locked'] = $lock->asBool();
    }
}

Well, we’ve definitely written less code. Let’s take a closer look.

The first thing to notice is the removal of the class properties. Eloquent stores all the properties within a protected $attributes array on each class with magical __get() and __set() methods for access and manipulation.

We also no longer have a constructor. Eloquent hydrates the object’s protected $attributes array by passing in the row data through the constructor. Eloquent relies on this ability to provide several features such as loading relationships.

Finally, we have added an additional method: comments(). Implementing this new method allows us to quickly make use of Eloquent’s Relationships. We could have called hasMany() internally within comment() without exposing a public method, but this would prevent eager loading if we decided to add an Active Record Query Repository.

Attributes

Let’s talk about the removal of the class properties. Does this make a huge difference? The properties in our original class were private anyways – does storing them in a protected $attributes array really change much?

Actually, this is probably the most significant change between a traditional object and an Active Record. Why? Because we are no longer dealing with an Object, we are dealing with a Data Structure. Objects expose behaviour, with data being hidden. Data Structures are exactly the opposite: they expose data and have no behaviour.

Eloquent provides data access with a public __get() magic method. Since we can access our data directly, it would be very easy to leach behavior which should be in our model. Remember the business rule that stated: “you can not comment on locked posts”? Imagine the following implementation:

if ($post->locked->isLocked()) {
    throw new PostIsLocked;
}

$post->comment($message);

This could easily lead to an Anemic Domain Model with little more than some “getters and setters”. Instead, apply the Tell-Dont-Ask principle and tell the model what it needs to do:

try {
    $post->comment($message);
}
catch (PostIsLocked $e)
{
    // Nope!
}

Be careful not to strip your objects of behavior. Just because you can directly access and manipulate your data doesn’t mean that you should.

Value Objects

Several additional methods were added to our Active Record version to allow the usage of Lock. Data within our object must cast to scalar types, but we can still use Value Objects if we take advantage of Eloquent’s accessor and mutator methods.

public function getLockedAttribute($value)
{
    return Lock::fromString($value);
}

public function setLockedAttribute(Lock $lock)
{
    $this->attributes['locked'] = $lock->toBool();
}

As you can see, this adds additional public methods to an object focused on persistence mapping – instead of providing behavior. This isn’t typically something to worry too much about with Active Record, especially when you consider we are inheriting a base-class which already contains public methods such as save() and forceDelete(). You will become accustomed to this particular trade-off when modeling with Active Record.

Where possible, you should take advantage of any ORM features which cast to and from Value Objects. Value Objects enforce their own invariants that can’t be invalid. The Lock class in our example is trivial, but imagine an Email object. You can be sure that instances of Email objects are valid. You may even wish to enforce additional business rules, such as ensuring the email can only belong to a particular domain.

Invariants

An invariant is a rule which must always be valid within our domain. By definition, they are “constant throughout a certain range of conditions”. Imagine I were to write this article without a title – would it be valid? What about an author without a name? An alphabet without J and K?

Violating an invariant would violate the essence of the concept. Aggregate Roots enforce invariants for itself and the cluster of objects within the Aggregate boundary. Entities also enforce invariants of their own, as do Value Objects, but only the Aggregate Root is responsible for enforcing rules for the cluster.

We can enforce invariants by using the class constructor and explicitly require arguments upon object instantiation. This would make it much harder for an object to be in a state which violates the concept. However, as we covered earlier, Eloquent requires the constructor to hydrate an object. This means we are unable to enforce invariants and protect our objects from being invalid when instantiated.

Udi Dahan suggest that we should never directly create Aggregate Roots and instead, use a factory method from another object to return our new instance:

final class Author extends Eloquent
{
    // snip…

    /**
     * @param Title $title
     * @param Copy $copy
     * @return Post
     */
    public function draftPost(Title $title, Copy $copy)
    {
        return new Post([
            'title' => $title,
            'copy' => $copy,
            'author_id' => $this->id
                ]);
    }
}

In this example, we take Udi Dahan’s advice and use a factory method within another object to construct our Post. This allows us to use the language of the business to explain that an Author drafts a Post – but it also allows us to enforce invariants when we were otherwise unable to do so. Using this approach when modeling with Active Record requires softer skills to implement and enforce – since we cannot prevent direct object instantiation, the team must be aware of “the whats and hows” of creating Aggregates.

Another alternative is to use a named constructor when creating a new instance:

final class Post
{
    // snip…

    /**
     * @param AuthorId $authorId
     * @param Title $title
     * @param Copy $copy
     * @return static
     */
    public static function draft(AuthorId $authorId, Title $title, Copy $copy)
    {
        return new Post([
            'title' => $title,
            'copy' => $copy,
            'author_id' => $authorId
        ]);
    }
}

Just like our previous factory method, we use the language of the business to describe that a draft of a Post gets created. We are also able to type hint our Value Objects and enforce invariants.

Again, this approach has a significant drawback when we consider inheriting from an Active Record ORM. Since we are extending Eloquent, there are already a number of static methods in the object graph. Not all domain concepts will have nice workflow titles such as “draft”. Often the most appropriate name for a method to describe creation is simply “create”. Unfortunately, Eloquent already has a static create() method.

There are other creational patterns which could help if separating the responsibility of object creation makes sense in your context.

Relationships

Aggregates allow us to treat a group of objects as a single unit. In our example, a post can have many comments. How might we go about coding this with Eloquent?

$post->comments()->save($comment);

Sure, this will get the job done, but there is a problem. By asking the post for its related comments, we completely bypass the Aggregate Root. All communication with an Aggregate must go through the Root so it can enforce the rules of the business. We were told by the business that “posts which are locked can not be commented on”, however we have just been able to save a new comment for a post without checking it’s locked status.

Our example pushes this logic into the Post class, where it encapsulates the creation of Comment along with the check for the locked status:

$post->comment($message);

With this approach, we no longer leak persistence concerns. Code outside of the $post object does not need to explicitly call save() as it has already been invoked internally.

Conclusion

We’ve shown that modeling an Aggregate with Active Record is possible – but it is complex and has a lot of pitfalls. It can become quite messy if you try to treat an Active Record like a traditional object. Active Record is solely about the data whereas objects expose behavior and hide the data. We are fundamentally misusing the Active Record pattern – and the best we can do is to add behavior into what is essentially a data structure.

This article is not attempting to say that Active Record is bad, nor is it an appeal saying to always use it. It is simply a tool which we can use depending upon the situation. Active Record is a great tool when focusing on RAD, however in some situations it’s just not a good fit. I think, due to the amount of trade-offs discussed, modeling Aggregates fall under the latter.

Some development teams that are modeling domains may be happy to make the compromises needed to model Aggregates with Active Record. Others, however, will believe that it is never a good idea to have a model which can not be decoupled from infrastructure.

Who is correct? The age-old answer to all questions: it depends.

Frequently Asked Questions (FAQs) about Modeling Aggregate with Eloquent

What is an aggregate in the context of Laravel Eloquent?

In the context of Laravel Eloquent, an aggregate is a cluster of associated objects that are treated as a single unit. It is a pattern in Domain-Driven Design (DDD) where an aggregate ensures the consistency of changes being made to the objects within the aggregate boundary. The aggregate root is the object that controls access to the aggregate.

How does Laravel Eloquent handle aggregates?

Laravel Eloquent does not have built-in support for aggregates. However, you can implement aggregates in Laravel Eloquent by creating a model that represents the aggregate root and encapsulates the logic for maintaining consistency within the aggregate.

What is a value object in Laravel Eloquent?

A value object is an immutable object that represents a descriptive aspect of the domain with no conceptual identity. They are used to represent things like money, a date range, a string, or any other value that doesn’t have an identity in your domain.

How can I use value objects in Laravel Eloquent?

You can use value objects in Laravel Eloquent by creating a class that represents the value object and implementing the necessary logic in the class. You can then use this class in your Eloquent models.

What is a mutator in Laravel Eloquent?

A mutator in Laravel Eloquent is a method in an Eloquent model that allows you to alter attribute values before they are inserted into the database. You can use mutators to implement value objects in your Eloquent models.

How can I implement aggregates in Laravel Eloquent?

You can implement aggregates in Laravel Eloquent by creating a model that represents the aggregate root and encapsulates the logic for maintaining consistency within the aggregate. This model should have methods for performing operations on the aggregate, and these methods should ensure that the aggregate remains in a valid state.

What is Domain-Driven Design (DDD)?

Domain-Driven Design (DDD) is an approach to software development that emphasizes collaboration between technical experts and domain experts. DDD focuses on creating software that is a model of the domain, which is the area of knowledge or activity that the software is intended to support.

How does DDD relate to Laravel Eloquent?

Laravel Eloquent is an implementation of the Active Record pattern, which is a different approach to modeling the domain than the approach used in DDD. However, you can use DDD concepts like aggregates and value objects in your Eloquent models to create a more expressive domain model.

What are the benefits of using aggregates in Laravel Eloquent?

Using aggregates in Laravel Eloquent can help you ensure the consistency of your data by encapsulating the rules for changing the state of the objects within the aggregate. This can make your code easier to understand and maintain, and it can help prevent bugs that could be caused by inconsistent state.

What are the challenges of using aggregates in Laravel Eloquent?

One of the challenges of using aggregates in Laravel Eloquent is that Eloquent does not have built-in support for aggregates. This means that you need to implement the aggregate logic yourself, which can be complex. Another challenge is that using aggregates can make your code more complex, as you need to manage the aggregate root and the objects within the aggregate.

Andrew CairnsAndrew Cairns
View Author

Andrew is passionate about Domain-Driven Design, Test-Driven Development, System Architecture and Agile Methodologies. He is Technical Lead at GatherContent.

aggregateBrunoSeloquentframeworklaravelOOPHPPHPphp frameworkphp frameworks
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week