search
HomeBackend DevelopmentPHP TutorialHandling Collections of Aggregate Roots – the Repository Pattern

Handling Collections of Aggregate Roots – the Repository Pattern

Core points

  • The warehousing model in Domain Driven Design (DDD) acts as an intermediary between the domain model and the data mapping layer, enhancing data query management and minimizing duplication.
  • Warehousing abstracts the complexity of the data layer from the domain model, promoting clear separation of concerns and persistence ignorance, which is in line with the DDD principle.
  • Implementing warehousing involves encapsulating the logic of data access and operations behind a collection-like interface, which can simplify interaction with the domain model.
  • While warehousing provides significant benefits in management domain complexity and isolation domain logic and data persistence details, their implementation can be too complex for simple applications.
  • The practical use of warehousing can be observed in systems requiring complex queries and data operations, where they provide a more domain-centric language and reduce infrastructure leakage into domain models.

One of the most typical aspects of traditional domain-driven design (DDD) architecture is the mandatory persistence agnosticity demonstrated by the domain model. In more conservative designs, including some implementations based on active records or data table gateways (in pursuit of quite deceptive simplicity, often end up with infrastructure pollution logic), there is always a clear concept of underlying storage mechanisms, usually relational databases. On the other hand, the domain model is conceptually designed from the outset to a strictly “store agnostic” nature, thus shifting any of its persistence logic beyond its boundaries. Even though considering that DDD is somewhat elusive when referring to a “database” directly, in the real world, it is likely that at least one database is running behind the scenes, because the domain model must eventually be persisted in some form. Therefore, it is common to deploy a mapping layer between the model and the data access layer. This not only actively promotes maintaining a considerable degree of isolation between layers, but also protects every complex detail in client code that involves moving domain objects back and forth between gaps in problem layers. Mea culpa To be fair, it is fair to say that handling singularity in the data mapper layer is a considerable burden, and the "write once/permanent use" strategy is often adopted. Nevertheless, the above pattern performs well under rather simple conditions, where only a small number of domain classes are processed by a small number of mappers. However, as the model starts to swell and becomes more complex, the situation can become more awkward, as there will definitely be additional mappers added over time. This briefly suggests that opening the door to persistence neglect can be difficult to implement in practice when using a rich domain model composed of multiple complex aggregate roots, at least if you don't create expensive object graphs in multiple places or embark on the path of sin of repeated implementation. Worse, in large systems that need to extract expensive aggregated root sets from the database that match different conditions, the entire query process itself can become an active, prolific enabler of such flawed duplication if it is not properly concentrated through a single entry point.In this complex use case, implementing an additional layer of abstraction (often referred to as warehousing in DDD jargon) that arbitrates between the data mapper and the domain model effectively helps minimize query logic duplication while exposing the semantics of the real memory set to the model. However, unlike mappers (which are part of the infrastructure), the warehousing itself is characterized by the language of the model, as it is closely tied to the model. And because of its implicit dependence on mappers, it also retains persistence ignorance, thus providing a higher level of abstraction, closer to domain objects. Unfortunately, not every possible application can easily implement the benefits of warehousing, so it is only worth implementing if the situation requires it. In any case, it would be very beneficial to build a small warehouse from scratch so you can see how it works internally and reveal what exactly is under its fairly esoteric shell.

Conduct preliminary preparations

The process of implementing warehousing can be very complex because it actually hides all the details of injecting and processing data mappers after a simplified collection-like API that in turn injects some kind of persistent adapter, and so on. This continuous injection of dependencies, coupled with a large amount of logic, explains why warehousing is often considered a simple look, even if some perspectives currently differ from the concept. In either case, the first step we should take to get up and run functional warehousing is to create a basic domain model. The model I plan to use here will be responsible for modeling the general user, with the basic structure as follows:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}
<?php namespace Model;

class User implements UserInterface
{
    const ADMINISTRATOR_ROLE = "Administrator";
    const GUEST_ROLE         = "Guest";

    protected $id;
    protected $name;
    protected $email;
    protected $role;

    public function __construct($name, $email, $role = self::GUEST_ROLE) {
        $this->setName($name);
        $this->setEmail($email);
        $this->setRole($role);
    }

    public function setId($id) {
        if ($this->id !== null) {
            throw new BadMethodCallException(
                "The ID for this user has been set already.");
        }
        if (!is_int($id) || $id             throw new InvalidArgumentException(
                "The user ID is invalid.");
        }
        $this->id = $id;
        return $this;
    }

    public function getId() {
        return $this->id;
    }

    public function setName($name) {
        if (strlen($name)  30) {
            throw new InvalidArgumentException(
                "The user name is invalid.");
        }
        $this->name = htmlspecialchars(trim($name), ENT_QUOTES);
        return $this;
    }

    public function getName() {
        return $this->name;
    }

    public function setEmail($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "The user email is invalid.");
        }
        $this->email = $email;
        return $this;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setRole($role) {
        if ($role !== self::ADMINISTRATOR_ROLE
            && $role !== self::GUEST_ROLE) {
            throw new InvalidArgumentException(
                "The user role is invalid.");
        }
        $this->role = $role;
        return $this;
    }

    public function getRole() {
        return $this->role;
    }
}

In this case, the domain model is a rather skeletal layer, barely higher than a simple data holder who can self-verify, which defines the data and behavior of some fictitious user only through isolated interfaces and simple implementers. To keep it simple and easy to understand, I will keep the model this streamlined. As the model is already running in easy isolation, let's make it richer by adding another class to it, which handles the collection of user objects. This "add-on" component is just a classic array wrapper that implements Countable, ArrayAccess, and IteratorAggregate SPL interfaces:

<?php namespace ModelCollection;
use MapperUserCollectionInterface,
    ModelUserInterface;

class UserCollection implements UserCollectionInterface
{
    protected $users = array();

    public function add(UserInterface $user) {
        $this->offsetSet($user);
    }

    public function remove(UserInterface $user) {
        $this->offsetUnset($user);
    }

    public function get($key) {
        return $this->offsetGet($key);
    }

    public function exists($key) {
        return $this->offsetExists($key);
    }

    public function clear() {
        $this->users = array();
    }

    public function toArray() {
        return $this->users;
    }

    public function count() {
        return count($this->users);
    }

    public function offsetSet($key, $value) {
        if (!$value instanceof UserInterface) {
            throw new InvalidArgumentException(
                "Could not add the user to the collection.");
        }
        if (!isset($key)) {
            $this->users[] = $value;
        }
        else {
            $this->users[$key] = $value;
        }
    }

    public function offsetUnset($key) {
        if ($key instanceof UserInterface) {
            $this->users = array_filter($this->users,
                function ($v) use ($key) {
                    return $v !== $key;
                });
        }
        else if (isset($this->users[$key])) {
            unset($this->users[$key]);
        }
    }

    public function offsetGet($key) {
        if (isset($this->users[$key])) {
            return $this->users[$key];
        }
    }

    public function offsetExists($key) {
        return ($key instanceof UserInterface)
            ? array_search($key, $this->users)
            : isset($this->users[$key]);
    }

    public function getIterator() {
        return new ArrayIterator($this->users);
    }
}

In fact, putting this set of arrays within the boundaries of the model is completely optional, because using a normal array can produce nearly the same results. However, in this case, by relying on independent collection classes, it is easier to access the set of user objects extracted from the database through an object-oriented API. Furthermore, considering that the domain model must completely ignore the underlying storage set up in the infrastructure, the next logical step we should take is to implement a mapping layer that separates it well from the database. The following are the elements that make up this layer:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}
<?php namespace Model;

class User implements UserInterface
{
    const ADMINISTRATOR_ROLE = "Administrator";
    const GUEST_ROLE         = "Guest";

    protected $id;
    protected $name;
    protected $email;
    protected $role;

    public function __construct($name, $email, $role = self::GUEST_ROLE) {
        $this->setName($name);
        $this->setEmail($email);
        $this->setRole($role);
    }

    public function setId($id) {
        if ($this->id !== null) {
            throw new BadMethodCallException(
                "The ID for this user has been set already.");
        }
        if (!is_int($id) || $id             throw new InvalidArgumentException(
                "The user ID is invalid.");
        }
        $this->id = $id;
        return $this;
    }

    public function getId() {
        return $this->id;
    }

    public function setName($name) {
        if (strlen($name)  30) {
            throw new InvalidArgumentException(
                "The user name is invalid.");
        }
        $this->name = htmlspecialchars(trim($name), ENT_QUOTES);
        return $this;
    }

    public function getName() {
        return $this->name;
    }

    public function setEmail($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "The user email is invalid.");
        }
        $this->email = $email;
        return $this;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setRole($role) {
        if ($role !== self::ADMINISTRATOR_ROLE
            && $role !== self::GUEST_ROLE) {
            throw new InvalidArgumentException(
                "The user role is invalid.");
        }
        $this->role = $role;
        return $this;
    }

    public function getRole() {
        return $this->role;
    }
}

Unbox, the batch of tasks performed by UserMapper is quite simple, limited to exposing a few general-purpose finders that are responsible for extracting users from the database and rebuilding the corresponding entity through the createUser() method. Also, if you've delved into some mappers before, and even wrote your own mapping masterpiece, the above is certainly easy to understand. The only subtle detail worth highlighting is probably that the UserCollectionInterface has been placed in the mapping layer, not in the model. I do this on purpose because in this way, the abstraction (protocol) that the user collection depends on is explicitly declared and owned by a higher level UserMapper, which is consistent with the guide promoted by the principle of dependency inversion. With the mapper already set up, we can use it directly out of the box and extract some user objects from the storage to allow the model to hydrate immediately. While this does seem to be the right path at first glance, we are actually tainting application logic with infrastructure unnecessary because mappers are actually part of the infrastructure. What if in the future, it is necessary to query user entities based on more granular, domain-specific conditions (not just common conditions exposed by the mapper's finder)? In this case, it does need to place an additional layer above the mapping layer, which not only provides a higher level of data access, but also carries the query logic block through a single point. Ultimately, that's the huge amount of benefits we expect from warehousing.

Implement user warehousing

In a production environment, warehousing can implement almost everything one can think of on its surface in order to expose the illusion of the memory set of aggregate roots to the model. However, in this case, we cannot so naively expect to enjoy this expensive luxury for free, because the warehouse we are going to build will be a rather artificial structure responsible for extracting users from the database:

<?php namespace ModelCollection;
use MapperUserCollectionInterface,
    ModelUserInterface;

class UserCollection implements UserCollectionInterface
{
    protected $users = array();

    public function add(UserInterface $user) {
        $this->offsetSet($user);
    }

    public function remove(UserInterface $user) {
        $this->offsetUnset($user);
    }

    public function get($key) {
        return $this->offsetGet($key);
    }

    public function exists($key) {
        return $this->offsetExists($key);
    }

    public function clear() {
        $this->users = array();
    }

    public function toArray() {
        return $this->users;
    }

    public function count() {
        return count($this->users);
    }

    public function offsetSet($key, $value) {
        if (!$value instanceof UserInterface) {
            throw new InvalidArgumentException(
                "Could not add the user to the collection.");
        }
        if (!isset($key)) {
            $this->users[] = $value;
        }
        else {
            $this->users[$key] = $value;
        }
    }

    public function offsetUnset($key) {
        if ($key instanceof UserInterface) {
            $this->users = array_filter($this->users,
                function ($v) use ($key) {
                    return $v !== $key;
                });
        }
        else if (isset($this->users[$key])) {
            unset($this->users[$key]);
        }
    }

    public function offsetGet($key) {
        if (isset($this->users[$key])) {
            return $this->users[$key];
        }
    }

    public function offsetExists($key) {
        return ($key instanceof UserInterface)
            ? array_search($key, $this->users)
            : isset($this->users[$key]);
    }

    public function getIterator() {
        return new ArrayIterator($this->users);
    }
}
<?php namespace Mapper;
use ModelUserInterface;

interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate 
{
    public function add(UserInterface $user);
    public function remove(UserInterface $user);
    public function get($key);
    public function exists($key);
    public function clear();
    public function toArray();
}
<?php namespace Mapper;
use ModelRepositoryUserMapperInterface,  
    ModelUser;

class UserMapper implements UserMapperInterface
{    
    protected $entityTable = "users";
    protected $collection;

    public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) {
        $this->adapter = $adapter;
        $this->collection = $collection;
    }

    public function fetchById($id) {
        $this->adapter->select($this->entityTable,
            array("id" => $id));
        if (!$row = $this->adapter->fetch()) {
            return null;
        }
        return $this->createUser($row);
    }

    public function fetchAll(array $conditions = array()) {
        $this->adapter->select($this->entityTable, $conditions);
        $rows = $this->adapter->fetchAll();
        return $this->createUserCollection($rows);

    }

    protected function createUser(array $row) {
        $user = new User($row["name"], $row["email"],
            $row["role"]);
        $user->setId($row["id"]);
        return $user;
    }

    protected function createUserCollection(array $rows) {
        $this->collection->clear();
        if ($rows) {
            foreach ($rows as $row) {
                $this->collection[] = $this->createUser($row);
            }
        }
        return $this->collection;
    }
}

Although on top of a rather lightweight structure, the implementation of UserRepository is very intuitive because its API allows it to extract a collection of user objects from a storage that conforms to fine predicates closely related to the model language. In addition, in its current state, the repository only exposes some simple finders to the client code, which in turn uses the functionality of the data mapper to access the storage. In a more realistic environment, warehousing should also be able to persist aggregate roots. If you want to add the insert() method or other similar method to UserRepository, feel free to do so. In either case, an effective way to capture the actual advantages of using warehousing through examples is:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}

As mentioned earlier, warehousing effectively interchanges business terms with client code (the so-called "universal language" created by Eric Evans in his book "Domain Driven Design") rather than lower-level technical terms. Unlike the ambiguity present in the data mapper finder, on the other hand, the method of warehousing describes itself with "name", "email", and "role", which are undoubtedly part of the properties of modeling user entities. This finer, higher-level data abstraction, and a complete set of features required when encapsulating query logic in complex systems are undoubtedly one of the most compelling reasons why warehousing is more attractive in multi-layer designs. Of course, most of the time, there is an implicit trade-off between the hassle of getting these benefits upfront and deploying additional layers of abstraction (which can be too bloated in simpler applications).

Conclusion

As one of the core concepts in domain-driven design, warehousing can be found in applications written in several other languages ​​such as Java and C#, to name just a few. However, in PHP, they are still relatively unknown, just taking the first step in the world. Still, there are some trusted frameworks like FLOW3 and of course Doctrine 2.x that can help you adopt the DDD paradigm. As with any existing development approach, you don't have to use repositories in your application, or even needlessly smash them with the concept heap behind DDD. Just apply common sense and choose them only if you think they are suitable for your needs. It's that simple. Pictures from Chance Agrella / Freerangestock.com

FAQs on Handling Aggregated Root Collections (FAQ)

What is the aggregate root in domain-driven design?

In Domain Driven Design (DDD), an aggregate root is a collection of associated objects that are considered as a unit. These objects are bound together by root entities (also known as aggregate roots). The aggregation root maintains consistency of changes being made to the aggregation by forbidding external objects to hold references to their members.

How is the difference between an aggregate root and a normal entity?

The main difference between aggregate roots and ordinary entities is their responsibilities. Normal entities encapsulate behavior and state, while the aggregation root also ensures the integrity of the entire aggregation by controlling access to its members. It is the only member in an aggregation that allows external objects to hold references to them.

How to identify aggregate roots in my domain model?

Identifying the aggregation root requires a deep understanding of the business area. It is usually a high-level entity with a global identity and encapsulates other entities and value objects. For example, in the e-commerce world, an order can be an aggregate root that encapsulates line items and delivery information.

How to deal with a collection of aggregated roots?

Processing a collection of aggregated roots can be challenging. It is important to remember that each aggregate root is a consistency boundary, so changes to one aggregate root should not affect other aggregate roots. Therefore, when processing a collection, it is usually better to load and persist each aggregate root separately for consistency.

Can an aggregate root refer to another aggregate root?

Yes, an aggregate root can refer to another aggregate root, but it should be referenced only by identification. This means it should not hold a direct reference to another aggregate root object, but its ID. This helps maintain the consistency boundaries for each aggregate root.

How does an aggregate root relate to warehousing in DDD?

In DDD, warehousing provides methods for retrieving and storing aggregate roots. It abstracts the underlying storage mechanism, allowing the domain model to ignore the details of data persistence. Each aggregate root usually has its own storage.

What is the role of aggregation roots in executing business rules?

Aggregation roots play a crucial role in executing business rules. It ensures that all changes to the aggregation put it in a valid state. This means that any business rules spanning multiple entities or value objects should be enforced by the aggregate root.

How does aggregation root help reduce complexity in domain models?

Aggregation roots help reduce complexity in domain models by acting as consistency boundaries and controlling access to their members. It simplifies the model by providing a single point of interaction for each aggregation, making it easier to understand the system.

Can the aggregate root be part of multiple aggregates?

No, the aggregate root should not be part of multiple aggregates. This will violate the consistency boundaries of aggregates and may lead to inconsistencies in the domain model.

How to deal with the concurrency problem of aggregate roots?

Various strategies can be used to deal with concurrency problems at aggregate roots, such as optimistic locks or pessimistic locks. The choice of a policy depends on the specific requirements of the application and the nature of the concurrency problems you are facing.

This revised output maintains the original image formatting and location, paraphrases the text to avoid plagiarism, and keeps the core meaning intact. Remember to always cite your sources appropriately.

The above is the detailed content of Handling Collections of Aggregate Roots – the Repository Pattern. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
How can you protect against Cross-Site Scripting (XSS) attacks related to sessions?How can you protect against Cross-Site Scripting (XSS) attacks related to sessions?Apr 23, 2025 am 12:16 AM

To protect the application from session-related XSS attacks, the following measures are required: 1. Set the HttpOnly and Secure flags to protect the session cookies. 2. Export codes for all user inputs. 3. Implement content security policy (CSP) to limit script sources. Through these policies, session-related XSS attacks can be effectively protected and user data can be ensured.

How can you optimize PHP session performance?How can you optimize PHP session performance?Apr 23, 2025 am 12:13 AM

Methods to optimize PHP session performance include: 1. Delay session start, 2. Use database to store sessions, 3. Compress session data, 4. Manage session life cycle, and 5. Implement session sharing. These strategies can significantly improve the efficiency of applications in high concurrency environments.

What is the session.gc_maxlifetime configuration setting?What is the session.gc_maxlifetime configuration setting?Apr 23, 2025 am 12:10 AM

Thesession.gc_maxlifetimesettinginPHPdeterminesthelifespanofsessiondata,setinseconds.1)It'sconfiguredinphp.iniorviaini_set().2)Abalanceisneededtoavoidperformanceissuesandunexpectedlogouts.3)PHP'sgarbagecollectionisprobabilistic,influencedbygc_probabi

How do you configure the session name in PHP?How do you configure the session name in PHP?Apr 23, 2025 am 12:08 AM

In PHP, you can use the session_name() function to configure the session name. The specific steps are as follows: 1. Use the session_name() function to set the session name, such as session_name("my_session"). 2. After setting the session name, call session_start() to start the session. Configuring session names can avoid session data conflicts between multiple applications and enhance security, but pay attention to the uniqueness, security, length and setting timing of session names.

How often should you regenerate session IDs?How often should you regenerate session IDs?Apr 23, 2025 am 12:03 AM

The session ID should be regenerated regularly at login, before sensitive operations, and every 30 minutes. 1. Regenerate the session ID when logging in to prevent session fixed attacks. 2. Regenerate before sensitive operations to improve safety. 3. Regular regeneration reduces long-term utilization risks, but the user experience needs to be weighed.

How do you set the session cookie parameters in PHP?How do you set the session cookie parameters in PHP?Apr 22, 2025 pm 05:33 PM

Setting session cookie parameters in PHP can be achieved through the session_set_cookie_params() function. 1) Use this function to set parameters, such as expiration time, path, domain name, security flag, etc.; 2) Call session_start() to make the parameters take effect; 3) Dynamically adjust parameters according to needs, such as user login status; 4) Pay attention to setting secure and httponly flags to improve security.

What is the main purpose of using sessions in PHP?What is the main purpose of using sessions in PHP?Apr 22, 2025 pm 05:25 PM

The main purpose of using sessions in PHP is to maintain the status of the user between different pages. 1) The session is started through the session_start() function, creating a unique session ID and storing it in the user cookie. 2) Session data is saved on the server, allowing data to be passed between different requests, such as login status and shopping cart content.

How can you share sessions across subdomains?How can you share sessions across subdomains?Apr 22, 2025 pm 05:21 PM

How to share a session between subdomains? Implemented by setting session cookies for common domain names. 1. Set the domain of the session cookie to .example.com on the server side. 2. Choose the appropriate session storage method, such as memory, database or distributed cache. 3. Pass the session ID through cookies, and the server retrieves and updates the session data based on the ID.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Dreamweaver Mac version

Dreamweaver Mac version

Visual web development tools

SublimeText3 Linux new version

SublimeText3 Linux new version

SublimeText3 Linux latest version

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use