• Jump To … +
    01-IdentifiesAggregate.php 02-DomainEvent.php 03-RecordsEvents.php 04-ProtectsInvariants.php 05-ProtectsMoreInvariants.php 06-IsEventSourced.php 07-EventStore.php 08-AggregateRepository.php index.markdown
  • 06-IsEventSourced.php

  • ¶
    <?php
  • ¶

    So far, we’ve kept our Basket aggregate in memory at all times. Often, we’ll want to persist it, and load it back in memory later for further use. As you’ve learned, our Aggregate is not represented by its state, but by its history of Domain Events. So loading our Aggregate back in history, simply involves reconstituting it from all the events that it has recorded previously. This concept is called Event Sourcing. The events are the single source of elements that make up the Aggregate.

    namespace Buttercup\Protects\Tests;
    
    use Buttercup\Protects\AggregateHistory;
    use Buttercup\Protects\DomainEvent;
    use Buttercup\Protects\DomainEvents;
    use Buttercup\Protects\IsEventSourced;
    use Buttercup\Protects\RecordsEvents;
    use Buttercup\Protects\Tests\Misc\ProductId;
    use Buttercup\Protects\Tests\Misc\When;
    
    $test = function() {
        $basketId = BasketId::generate();
        $basket = BasketV4::pickUp($basketId);
        $productId = new ProductId('TPB1');
        $basket->addProduct($productId, "The Princess Bride");
        $basket->addProduct(new ProductId('TPB2'), "The book");
        $basket->removeProduct($productId);
        $events = $basket->getRecordedEvents();
        $basket->clearRecordedEvents();
  • ¶

    Here we would store the events, and later retrieve them from that store.

        $reconstitutedBasket = BasketV4::reconstituteFrom(
            new AggregateHistory($basketId, (array) $events)
        );
    
        it("should be the same after reconstitution",
            $basket == $reconstitutedBasket
        );
    };
  • ¶

    We declare that Basket IsEventSourced, in other words, that we can rebuild it from it’s events.

    final class BasketV4 implements RecordsEvents, IsEventSourced
    {
        use When;
  • ¶

    The IsEventSourced interface requires us to implement a reconstituteFrom(AggregateHistory) static method. This method is like an alternative constructor. Recall that we had pickUp() earlier, which was the constructor for calling the Basket into life. Reconstitute implies that this Basket already exists conceptually, but that we suspended it temporarily by unloading it from memory. The difference is subtle but important.

        /**
         * @param AggregateHistory $aggregateHistory
         * @return RecordsEvents
         */
        public static function reconstituteFrom(AggregateHistory $aggregateHistory)
        {
  • ¶

    AggregateHistory is a list of chronological DomainEvents for a single Aggregate instance. Let’s start by fetching its identifier.

            $basketId = $aggregateHistory->getAggregateId();
  • ¶

    We instantiate the Basket object. (As you recall, the constructor is private.)

            $basket = new BasketV4(new BasketId($basketId));
    
    
            foreach($aggregateHistory as $event) {
  • ¶

    As you saw earlier, our Aggregate keeps state, to protect invariants. We need to rebuild this state from the events in the AggregateHistory. But there’s a problem: we can’t call methods like pickUp(), addProduct(), and removeProduct(), because these would call recordThat(). That would cause the events to be recorded a second time.

  • ¶

    The trick is to separate the logic that applies events to the state. We’ll call a new private method:

                $basket->when($event);
            }
  • ¶

    Finally we return the newly reconstituted Basket.

            return $basket;
        }
  • ¶

    Inside each whenEventName() method, we manipulate the state. The first one is not very interesting.

        private function whenBasketWasPickedUp(BasketWasPickedUp $event)
        {
            $this->productCount = 0;
            $this->products = [];
        }
    
        private function whenProductWasAddedToBasket(ProductWasAddedToBasket $event)
        {
  • ¶

    Remember that all of this code used to be in addProduct()

            $productId = $event->getProductId();
            if(!$this->productIsInBasket($productId)) {
                $this->products[(string) $productId] = 0;
            }
    
            ++$this->products[(string) $productId];
            ++$this->productCount;
        }
    
        private function whenProductWasRemovedFromBasket(ProductWasRemovedFromBasket $event)
        {
            --$this->products[(string) $event->getProductId()];
            --$this->productCount;
        }
    
        public static function pickUp(BasketId $basketId)
        {
            $basket = new BasketV4($basketId);
            $basket->recordThat(new BasketWasPickedUp($basketId));
  • ¶

    We moved the code that was on this line, to the whenBasketWasPickedUp() method.

            return $basket;
        }
        public function addProduct(ProductId $productId, $name)
        {
            $this->guardProductLimit();
            $this->recordThat(
                new ProductWasAddedToBasket($this->basketId, $productId, $name)
            );
  • ¶

    The code that used to be here, is now in `whenProductWasAddedToBasket().

        }
    
        public function removeProduct(ProductId $productId)
        {
            if(! $this->productIsInBasket($productId)) {
                return;
            }
    
            $this->recordThat(
                new ProductWasRemovedFromBasket($this->basketId, $productId)
            );
  • ¶

    And this code moved to whenProductWasRemovedFromBasket().

        }
    
        private function recordThat(DomainEvent $domainEvent)
        {
            $this->latestRecordedEvents[] = $domainEvent;
  • ¶

    Finally, we make sure that newly recorded events are still being applied to the state. when($domainEvent) delegates to whenDomainEvent($domainEvent)

            $this->when($domainEvent);
        }
  • ¶

    no changes here

        private $products;
        private $productCount;
        private $basketId;
        private $latestRecordedEvents = [];
        private function productIsInBasket(ProductId $productId) { return array_key_exists((string) $productId, $this->products) && $this->products[(string)$productId] > 0; }
        private function guardProductLimit() { if ($this->productCount >= 3) { throw new BasketLimitReached; } }
        private function __construct(BasketId $basketId) { $this->basketId = $basketId; }
        public function getRecordedEvents() { return new DomainEvents($this->latestRecordedEvents); }
        public function clearRecordedEvents() { $this->latestRecordedEvents = []; }
    
    }
    
    $test();