PHP
Article

Modeling an Aggregate with Eloquent

By Andrew Cairns

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.

  • http://SalaryNet30.com louise adame

    Every
    person now days want to EARN in easiest way…I earn $98978 in week easily,the
    biggest thing is that I earn at home at my laptop..My husband anger on me
    because I can’t spend more time with him…Now he is very happy because I earn
    a lot and spend more time with her at home….You also can Earn…

    ====> To how earn Link in my Prof|£€®

    (dddddddd

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.