Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract aggregate root into traits to make it easier to avoid domain extending infrastructure #62

Merged
merged 5 commits into from
May 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,29 @@ then you can also just change the `aggregate_translator` key in your config to p
and register the `UserAggregateTranslator` in your container.

see also: http://www.sasaprolic.com/2016/02/inheritance-with-aggregate-roots-in.html

## Alternative to AggregateRoot inheritance

Abstract `Prooph\EventSourcing\AggregateRoot` class provides a solid basis for
your aggregate roots, however, it is not mandatory. Two traits,
`Prooph\EventSourcing\Aggregate\EventProducerTrait` and
`Prooph\EventSourcing\Aggregate\EventSourcedTrait`, together provide exactly
the same functionality.

- `EventProducerTrait` is responsible for event producing side of Event
Sourcing and might be used independently of `EventSourcedTrait` when you are
not ready to start with full event sourcing but still want to get the benefit
of design validation and audit trail provided by Event Sourcing. Forcing all
changes to be applied internally via event sourcing will ensure events data
consistency with the state and will make it easier to switch to full event
sourcing later on.

- `EventSourcedTrait` is responsible for restoring state from event stream, it
should be used together with `EventProducerTrait` as normally you will not be
applying events not produced by that aggregate root.

Default aggregate translator uses `AggregateRootDecorator` to access protected
methods of `Prooph\EventSourcing\AggregateRoot` descendants, you will need to
switch to
`Prooph\EventSourcing\EventStoreIntegration\ClosureAggregateTranslator` for
aggregate roots using traits.
65 changes: 65 additions & 0 deletions src/Aggregate/EventProducerTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
/**
* This file is part of the prooph/event-sourcing.
* (c) 2014-2017 prooph software GmbH <contact@prooph.de>
* (c) 2015-2017 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

use Prooph\EventSourcing\AggregateChanged;

trait EventProducerTrait
{
/**
* Current version
*
* @var int
*/
protected $version = 0;

/**
* List of events that are not committed to the EventStore
*
* @var AggregateChanged[]
*/
protected $recordedEvents = [];

/**
* Get pending events and reset stack
*
* @return AggregateChanged[]
*/
protected function popRecordedEvents(): array
{
$pendingEvents = $this->recordedEvents;

$this->recordedEvents = [];

return $pendingEvents;
}

/**
* Record an aggregate changed event
*/
protected function recordThat(AggregateChanged $event): void
{
$this->version += 1;

$this->recordedEvents[] = $event->withVersion($this->version);

$this->apply($event);
}

abstract protected function aggregateId(): string;

/**
* Apply given event
*/
abstract protected function apply(AggregateChanged $event): void;
}
60 changes: 60 additions & 0 deletions src/Aggregate/EventSourcedTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
/**
* This file is part of the prooph/event-sourcing.
* (c) 2014-2017 prooph software GmbH <contact@prooph.de>
* (c) 2015-2017 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

use Iterator;
use Prooph\EventSourcing\AggregateChanged;
use RuntimeException;

trait EventSourcedTrait
{
/**
* Current version
*
* @var int
*/
protected $version = 0;

/**
* @throws RuntimeException
*/
protected static function reconstituteFromHistory(Iterator $historyEvents): self
{
$instance = new static();
$instance->replay($historyEvents);

return $instance;
}

/**
* Replay past events
*
* @throws RuntimeException
*/
protected function replay(Iterator $historyEvents): void
{
foreach ($historyEvents as $pastEvent) {
/** @var AggregateChanged $pastEvent */
$this->version = $pastEvent->version();

$this->apply($pastEvent);
}
}

abstract protected function aggregateId(): string;

/**
* Apply given event
*/
abstract protected function apply(AggregateChanged $event): void;
}
78 changes: 4 additions & 74 deletions src/AggregateRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,13 @@

namespace Prooph\EventSourcing;

use Iterator;
use RuntimeException;
use Prooph\EventSourcing\Aggregate\EventProducerTrait;
use Prooph\EventSourcing\Aggregate\EventSourcedTrait;

abstract class AggregateRoot
{
/**
* Current version
*
* @var int
*/
protected $version = 0;

/**
* List of events that are not committed to the EventStore
*
* @var AggregateChanged[]
*/
protected $recordedEvents = [];

/**
* @throws RuntimeException
*/
protected static function reconstituteFromHistory(Iterator $historyEvents): self
{
$instance = new static();
$instance->replay($historyEvents);

return $instance;
}
use EventProducerTrait;
use EventSourcedTrait;

/**
* We do not allow public access to __construct, this way we make sure that an aggregate root can only
Expand All @@ -49,52 +27,4 @@ protected static function reconstituteFromHistory(Iterator $historyEvents): self
protected function __construct()
{
}

abstract protected function aggregateId(): string;

/**
* Get pending events and reset stack
*
* @return AggregateChanged[]
*/
protected function popRecordedEvents(): array
{
$pendingEvents = $this->recordedEvents;

$this->recordedEvents = [];

return $pendingEvents;
}

/**
* Record an aggregate changed event
*/
protected function recordThat(AggregateChanged $event): void
{
$this->version += 1;

$this->recordedEvents[] = $event->withVersion($this->version);

$this->apply($event);
}

/**
* Replay past events
*
* @throws RuntimeException
*/
protected function replay(Iterator $historyEvents): void
{
foreach ($historyEvents as $pastEvent) {
/** @var AggregateChanged $pastEvent */
$this->version = $pastEvent->version();

$this->apply($pastEvent);
}
}

/**
* Apply given event
*/
abstract protected function apply(AggregateChanged $event): void;
}
117 changes: 117 additions & 0 deletions src/EventStoreIntegration/ClosureAggregateTranslator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
/**
* This file is part of the prooph/event-sourcing.
* (c) 2014-2017 prooph software GmbH <contact@prooph.de>
* (c) 2015-2017 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Prooph\EventSourcing\EventStoreIntegration;

use Iterator;
use Prooph\Common\Messaging\Message;
use Prooph\EventSourcing\Aggregate\AggregateTranslator as EventStoreAggregateTranslator;
use Prooph\EventSourcing\Aggregate\AggregateType;
use RuntimeException;

final class ClosureAggregateTranslator implements EventStoreAggregateTranslator
{
protected $aggregateIdExtractor;
protected $aggregateReconstructor;
protected $pendingEventsExtractor;
protected $replayStreamEvents;
protected $versionExtractor;

/**
* @param object $eventSourcedAggregateRoot
*
* @return int
*/
public function extractAggregateVersion($eventSourcedAggregateRoot): int
{
if (null === $this->versionExtractor) {
$this->versionExtractor = function (): int {
return $this->version;
};
}

return $this->versionExtractor->call($eventSourcedAggregateRoot);
}

/**
* @param object $anEventSourcedAggregateRoot
*
* @return string
*/
public function extractAggregateId($anEventSourcedAggregateRoot): string
{
if (null === $this->aggregateIdExtractor) {
$this->aggregateIdExtractor = function (): string {
return $this->aggregateId();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the problem from above.

It is no longer guaranteed and the user gets no hint that such an aggregateId method is required by the translator.
The least thing we could do is add a method exists check and throw a meaningful exception instead of fatal but still not the best experience for the user.

};
}

return $this->aggregateIdExtractor->call($anEventSourcedAggregateRoot);
}

/**
* @param AggregateType $aggregateType
* @param Iterator $historyEvents
*
* @return object reconstructed AggregateRoot
*/
public function reconstituteAggregateFromHistory(AggregateType $aggregateType, Iterator $historyEvents)
{
if (null === $this->aggregateReconstructor) {
$this->aggregateReconstructor = function ($historyEvents) {
return static::reconstituteFromHistory($historyEvents);
};
}

$arClass = $aggregateType->toString();

if (! class_exists($arClass)) {
throw new RuntimeException(
sprintf('Aggregate root class %s cannot be found', $arClass)
);
}

return ($this->aggregateReconstructor->bindTo(null, $arClass))($historyEvents);
}

/**
* @param object $anEventSourcedAggregateRoot
*
* @return Message[]
*/
public function extractPendingStreamEvents($anEventSourcedAggregateRoot): array
{
if (null === $this->pendingEventsExtractor) {
$this->pendingEventsExtractor = function (): array {
return $this->popRecordedEvents();
};
}

return $this->pendingEventsExtractor->call($anEventSourcedAggregateRoot);
}

/**
* @param object $anEventSourcedAggregateRoot
* @param Iterator $events
*
* @return void
*/
public function replayStreamEvents($anEventSourcedAggregateRoot, Iterator $events): void
{
if (null === $this->replayStreamEvents) {
$this->replayStreamEvents = function ($events): void {
$this->replay($events);
};
}
$this->replayStreamEvents->call($anEventSourcedAggregateRoot, $events);
}
}
2 changes: 1 addition & 1 deletion tests/EventStoreIntegration/AggregateTranslatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ protected function resetRepository()
{
$this->repository = new AggregateRepository(
$this->eventStore,
AggregateType::fromAggregateRootClass('ProophTest\EventSourcing\Mock\User'),
AggregateType::fromAggregateRootClass(User::class),
new AggregateTranslator()
);
}
Expand Down
Loading