Dividing responsibilities - Part 2

Posted on by Matthias Noback

This is another excerpt from my latest book, which is currently part of Manning's Early Access Program. Take 37% off Object Design Style Guide by entering fccnoback into the discount code box at checkout at manning.com.

Make sure to read part 1 first.

Create read models directly from their data source

Instead of creating a StockReport model from PurchaseOrderForStock objects, we could go directly to the source of the data, that is, the database where the application stores its purchase orders. If this is a relational database, there might be a table called purchase_orders, with columns for purchase_order_id, product_id, ordered_quantity, and was_received. If that's the case, then StockReportRepository wouldn't have to load any other object before it could build a StockReport object; it could make a single SQL query and use it to create the StockReport, as shown in Listing 11).

Listing 11. StockReportSqlRepository creates a stock report using plain SQL.

final class StockReportSqlRepository implements StockReportRepository
{
    public function getStockReport(): StockReport
    {
        result = this.connection.execute(
            'SELECT ' .
            ' product_id, ' .
            ' SUM(ordered_quantity) as quantity_in_stock ' .
            'FROM purchase_orders ' .
            'WHERE was_received = 1 ' .
            'GROUP BY product_id'
        );

        data = result.fetchAll();

        return new StockReport(data);
    }
}

Creating read models directly from the write model's data source is usually pretty efficient in terms of runtime performance. It's also an efficient solution in terms of development and maintenance costs. This solution will be less efficient if the write model changes often, or if the raw data can't easily be used as-is, because it needs to be interpreted first.

Build read models from domain events

One disadvantage of creating the StockReport read model directly from the write model's data is that the application will make the calculations again and again, every time the user requests a stock report. Although the SQL query won't take too long to execute (until the table grows very large), in some cases it'll be necessary to use another approach for creating read models.

First, let's take another look at the result of the SQL query we used in the previous example (Table 1).

Table 1. The result of the SQL query for generating a stock report.

product_id quantity_in_stock

123

10

124

5

What would be another way in which we can come up with the numbers in the second column, that wouldn't involve looking up all the records in the purchase_orders table and summing their ordered_quantity values?

What if we could sit next to the user with a piece of paper, and whenever they mark a purchase order as "received", we'd write down the ID of the product and how many items of it were received. The resulting list would look like Table 2:

Table 2. The result if we write down every received product.

product_id received

123

2

124

4

124

1

123

8

Now, instead of having multiple rows for the same product, we could also look up the row with the product that was just received, and add the quantity we received to the number that's already in the received column, as is done in Table 3.

Table 3. The result of combining received quantities per product.

product_id received

123

2 + 8

124

4 + 1

Doing the calculations, this amounts to the exact same result as when we used the SUM query to find out how much of a product we have in stock.

Instead of sitting next to the user with a piece of paper, we should listen in on our PurchaseOrder entity to find out when a user marks it as received. We can do this by recording and dispatching Domain events; a technique we already saw in a previous chapter. First, we need to let PurchaseOrder record a domain event, indicating that the ordered items were received. This is shown in Listing 12.

Listing 12. PurchaseOrder entities record PurchaseOrderReceived events.

final class PurchaseOrderReceived                         
{
    private int purchaseOrderId;
    private int productId;
    private int receivedQuantity;

    public function __construct(
        int purchaseOrderId,
        int productId,
        int receivedQuantity
    ) {
        this.purchaseOrderId = purchaseOrderId;
        this.productId = productId;
        this.receivedQuantity = receivedQuantity;
    }

    public function productId(): int
    {
        return this.productId;
    }

    public function receivedQuantity(): int
    {
        return this.receivedQuantity;
    }
}

final class PurchaseOrder
{
    private array events = [];

    // ...

    public function markAsReceived(): void
    {
        this.wasReceived = true;

        this.events[] = new PurchaseOrderReceived(       
            this.purchaseOrderId,
            this.productId,
            this.orderedQuantity
        );
    }

    public function recordedEvents(): array
    {
        return this.events;
    }
}
  • This is the new domain event.

  • We record the domain event inside PurchaseOrder.

Calling markAsReceived() will from now on add a PurchaseOrderReceived event object to the list of internally recorded events. These events can be taken out and handed over to an event dispatcher, for example in the ReceiveItems service (Listing 13).

Listing 13. ReceiveItems dispatches any recorded domain event.

final class ReceiveItems
{
    // ...

    public function receiveItems(int purchaseOrderId): void
    {
        // ...

        this.repository.save(purchaseOrder);

        this.eventDispatcher.dispatchAll(
            purchaseOrder.recordedEvents()
        );
    }
}

An event listener that has been registered for this particular event can take the relevant data from the event object, and update its own private list of products and quantities in stock. For instance, it could build up the stock report, by maintaining its own table stock_report with rows for every product. It has to process the incoming PurchaseOrderReceived events and create new rows or updating existing ones in this stock_report table, as shown in Listing 14).

Listing 14. UpdateStockReport uses the event to update the stock_report table.

final class UpdateStockReport
{
    public function whenPurchaseOrderReceived(
        PurchaseOrderReceived event
    ): void {
        this.connection.transactional(function () {
            /*
             * Find out if we have an existing row.
             */
            existingRow = this.connection                        
                .prepare(
                    'SELECT quantity_in_stock ' .
                    'FROM stock_report ' .
                    'WHERE product_id = :productId FOR UPDATE'
                )
                .bindValue('productId', event.productId())
                .execute()
                .fetch();

            if (!existingRow) {  
                /*
                 * If no row exists for this product, create a new 
                 * one and set an initial value for quantity-in-stock.
                 */                         
                this.connection
                    .prepare(
                        'INSERT INTO stock_report ' .
                        ' (product_id, quantity_in_stock) ' .
                        'VALUES (:productId, :quantityInStock)'
                    )
                    .bindValue(
                        'productId',
                        event.productId()
                    )
                    .bindValue(
                        'quantityInStock',
                        event.quantityReceived()
                    )
                    .execute();
            } else {                    
                /*
                 * Otherwise, update the existing one; increase 
                 * the quantity-in-stock.
                 */                            
                this.connection
                    .prepare(
                        'UPDATE stock_report ' .
                        'SET quantity_in_stock = ' .
                        ' quantity_in_stock + :quantityReceived ' .
                        'WHERE product_id = :productId'
                    )
                    .bindValue(
                        'productId',
                        event.productId()
                    )
                    .bindValue(
                        'quantityReceived',
                        event.quantityReceived()
                    )
                    .execute();
            }
        });
    }
}

Figure 1. This diagram shows how the ReceiveItems service makes a change to the PurchaseOrder write model, after which it allows other services like UpdateStockReport to listen in on those changes by dispatching domain events to the EventDispatcher.

Entangled strings

Once we have a separate data source for the stock report, we can make StockReportSqlRepository even simpler, because all the information is already in the stock_reports table (see Listing 15).

Listing 15. The query in StockReportSqlRepository is now much simpler.

final class StockReportSqlRepository implements StockReportRepository
{
    public function getStockReport(): StockReport
    {
        result = this.connection.execute(
            'SELECT * FROM stock_report'
        );

        data = result.fetchAll();

        return new StockReport(data);
    }
}

This kind of simplification may offer you a way to make your read model queries more efficient. However, in terms of development and maintenance costs, using domain events to build up read models is more expensive. As you can see by looking at the examples in this section, there are more moving parts involved. If something changes about a domain event, it will be more work to adapt the other parts that depend on it. If one of the event listeners fails, you need to be able to fix the error, and run it again, which requires some extra effort in terms of tooling and operations as well.

Things will be even more complex if besides using events for building up read models, you also use events for reconstructing write models. This technique is called Event sourcing and fits very well with the idea of separating write models from read models. However, as demonstrated in this chapter, you don't need to apply event sourcing if you're only looking for a better way to divide responsibilities between objects. Using any of the techniques described here, you can already provide clients that only want to retrieve information from an entity with a separate read model.

Summary

  • For your domain objects, make sure to separate write models from read models. Clients that are only interested in an entity because they need data from it should use a dedicated object, instead of the same entity that exposes methods for changing its state.
  • Read models can be created directly from the write model. A more efficient way would be to create it from the data source used by the write model. If that is impossible, or the read model can't be created in an efficient way, consider using domain events to build up the read model over time.

If you want to learn more about the book, check it out here on liveBook.

PHP object design CQRS Comments

Dividing responsibilities - Part 1

Posted on by Matthias Noback

I'm happy to share with you an excerpt of my latest book, which is currently part of Manning's Early Access Program. Take 37% off Object Design Style Guide by entering fccnoback into the discount code box at checkout at manning.com.

Chapter 7: Dividing responsibilities

We've looked at how objects can be used to retrieve information, or perform tasks. The methods for retrieving information are called query methods, the ones that perform tasks are command methods. Service objects may combine both of these responsibilities. For instance, a repository (like the one in Listing 1) could perform the task of saving an entity to the database, and at the same time it would also be capable of retrieving an entity from the database.

Listing 1. The PurchaseOrderRepository can save and retrieve a PurchaseOrder entity.

interface PurchaseOrderRepository
{
    /**
     * @throws CouldNotSavePurchaseOrder
     */
    public function save(PurchaseOrder purchaseOrder): void;

    /**
     * @throws CouldNotFindPurchaseOrder
     */
    public function getById(int purchaseOrderId): PurchaseOrder;
}

Since saving and retrieving an entity are more or less each other's inverse operations, it's only natural to let one object have both responsibilities. However, in most other cases you will find that performing tasks and retrieving information are better off being divided amongst different objects.

Separate write models from read models

As we saw earlier, there are services, and other objects. Some of these other objects can be characterized as Entities, which model a particular domain concept. In doing so, they contain some relevant data, and offer ways to manipulate that data in valid and meaningful ways. Entities can also expose data, allowing clients to retrieve information from them, whether that is exposed internal data (like the date on which an order was placed), or calculated data (like the total amount of the order).

In practice, it turns out that different clients use entities in different ways. Some clients will want to manipulate an entity's data using its command methods, while others just want to retrieve a piece of information from it using its query methods. Nevertheless, all these clients will share the same object, and potentially have access to all the methods, even when they don't need them, or shouldn't even have access to them.

You should never pass an entity that can be modified to a client that isn't allowed to modify it. Even if the client doesn't modify it today, one day it might, and then it will be hard to find out what happened. That's why the first thing you should do to improve the design of an entity, is separate the Write model from the Read model.

We'll find out how to accomplish this by looking at an example of a PurchaseOrder entity (Listing 2). A purchase order represents the fact that a company buys a product from one of its suppliers. Once the product has been received, it's shelved in the company's warehouse. From that moment on the company has this product in stock. We'll use the same example for the remaining part of this chapter and work out different ways to improve it.

Listing 2. The PurchaseOrder entity.

final class PurchaseOrder
{
    private int purchaseOrderId;
    private int productId;
    private int orderedQuantity;
    private bool wasReceived;

    private function __construct()
    {
    }

    public static function place(                
        int purchaseOrderId,
        int productId,
        int orderedQuantity
    ): PurchaseOrder {
        /*
         * For brevity, we use primitive type values, while in 
         * practice, the use of value objects is recommended.
         */

        purchaseOrder = new self();

        purchaseOrder.productId = productId;
        purchaseOrder.orderedQuantity = orderedQuantity;
        purchaseOrder.wasReceived = false;

        return purchaseOrder;
    }

    public function markAsReceived(): void
    {
        this.wasReceived = true;
    }

    public function purchaseOrderId(): int
    {
        return this.purchaseOrderId;
    }

    public function productId(): int
    {
        return this.productId;
    }

    public function orderedQuantity(): int
    {
        return this.orderedQuantity;
    }

    public function wasReceived(): bool
    {
        return this.wasReceived;
    }
}

In the current implementation, the PurchaseOrder entity exposes methods for creating and manipulating the entity (place() and markAsReceived(), as well as for retrieving information from it (productId(), orderedQuantity() and wasReceived()). Now take a look at how different clients use this entity. First, the ReceiveItems service (Listing 3), which will be called from a controller, passing in a raw purchase order ID.

Listing 3. The ReceiveItems service.

final class ReceiveItems
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function receiveItems(int purchaseOrderId): void
    {
        purchaseOrder = this.repository.getById(purchaseOrderId);

        purchaseOrder.markAsReceived();

        this.repository.save(purchaseOrder);
    }
}

Note that this service doesn't use any of the getters on PurchaseOrder. It's only interested in changing the state of the entity. Next, let's take a look at a controller which renders a JSON-encoded data structure detailing how much of a product the company has in stock (Listing 4).

Listing 4. The StockReportController class.

final class StockReportController
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        allPurchaseOrders = this.repository.findAll();

        stockReport = [];

        foreach (allPurchaseOrders as purchaseOrder) {
            /*
             * We didn't receive the items yet, so we shouldn't add 
             * them to the quantity-in-stock. */
            if (!purchaseOrder.wasReceived()) {                
                continue;
            }

            if (!isset(stockReport[purchaseOrder.productId()] )) {
                /*
                 * We didn't see this product before...
                 */ 
                stockReport[purchaseOrder.productId()] = 0;
            }

            /*
             * Add the ordered (and received) quantity to the
             * quantity-in-stock.
             */
            stockReport[purchaseOrder.productId()]
                +== purchaseOrder.orderedQuantity;
        }

        return new JsonResponse(stockReport);
    }
}

This controller doesn't make any change to a PurchaseOrder. It just needs a bit of information from all of them. In other words, it isn't interested in the write part of the entity, only in the read part. Besides the fact that it is undesirable to expose more behavior to a client than it needs, it isn't very efficient to loop over all purchase orders of all times, to find out how much of a product the company has in stock.

The solution is to divide the entity's responsibilities. First, we create a new object that can be used for retrieving information about a purchase order. Let's call it PurchaseOrderForStockReport (Listing 5).

Listing 5. The PurchaseOrderForStockReport class.

final class PurchaseOrderForStockReport
{
    private int productId;
    private int orderedQuantity;
    private bool wasReceived;

    public function __construct(
        int productId,
        int orderedQuantity,
        bool wasReceived
    ) {
        this.productId = productId;
        this.orderedQuantity = orderedQuantity;
        this.wasReceived = wasReceived;
    }

    public function productId(): ProductId
    {
        return this.productId;
    }

    public function orderedQuantity(): int
    {
        return this.orderedQuantity;
    }

    public function wasReceived(): bool
    {
        return this.wasReceived;
    }
}

This new PurchaseOrderForStockReport object can be used inside the controller as soon as there is a repository which can provide it. A quick and dirty solution would be to let PurchaseOrder return an instance of PurchaseOrderForStockReport, based on its internal data, like in Listing 6).

Listing 6. A quick solution: PurchaseOrder generates the report.

final class PurchaseOrder
{
    private int purchaseOrderId
    private int productId;
    private int orderedQuantity;
    private bool wasReceived;

    // ...

    public function forStockReport(): PurchaseOrderForStockReport
    {
        return new PurchaseOrderForStockReport(
            this.productId,
            this.orderedQuantity,
            this.wasReceived
        );
    }
}

final class StockReportController
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        /*
         * For now, we still load `PurchaseOrder` entities.
         */
        allPurchaseOrders = this.repository.findAll();           

        /*
         * But we immediately convert them to 
         * `PurchaseOrderForStockReport` instances.
         */
        forStockReport = array_map(                                 
            function (PurchaseOrder purchaseOrder) {
                return purchaseOrder.forStockReport();
            },
            allPurchaseOrders
        );

        // ...
    }
}

We can now remove pretty much all of the query methods (productId(), orderedQuantity(), wasReceived()) from the original PurchaseOrder entity (see Listing 7). This makes it a proper write model; it isn't used by clients who just want information from it anymore.

Listing 7. PurchaseOrder with its getters removed.

final class PurchaseOrder
{
    private int purchaseOrderId
    private int productId;
    private int orderedQuantity;
    private bool wasReceived;

    private function __construct()
    {
    }

    public static function place(
        int purchaseOrderId,
        int productId,
        int orderedQuantity
    ): PurchaseOrder {
        purchaseOrder = new self();

        purchaseOrder.productId = productId;
        purchaseOrder.orderedQuantity = orderedQuantity;

        return purchaseOrder;
    }

    public function markAsReceived(): void
    {
        this.wasReceived = true;
    }
}

Removing these query methods won't do any harm to the existing clients of PurchaseOrder that use this object as a write model, like the ReceiveItems service we saw earlier (Listing 8).

Listing 8. Existing clients use PurchaseOrder as a write model.

final class ReceiveItems
{
    // ...

    public function receiveItems(int purchaseOrderId): void
    {
        /*
         * This service doesn't use any query method of 
         * `PurchaseOrder`.
         */
        purchaseOrder = this.repository.getById(            
            PurchaseOrderId.fromInt(purchaseOrderId)
        );

        purchaseOrder.markAsReceived();

        this.repository.save(purchaseOrder);
    }
}

Some clients use the entity as a write model, but still need to retrieve some information from it. For instance in order to make decisions, perform extra validations, etc. Don't feel blocked to add more query methods in these cases; query methods aren't by any means forbidden. The point of this chapter is that clients that solely use an entity to retrieve information from it, should use a dedicated read model instead of a write model.

Create read models that are specific for their use cases

In the previous section, we decided to split the PurchaseOrder entity into a write and a read model. The write model still carries the old name, but we called the read model PurchaseOrderForStockReport. The extra qualification ForStockReport indicates that this object now serves a specific purpose. The object will be suitable for use in a very specific context, namely the context where we arrange the data in such a way that we can produce a useful stock report for the user. The proposed solution isn't optimal yet, because the controller still needs to load all the PurchaseOrder entities, then convert them to PurchaseOrderForStockReport instances by calling forStockReport() on them (see Listing 9). This means that the client still has access to that write model, even though our initial goal was to prevent that from happening:

Listing 9. Creating a stock report still relies on the write model.

public function execute(Request request): Response
{
    /*
     * We still rely on `PurchaseOrder` instances here.
     */
    allPurchaseOrders = this.repository.findAll();     

    forStockReport = array_map(
        function (PurchaseOrder purchaseOrder) {
            return purchaseOrder.forStockReport();
        },
        allPurchaseOrders
    );

    // ...
}

There is another aspect of the design that isn't quite right: even though we now have PurchaseOrderForStockReport objects, we still need to loop over them and build up yet another data structure, before we can present the data to the user. What if we had an object whose structure completely matched the way we intend to use it? Concerning the name of this object, there's already a hint in the name of the read model (ForStockReport). So let's call this new object StockReport, and assume it already exists. The controller would become much simpler now, as shown in Listing 10).

Listing 10. StockReportController can retrieve the stock report directly.

final class StockReportController
{
    private StockReportRepository repository;

    public function __construct(StockReportRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        stockReport = this.repository.getStockReport();

        /*
         * `asArray()` is expected to return an array like we the one 
         * we created manually before.
         */
        return new JsonResponse(stockReport.asArray());    
    }
}

Besides StockReport we may create any number of read models which correspond to each of the application's specific use cases. For instance, we could create a read model that's used for listing purchase orders only. It would expose just the ID and the date on which it was created. We could then have a separate read model that provides all the details needed to render a form that allows the user to update some of its information; and so on.

Behind the scenes, the StockReportRepository could still create the StockReport object based on PurchaseOrderForStock objects provided by the write model entities. But there are much better and more efficient alternatives to do it. We'll cover some of them in the following sections.

Continue reading part 2

If you want to learn more about the book, check it out here on liveBook. Stay tuned for part two.

PHP object design CQRS Comments

You may not need a query bus

Posted on by Matthias Noback

"Can you make a query bus with SimpleBus?" The question has been asked many times. I've always said no. Basically, because I didn't build in the option to return anything from a command handler. So a handler could never become a query handler, since a query will of course have to return something.

I've always thought that the demand for a query bus is just a sign of the need for symmetry. If you have command and query methods, then why not have command and query buses too? A desire for symmetry isn't a bad thing per se. Symmetry is attractive because it feels natural, and that feeling can serve as design feedback. For instance, you can use lack of symmetry to find out what aspect of a design is still missing, or to find alternative solutions.

Nonetheless, I think that we may actually not need a query bus at all.

The return type of a query bus is "mixed"

A command or query bus interface will look something like this:

interface Bus
{
    /**
     * @return mixed
     */
    public function handle(object $message);
}

A sample query and query handler would look like this:

final class GetExchangeRate
{
    // ...
}

final class GetExchangeRateHandler
{
    public function handle(GetExchangeRate $query): ExchangeRate
    {
        // ...
    }
}

When you pass an instance of GetExchangeRate to Bus::handle() it will eventually call GetExchangeRateHandler::handle() and return the value. But Bus::handle() has an unknown return type, which we would call "mixed". Now, you know that the return type is going to be ExchangeRate, but a compiler wouldn't know. Nor does your IDE.

// What type of value is `$result`?
$result = $bus->handle(new GetExchangeRate(/* ... */));

This situation reminds me of the problem of a service locator (or container, used as locator) that offers a generic method for retrieving services:

interface Container
{
    public function get(string $id): object;
}

You don't know what you're going to get until you get it. Still, you rely on it to return the thing you were expecting to get.

Implicit dependencies

This brings me to the next objection: if you know which service is going to answer your query, and what type the answer is going to be, why would you depend on another service?

If I see a service that needs an exchange rate, I would expect this service to have a dependency called ExchangeRateRepository, or ExchangeRateProvider, or anything else, but not a QueryBus, or even a Bus. I like to see what the actual dependencies of a service are.

final class CreateInvoice
{
    // What does this service need a `Bus` for?!

    public function __construct(Bus $bus)
    {
        // ...
    }
}

In fact, this argument is also valid for the command bus itself; we may not even need it, since there is one command handler for a given command. Why not call the handler directly? For the automatic database transaction wrapping the handler? I actually prefer dealing with the transaction in the repository implementation only. Automatic event dispatching? I do that manually in my application service.

Really, the main thing that I hope the command bus brought us, is a tendency to model use cases as application services, which are independent of an application's infrastructure. And I introduced the void return type for command handlers to prevent write model entities from ending up in the views. However, I've become much less dogmatic over the years: I happily return IDs of new entities from my application services these days.

No need for middleware

Actually, the idea of the command bus having middleware that could do things before or after executing the command handler, was pretty neat. Dealing with database transactions, dispatching events, logging, security checks, etc. However, middlewares also tend to hide important facts from the casual reader. One type of middleware is quite powerful nonetheless: one that serializes an incoming message and adds it to a queue for asynchronous processing. This works particularly well with commands, because they don't return anything anyway.

I'm not sure if any of these middleware solutions will be interesting for a query bus though. Queries shouldn't need to run within a database transaction. They won't dispatch events, they won't need logging, etc. In particular, they shouldn't need to be queued. That would not make a timely answer to your query likely.

A query handler that doesn't need middlewares, doesn't need a bus either. The only thing the bus can still do in that case is directly forward the query to the right handler. And, as I mentioned, if there's just one handler, and you wrote it, why not make it an explicit dependency and call it directly?

Suggested refactoring: Replace Query Bus with Service Dependency

It won't be a surprise that my advice is to replace usages of a query bus with a real service dependency. This gives you the following benefits:

  • Service dependencies will be explicit
  • Return types will be specific

The refactoring in case of the GetExchangeRate case looks as follows:

// Before:

final class GetExchangeRate
{
    public function __construct(Currency $from, Currency $to, Date $date)
    {
         // ...
    }
}

final class GetExchangeRateHandler
{
    public function handle(GetExchangeRate $query): ExchangeRate
    {
        // ...
    }
}

 // After:

final class ExchangeRateProvider
{
    public function getExchangeRateFor(Currency $from, Currency $to, Date $date): ExchangeRate
    {
        // ...
    }
}

Also, every service that used to depend on the bus for answering their GetExchangeRate query, would now depend on ExchangeRateProvider and it should get this dependency injected as a constructor argument.

final class CreateInvoice
{
    public function __construct(ExchangeRateProvider $exchangeRateProvider)
    {
        // ...
    }
}

Optional refactoring: Introduce Parameter Object

As you may have noticed, the constructor arguments of the query object are now the method arguments of getExchangeRateFor(). This means that we've applied the opposite of the Introduce Parameter Object refactoring. I find that in some cases it still pays to keep the query object. In particular if it's something that represents a complicated query, with multiple options for filtering, searching, limiting, etc. In that case, sometimes a builder can give quite elegant results:

final class Invoices
{
    public static function all(): self
    {
        // ...
    }

    public function createdAfter(Date $date): self
    {
        // ...
    }

    public function thatHaveBeenPaid(): self
    {
        // ...
    }
}

$this->invoiceRepository->find(
    Invoices::all()
        ->createdAfter(Date::fromString('2019-06-30'),
        ->thatHaveBeenPaid()
);
PHP Command bus Query bus Comments