diff --git a/.travis.yml b/.travis.yml index ba5776f..3f34913 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,12 @@ matrix: - EXECUTE_CS_CHECK=true - TEST_COVERAGE=true + - php: 7.2 + env: + - DEPENDENCIES="" + - EXECUTE_CS_CHECK=false + - TEST_COVERAGE=false + cache: directories: - $HOME/.composer/cache diff --git a/composer.json b/composer.json index a458b30..6c9836d 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "bookdown/bookdown": "1.x-dev", "webuni/commonmark-table-extension": "^0.6.1", "webuni/commonmark-attributes-extension": "^0.5.0", - "prooph/php-cs-fixer-config": "^0.1.1", + "prooph/php-cs-fixer-config": "^0.2", "satooshi/php-coveralls": "^1.0", "malukenho/docheader": "^0.1.4" }, diff --git a/examples/Aggregate/Aggregate.php b/examples/FunctionalFlavour/Aggregate/Aggregate.php similarity index 88% rename from examples/Aggregate/Aggregate.php rename to examples/FunctionalFlavour/Aggregate/Aggregate.php index a36368a..9835553 100644 --- a/examples/Aggregate/Aggregate.php +++ b/examples/FunctionalFlavour/Aggregate/Aggregate.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace ProophExample\Aggregate; +namespace ProophExample\FunctionalFlavour\Aggregate; final class Aggregate { diff --git a/examples/FunctionalFlavour/Aggregate/UserDescription.php b/examples/FunctionalFlavour/Aggregate/UserDescription.php new file mode 100644 index 0000000..24b6d8d --- /dev/null +++ b/examples/FunctionalFlavour/Aggregate/UserDescription.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Aggregate; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\EventMachineDescription; +use ProophExample\FunctionalFlavour\Api\Command; +use ProophExample\FunctionalFlavour\Api\Event; +use ProophExample\FunctionalFlavour\Command\ChangeUsername; +use ProophExample\FunctionalFlavour\Command\RegisterUser; +use ProophExample\FunctionalFlavour\Event\UsernameChanged; +use ProophExample\FunctionalFlavour\Event\UserRegistered; +use ProophExample\FunctionalFlavour\Event\UserRegistrationFailed; + +/** + * Class UserDescription + * + * Tell EventMachine how to handle commands with aggregates, which events are yielded by the handle methods + * and how to apply the yielded events to the aggregate state. + * + * Please note: + * UserDescription uses closures. It is the fastest and most readable way of describing + * aggregate behaviour BUT closures cannot be serialized/cached. + * So the closure style is useful for learning and prototyping but if you want to use Event Machine for + * production, you should consider using a cacheable description like illustrated with CacheableUserDescription. + * Also see EventMachine::cacheableConfig() which throws an exception if it detects usage of closure + * The returned array can be used to call EventMachine::fromCachedConfig(). You can json_encode the config and store it + * in a json file. + * + * @package ProophExample\Aggregate + */ +final class UserDescription implements EventMachineDescription +{ + public const IDENTIFIER = 'userId'; + public const USERNAME = 'username'; + public const EMAIL = 'email'; + + const STATE_CLASS = UserState::class; + + public static function describe(EventMachine $eventMachine): void + { + self::describeRegisterUser($eventMachine); + self::describeChangeUsername($eventMachine); + } + + private static function describeRegisterUser(EventMachine $eventMachine): void + { + $eventMachine->process(Command::REGISTER_USER) + ->withNew(Aggregate::USER) + ->identifiedBy(self::IDENTIFIER) + // Note: Our custom command is passed to the function + ->handle(function (RegisterUser $command) { + //We can return a custom event + if ($command->shouldFail) { + yield new UserRegistrationFailed([self::IDENTIFIER => $command->userId]); + + return; + } + + yield new UserRegistered([ + 'userId' => $command->userId, + 'username' => $command->username, + 'email' => $command->email, + ]); + }) + ->recordThat(Event::USER_WAS_REGISTERED) + // The custom event is passed to the apply function + ->apply(function (UserRegistered $event) { + return new UserState((array) $event); + }) + ->orRecordThat(Event::USER_REGISTRATION_FAILED) + ->apply(function (UserRegistrationFailed $failed): UserState { + return new UserState([self::IDENTIFIER => $failed->userId, 'failed' => true]); + }); + } + + private static function describeChangeUsername(EventMachine $eventMachine): void + { + $eventMachine->process(Command::CHANGE_USERNAME) + ->withExisting(Aggregate::USER) + // This time we handle command with existing aggregate, hence we get current user state injected + ->handle(function (UserState $user, ChangeUsername $changeUsername) { + yield new UsernameChanged([ + self::IDENTIFIER => $user->userId, + 'oldName' => $user->username, + 'newName' => $changeUsername->username, + ]); + }) + ->recordThat(Event::USERNAME_WAS_CHANGED) + // Same here, UsernameChanged is NOT the first event, so current user state is passed + ->apply(function (UserState $user, UsernameChanged $event) { + $user->username = $event->newName; + + return $user; + }); + } + + private function __construct() + { + //static class only + } +} diff --git a/examples/FunctionalFlavour/Aggregate/UserState.php b/examples/FunctionalFlavour/Aggregate/UserState.php new file mode 100644 index 0000000..0dadd75 --- /dev/null +++ b/examples/FunctionalFlavour/Aggregate/UserState.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Aggregate; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +class UserState +{ + use ApplyPayload; + + public $userId; + public $username; + public $email; + public $failed; +} diff --git a/examples/FunctionalFlavour/Api/Command.php b/examples/FunctionalFlavour/Api/Command.php new file mode 100644 index 0000000..51121dc --- /dev/null +++ b/examples/FunctionalFlavour/Api/Command.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Api; + +use ProophExample\FunctionalFlavour\Command\ChangeUsername; +use ProophExample\FunctionalFlavour\Command\RegisterUser; + +final class Command +{ + const REGISTER_USER = 'RegisterUser'; + const CHANGE_USERNAME = 'ChangeUsername'; + const DO_NOTHING = 'DoNothing'; + + const CLASS_MAP = [ + self::REGISTER_USER => RegisterUser::class, + self::CHANGE_USERNAME => ChangeUsername::class, + ]; + + public static function createFromNameAndPayload(string $commandName, array $payload) + { + $class = self::CLASS_MAP[$commandName]; + + return new $class($payload); + } + + public static function nameOf($command): string + { + $map = \array_flip(self::CLASS_MAP); + + return $map[\get_class($command)]; + } + + private function __construct() + { + //static class only + } +} diff --git a/examples/FunctionalFlavour/Api/Event.php b/examples/FunctionalFlavour/Api/Event.php new file mode 100644 index 0000000..2c0b6d2 --- /dev/null +++ b/examples/FunctionalFlavour/Api/Event.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Api; + +use ProophExample\FunctionalFlavour\Event\UsernameChanged; +use ProophExample\FunctionalFlavour\Event\UserRegistered; +use ProophExample\FunctionalFlavour\Event\UserRegistrationFailed; + +final class Event +{ + const USER_WAS_REGISTERED = 'UserWasRegistered'; + const USER_REGISTRATION_FAILED = 'UserRegistrationFailed'; + const USERNAME_WAS_CHANGED = 'UsernameWasChanged'; + + const CLASS_MAP = [ + self::USER_WAS_REGISTERED => UserRegistered::class, + self::USER_REGISTRATION_FAILED => UserRegistrationFailed::class, + self::USERNAME_WAS_CHANGED => UsernameChanged::class, + ]; + + public static function createFromNameAndPayload(string $commandName, array $payload) + { + $class = self::CLASS_MAP[$commandName]; + + return new $class($payload); + } + + public static function nameOf($event): string + { + $map = \array_flip(self::CLASS_MAP); + + return $map[\get_class($event)]; + } + + private function __construct() + { + //static class only + } +} diff --git a/examples/FunctionalFlavour/Api/MessageDescription.php b/examples/FunctionalFlavour/Api/MessageDescription.php new file mode 100644 index 0000000..008d8ee --- /dev/null +++ b/examples/FunctionalFlavour/Api/MessageDescription.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Api; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\EventMachineDescription; +use Prooph\EventMachine\JsonSchema\JsonSchema; +use Prooph\EventMachine\JsonSchema\Type\EmailType; +use Prooph\EventMachine\JsonSchema\Type\StringType; +use Prooph\EventMachine\JsonSchema\Type\UuidType; +use ProophExample\FunctionalFlavour\Resolver\GetUserResolver; +use ProophExample\FunctionalFlavour\Resolver\GetUsersResolver; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; + +/** + * You're free to organize EventMachineDescriptions in the way that best fits your personal preferences + * + * We decided to describe all messages of the bounded context in a centralized MessageDescription. + * Another idea would be to register messages within an aggregate description. + * + * You only need to follow one rule: + * Messages need be registered BEFORE they are referenced by handling or listing descriptions + * + * Class MessageDescription + * @package ProophExample\Messaging + */ +final class MessageDescription implements EventMachineDescription +{ + public static function describe(EventMachine $eventMachine): void + { + /* Schema Definitions */ + $userId = new UuidType(); + + $username = (new StringType())->withMinLength(1); + + $userDataSchema = JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => $username, + UserDescription::EMAIL => new EmailType(), + ], [ + //If it is set to true user registration handler will record a UserRegistrationFailed event + 'shouldFail' => JsonSchema::boolean(), + ]); + $eventMachine->registerCommand(Command::DO_NOTHING, JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + ])); + + /* Message Registration */ + $eventMachine->registerCommand(Command::REGISTER_USER, $userDataSchema); + $eventMachine->registerCommand(Command::CHANGE_USERNAME, JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => $username, + ])); + + $eventMachine->registerEvent(Event::USER_WAS_REGISTERED, $userDataSchema); + $eventMachine->registerEvent(Event::USERNAME_WAS_CHANGED, JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + 'oldName' => $username, + 'newName' => $username, + ])); + + $eventMachine->registerEvent(Event::USER_REGISTRATION_FAILED, JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + ])); + + //Register user state as a Type so that we can reference it as query return type + $eventMachine->registerType('User', $userDataSchema); + $eventMachine->registerQuery(Query::GET_USER, JsonSchema::object([ + UserDescription::IDENTIFIER => $userId, + ])) + ->resolveWith(GetUserResolver::class) + ->setReturnType(JsonSchema::typeRef('User')); + + $eventMachine->registerQuery(Query::GET_USERS) + ->resolveWith(GetUsersResolver::class) + ->setReturnType(JsonSchema::array(JsonSchema::typeRef('User'))); + + $filterInput = JsonSchema::object([ + 'username' => JsonSchema::nullOr(JsonSchema::string()), + 'email' => JsonSchema::nullOr(JsonSchema::email()), + ]); + $eventMachine->registerQuery(Query::GET_FILTERED_USERS, JsonSchema::object([], [ + 'filter' => $filterInput, + ])) + ->resolveWith(GetUsersResolver::class) + ->setReturnType(JsonSchema::array(JsonSchema::typeRef('User'))); + } +} diff --git a/examples/FunctionalFlavour/Api/Query.php b/examples/FunctionalFlavour/Api/Query.php new file mode 100644 index 0000000..2b5d0a2 --- /dev/null +++ b/examples/FunctionalFlavour/Api/Query.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Api; + +use ProophExample\FunctionalFlavour\Query\GetUser; +use ProophExample\FunctionalFlavour\Query\GetUsers; + +final class Query +{ + const GET_USER = 'GetUser'; + const GET_USERS = 'GetUsers'; + const GET_FILTERED_USERS = 'GetFilteredUsers'; + + const CLASS_MAP = [ + self::GET_USER => GetUser::class, + self::GET_USERS => GetUsers::class, + ]; + + public static function createFromNameAndPayload(string $queryName, array $payload) + { + $class = self::CLASS_MAP[$queryName]; + + return new $class($payload); + } + + public static function nameOf($query): string + { + $map = \array_flip(self::CLASS_MAP); + + return $map[\get_class($query)]; + } + + private function __construct() + { + //static class only + } +} diff --git a/examples/FunctionalFlavour/Command/ChangeUsername.php b/examples/FunctionalFlavour/Command/ChangeUsername.php new file mode 100644 index 0000000..b5ab6d9 --- /dev/null +++ b/examples/FunctionalFlavour/Command/ChangeUsername.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Command; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class ChangeUsername +{ + use ApplyPayload; + + /** + * @var string + */ + public $userId; + + /** + * @var string + */ + public $username; +} diff --git a/examples/FunctionalFlavour/Command/RegisterUser.php b/examples/FunctionalFlavour/Command/RegisterUser.php new file mode 100644 index 0000000..221ea5d --- /dev/null +++ b/examples/FunctionalFlavour/Command/RegisterUser.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Command; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class RegisterUser +{ + use ApplyPayload; + + /** + * @var string + */ + public $userId; + + /** + * @var string + */ + public $username; + + /** + * @var string + */ + public $email; + + /** + * @var bool + */ + public $shouldFail = false; +} diff --git a/examples/FunctionalFlavour/Event/UserRegistered.php b/examples/FunctionalFlavour/Event/UserRegistered.php new file mode 100644 index 0000000..0e7a633 --- /dev/null +++ b/examples/FunctionalFlavour/Event/UserRegistered.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Event; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class UserRegistered +{ + use ApplyPayload; + + /** + * @var string + */ + public $userId; + + /** + * @var string + */ + public $username; + + /** + * @var string + */ + public $email; +} diff --git a/examples/FunctionalFlavour/Event/UserRegistrationFailed.php b/examples/FunctionalFlavour/Event/UserRegistrationFailed.php new file mode 100644 index 0000000..d72a6c4 --- /dev/null +++ b/examples/FunctionalFlavour/Event/UserRegistrationFailed.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Event; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class UserRegistrationFailed +{ + use ApplyPayload; + + /** + * @var string + */ + public $userId; +} diff --git a/examples/FunctionalFlavour/Event/UsernameChanged.php b/examples/FunctionalFlavour/Event/UsernameChanged.php new file mode 100644 index 0000000..441e341 --- /dev/null +++ b/examples/FunctionalFlavour/Event/UsernameChanged.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Event; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class UsernameChanged +{ + use ApplyPayload; + + /** + * @var string + */ + public $userId; + + /** + * @var string + */ + public $oldName; + + public $newName; +} diff --git a/examples/FunctionalFlavour/ExampleFunctionalPort.php b/examples/FunctionalFlavour/ExampleFunctionalPort.php new file mode 100644 index 0000000..5b97678 --- /dev/null +++ b/examples/FunctionalFlavour/ExampleFunctionalPort.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageBag; +use Prooph\EventMachine\Runtime\Functional\Port; +use ProophExample\FunctionalFlavour\Api\Command; +use ProophExample\FunctionalFlavour\Api\Event; +use ProophExample\FunctionalFlavour\Api\Query; + +final class ExampleFunctionalPort implements Port +{ + /** + * {@inheritdoc} + */ + public function deserialize(Message $message) + { + //Note: we use a very simple mapping strategy here + //You could also use a deserializer or other techniques + switch ($message->messageType()) { + case Message::TYPE_COMMAND: + return Command::createFromNameAndPayload($message->messageName(), $message->payload()); + case Message::TYPE_EVENT: + return Event::createFromNameAndPayload($message->messageName(), $message->payload()); + case Message::TYPE_QUERY: + return Query::createFromNameAndPayload($message->messageName(), $message->payload()); + } + } + + /** + * {@inheritdoc} + */ + public function serializePayload($customMessage): array + { + //Since, we use objects with public properties as custom messages, casting to array is enough + //In a production setting, you should use your own immutable messages and a serializer + return (array) $customMessage; + } + + /** + * {@inheritdoc} + */ + public function decorateEvent($customEvent): MessageBag + { + return new MessageBag( + Event::nameOf($customEvent), + MessageBag::TYPE_EVENT, + $customEvent + ); + } + + /** + * {@inheritdoc} + */ + public function getAggregateIdFromCustomCommand(string $aggregateIdPayloadKey, $customCommand): string + { + //Duck typing, do not do this in production but rather use your own interfaces + return $customCommand->{$aggregateIdPayloadKey}; + } + + /** + * {@inheritdoc} + */ + public function callCommandPreProcessor($customCommand, $preProcessor) + { + //Duck typing, do not do this in production but rather use your own interfaces + return $preProcessor->preProcess($customCommand); + } + + /** + * {@inheritdoc} + */ + public function callContextProvider($customCommand, $contextProvider) + { + //Duck typing, do not do this in production but rather use your own interfaces + return $contextProvider->provide($customCommand); + } +} diff --git a/examples/FunctionalFlavour/ProcessManager/SendWelcomeEmail.php b/examples/FunctionalFlavour/ProcessManager/SendWelcomeEmail.php new file mode 100644 index 0000000..d8e23f4 --- /dev/null +++ b/examples/FunctionalFlavour/ProcessManager/SendWelcomeEmail.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\ProcessManager; + +use Prooph\EventMachine\Messaging\MessageDispatcher; +use ProophExample\FunctionalFlavour\Event\UserRegistered; + +final class SendWelcomeEmail +{ + /** + * @var MessageDispatcher + */ + private $messageDispatcher; + + public function __construct(MessageDispatcher $messageDispatcher) + { + $this->messageDispatcher = $messageDispatcher; + } + + public function __invoke(UserRegistered $event) + { + $this->messageDispatcher->dispatch('SendWelcomeEmail', ['email' => $event->email]); + } +} diff --git a/examples/FunctionalFlavour/Projector/RegisteredUsersProjector.php b/examples/FunctionalFlavour/Projector/RegisteredUsersProjector.php new file mode 100644 index 0000000..657a1db --- /dev/null +++ b/examples/FunctionalFlavour/Projector/RegisteredUsersProjector.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Projector; + +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Projecting\CustomEventProjector; +use ProophExample\FunctionalFlavour\Event\UserRegistered; + +final class RegisteredUsersProjector implements CustomEventProjector +{ + /** + * @var DocumentStore + */ + private $documentStore; + + public function __construct(DocumentStore $documentStore) + { + $this->documentStore = $documentStore; + } + + public function handle(string $appVersion, string $projectionName, $event): void + { + switch (\get_class($event)) { + case UserRegistered::class: + /** @var UserRegistered $event */ + $this->documentStore->addDoc($projectionName . '_' . $appVersion, $event->userId, [ + 'userId' => $event->userId, + 'username' => $event->username, + 'email' => $event->email, + ]); + break; + default: + throw new RuntimeException('Cannot handle event: ' . $event->messageName()); + } + } + + public function prepareForRun(string $appVersion, string $projectionName): void + { + $this->documentStore->addCollection($projectionName . '_' . $appVersion); + } + + public function deleteReadModel(string $appVersion, string $projectionName): void + { + $this->documentStore->dropCollection($projectionName . '_' . $appVersion); + } +} diff --git a/examples/Resolver/GetUsersResolver.php b/examples/FunctionalFlavour/Query/GetUser.php similarity index 59% rename from examples/Resolver/GetUsersResolver.php rename to examples/FunctionalFlavour/Query/GetUser.php index f8e6c09..a2c0411 100644 --- a/examples/Resolver/GetUsersResolver.php +++ b/examples/FunctionalFlavour/Query/GetUser.php @@ -9,12 +9,16 @@ declare(strict_types=1); -namespace ProophExample\Resolver; +namespace ProophExample\FunctionalFlavour\Query; -use Prooph\Common\Messaging\Message; -use React\Promise\Deferred; +use ProophExample\FunctionalFlavour\Util\ApplyPayload; -interface GetUsersResolver +final class GetUser { - public function __invoke(Message $getUsers, Deferred $deferred): void; + use ApplyPayload; + + /** + * @var string + */ + public $userId; } diff --git a/examples/FunctionalFlavour/Query/GetUsers.php b/examples/FunctionalFlavour/Query/GetUsers.php new file mode 100644 index 0000000..f70ca62 --- /dev/null +++ b/examples/FunctionalFlavour/Query/GetUsers.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Query; + +use ProophExample\FunctionalFlavour\Util\ApplyPayload; + +final class GetUsers +{ + use ApplyPayload; + + /** + * @var string|null + */ + public $username; + + /** + * @var string|null + */ + public $email; +} diff --git a/examples/FunctionalFlavour/Resolver/GetUserResolver.php b/examples/FunctionalFlavour/Resolver/GetUserResolver.php new file mode 100644 index 0000000..eb18c39 --- /dev/null +++ b/examples/FunctionalFlavour/Resolver/GetUserResolver.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Resolver; + +use Prooph\EventMachine\Querying\SyncResolver; +use ProophExample\FunctionalFlavour\Query\GetUser; + +final class GetUserResolver implements SyncResolver +{ + /** + * @var array + */ + private $cachedUserState; + + public function __construct(array $cachedUserState) + { + $this->cachedUserState = $cachedUserState; + } + + public function __invoke(GetUser $getUser) + { + if ($this->cachedUserState['userId'] === $getUser->userId) { + return $this->cachedUserState; + } + new \RuntimeException('User not found'); + } +} diff --git a/examples/FunctionalFlavour/Resolver/GetUsersResolver.php b/examples/FunctionalFlavour/Resolver/GetUsersResolver.php new file mode 100644 index 0000000..0de71cf --- /dev/null +++ b/examples/FunctionalFlavour/Resolver/GetUsersResolver.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Resolver; + +use Prooph\EventMachine\Querying\AsyncResolver; +use ProophExample\FunctionalFlavour\Query\GetUsers; +use React\Promise\Deferred; + +final class GetUsersResolver implements AsyncResolver +{ + private $cachedUsers; + + public function __construct(array $cachedUsers) + { + $this->cachedUsers = $cachedUsers; + } + + public function __invoke(GetUsers $getUsers, Deferred $deferred): void + { + $deferred->resolve(\array_filter($this->cachedUsers, function (array $user) use ($getUsers): bool { + return (null === $getUsers->username || $user['username'] === $getUsers->username) + && (null === $getUsers->email || $user['email'] === $getUsers->email); + })); + } +} diff --git a/examples/FunctionalFlavour/Util/ApplyPayload.php b/examples/FunctionalFlavour/Util/ApplyPayload.php new file mode 100644 index 0000000..c98592b --- /dev/null +++ b/examples/FunctionalFlavour/Util/ApplyPayload.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\FunctionalFlavour\Util; + +trait ApplyPayload +{ + public function __construct(array $payload) + { + foreach ($payload as $key => $val) { + $this->{$key} = $val; + } + } +} diff --git a/examples/OopFlavour/Aggregate/User.php b/examples/OopFlavour/Aggregate/User.php new file mode 100644 index 0000000..24e7301 --- /dev/null +++ b/examples/OopFlavour/Aggregate/User.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\OopFlavour\Aggregate; + +use Prooph\EventMachine\Exception\RuntimeException; +use ProophExample\FunctionalFlavour\Command\ChangeUsername; +use ProophExample\FunctionalFlavour\Command\RegisterUser; +use ProophExample\FunctionalFlavour\Event\UsernameChanged; +use ProophExample\FunctionalFlavour\Event\UserRegistered; +use ProophExample\FunctionalFlavour\Event\UserRegistrationFailed; + +final class User +{ + public const TYPE = 'User'; + + private $userId; + + private $username; + + private $email; + + private $failed; + + private $recordedEvents = []; + + public static function reconstituteFromHistory(iterable $history): self + { + $self = new self(); + foreach ($history as $event) { + $self->apply($event); + } + + return $self; + } + + public static function register(RegisterUser $command): self + { + $self = new self(); + + if ($command->shouldFail) { + $self->recordThat(new UserRegistrationFailed([ + 'userId' => $command->userId, + ])); + + return $self; + } + + $self->recordThat(new UserRegistered([ + 'userId' => $command->userId, + 'username' => $command->username, + 'email' => $command->email, + ])); + + return $self; + } + + public function changeName(ChangeUsername $command): void + { + $this->recordThat(new UsernameChanged([ + 'userId' => $this->userId, + 'oldName' => $this->username, + 'newName' => $command->username, + ])); + } + + public function popRecordedEvents(): array + { + $events = $this->recordedEvents; + $this->recordedEvents = []; + + return $events; + } + + public function apply($event): void + { + switch (\get_class($event)) { + case UserRegistered::class: + /** @var UserRegistered $event */ + $this->userId = $event->userId; + $this->username = $event->username; + $this->email = $event->email; + break; + case UserRegistrationFailed::class: + /** @var UserRegistrationFailed $event */ + $this->userId = $event->userId; + $this->failed = true; + break; + case UsernameChanged::class: + /** @var UsernameChanged $event */ + $this->username = $event->newName; + break; + default: + throw new RuntimeException('Unknown event: ' . \get_class($event)); + } + } + + public function toArray(): array + { + return [ + 'userId' => $this->userId, + 'username' => $this->username, + 'email' => $this->email, + 'failed' => $this->failed, + ]; + } + + private function recordThat($event): void + { + $this->recordedEvents[] = $event; + } + + private function __construct() + { + } +} diff --git a/examples/OopFlavour/Aggregate/UserDescription.php b/examples/OopFlavour/Aggregate/UserDescription.php new file mode 100644 index 0000000..9fcdeb1 --- /dev/null +++ b/examples/OopFlavour/Aggregate/UserDescription.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\OopFlavour\Aggregate; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\EventMachineDescription; +use Prooph\EventMachine\Runtime\Oop\InterceptorHint; +use ProophExample\FunctionalFlavour\Api\Command; +use ProophExample\FunctionalFlavour\Api\Event; + +/** + * Class UserDescription + * + * @package ProophExample\Aggregate + */ +final class UserDescription implements EventMachineDescription +{ + public const IDENTIFIER = 'userId'; + public const USERNAME = 'username'; + public const EMAIL = 'email'; + + public static function describe(EventMachine $eventMachine): void + { + self::describeRegisterUser($eventMachine); + self::describeChangeUsername($eventMachine); + } + + private static function describeRegisterUser(EventMachine $eventMachine): void + { + $eventMachine->process(Command::REGISTER_USER) + ->withNew(User::TYPE) + ->identifiedBy(self::IDENTIFIER) + // Note: Our custom command is passed to the function + ->handle([User::class, 'register']) + ->recordThat(Event::USER_WAS_REGISTERED) + // We pass a call hint. This is a No-Op callable + // because OOPAggregateCallInterceptor does not use this callable + // see OOPAggregateCallInterceptor::callApplyFirstEvent() + // and OOPAggregateCallInterceptor::callApplySubsequentEvent() + ->apply([InterceptorHint::class, 'useAggregate']); + } + + private static function describeChangeUsername(EventMachine $eventMachine): void + { + $eventMachine->process(Command::CHANGE_USERNAME) + ->withExisting(User::TYPE) + ->handle([InterceptorHint::class, 'useAggregate']) + ->recordThat(Event::USERNAME_WAS_CHANGED) + ->apply([InterceptorHint::class, 'useAggregate']); + } + + private function __construct() + { + //static class only + } +} diff --git a/examples/OopFlavour/ExampleOopPort.php b/examples/OopFlavour/ExampleOopPort.php new file mode 100644 index 0000000..53def23 --- /dev/null +++ b/examples/OopFlavour/ExampleOopPort.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\OopFlavour; + +use Prooph\EventMachine\Exception\InvalidArgumentException; +use Prooph\EventMachine\Runtime\Oop\Port; +use Prooph\EventMachine\Util\DetermineVariableType; +use ProophExample\FunctionalFlavour\Command\ChangeUsername; +use ProophExample\OopFlavour\Aggregate\User; + +final class ExampleOopPort implements Port +{ + use DetermineVariableType; + + /** + * {@inheritdoc} + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFactory, $customCommand, $context = null) + { + return $aggregateFactory($customCommand, $context); + } + + /** + * {@inheritdoc} + */ + public function callAggregateWithCommand($aggregate, $customCommand, $context = null): void + { + switch (\get_class($customCommand)) { + case ChangeUsername::class: + /** @var User $aggregate */ + $aggregate->changeName($customCommand); + break; + default: + throw new InvalidArgumentException('Unknown command: ' . self::getType($customCommand)); + } + } + + /** + * {@inheritdoc} + */ + public function popRecordedEvents($aggregate): array + { + //Duck typing, do not do this in production but rather use your own interfaces + return $aggregate->popRecordedEvents(); + } + + /** + * {@inheritdoc} + */ + public function applyEvent($aggregate, $customEvent): void + { + //Duck typing, do not do this in production but rather use your own interfaces + $aggregate->apply($customEvent); + } + + /** + * {@inheritdoc} + */ + public function serializeAggregate($aggregate): array + { + //Duck typing, do not do this in production but rather use your own interfaces + return $aggregate->toArray(); + } + + /** + * {@inheritdoc} + */ + public function reconstituteAggregate(string $aggregateType, iterable $events) + { + switch ($aggregateType) { + case User::TYPE: + return User::reconstituteFromHistory($events); + break; + default: + throw new InvalidArgumentException("Unknown aggregate type $aggregateType"); + } + } +} diff --git a/examples/PrototypingFlavour/Aggregate/Aggregate.php b/examples/PrototypingFlavour/Aggregate/Aggregate.php new file mode 100644 index 0000000..d242b04 --- /dev/null +++ b/examples/PrototypingFlavour/Aggregate/Aggregate.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\PrototypingFlavour\Aggregate; + +final class Aggregate +{ + const USER = 'User'; + + private function __construct() + { + //static class only + } +} diff --git a/examples/Aggregate/CachableUserFunction.php b/examples/PrototypingFlavour/Aggregate/CachableUserFunction.php similarity index 83% rename from examples/Aggregate/CachableUserFunction.php rename to examples/PrototypingFlavour/Aggregate/CachableUserFunction.php index c7a786b..a6e8366 100644 --- a/examples/Aggregate/CachableUserFunction.php +++ b/examples/PrototypingFlavour/Aggregate/CachableUserFunction.php @@ -9,16 +9,16 @@ declare(strict_types=1); -namespace ProophExample\Aggregate; +namespace ProophExample\PrototypingFlavour\Aggregate; use Prooph\Common\Messaging\Message; -use ProophExample\Messaging\Event; +use ProophExample\PrototypingFlavour\Messaging\Event; final class CachableUserFunction { public static function registerUser(Message $registerUser) { - if (! array_key_exists('shouldFail', $registerUser->payload()) || ! $registerUser->payload()['shouldFail']) { + if (! \array_key_exists('shouldFail', $registerUser->payload()) || ! $registerUser->payload()['shouldFail']) { //We just turn the command payload into event payload by yielding it yield [Event::USER_WAS_REGISTERED, $registerUser->payload()]; } else { @@ -31,7 +31,7 @@ public static function registerUser(Message $registerUser) public static function whenUserWasRegistered(Message $userWasRegistered) { $user = new UserState(); - $user->id = $userWasRegistered->payload()[CacheableUserDescription::IDENTIFIER]; + $user->userId = $userWasRegistered->payload()[CacheableUserDescription::IDENTIFIER]; $user->username = $userWasRegistered->payload()['username']; $user->email = $userWasRegistered->payload()['email']; @@ -49,7 +49,7 @@ public static function whenUserRegistrationFailed(Message $userRegistrationFaile public static function changeUsername(UserState $user, Message $changeUsername) { yield [Event::USERNAME_WAS_CHANGED, [ - CacheableUserDescription::IDENTIFIER => $user->id, + CacheableUserDescription::IDENTIFIER => $user->userId, 'oldName' => $user->username, 'newName' => $changeUsername->payload()['username'], ]]; diff --git a/examples/Aggregate/CacheableUserDescription.php b/examples/PrototypingFlavour/Aggregate/CacheableUserDescription.php similarity index 94% rename from examples/Aggregate/CacheableUserDescription.php rename to examples/PrototypingFlavour/Aggregate/CacheableUserDescription.php index cfd3495..7b6ea37 100644 --- a/examples/Aggregate/CacheableUserDescription.php +++ b/examples/PrototypingFlavour/Aggregate/CacheableUserDescription.php @@ -9,11 +9,11 @@ declare(strict_types=1); -namespace ProophExample\Aggregate; +namespace ProophExample\PrototypingFlavour\Aggregate; use Prooph\EventMachine\EventMachine; -use ProophExample\Messaging\Command; -use ProophExample\Messaging\Event; +use ProophExample\PrototypingFlavour\Messaging\Command; +use ProophExample\PrototypingFlavour\Messaging\Event; /** * Class CacheableUserDescription diff --git a/examples/Aggregate/UserDescription.php b/examples/PrototypingFlavour/Aggregate/UserDescription.php similarity index 93% rename from examples/Aggregate/UserDescription.php rename to examples/PrototypingFlavour/Aggregate/UserDescription.php index 6835421..5fb65b0 100644 --- a/examples/Aggregate/UserDescription.php +++ b/examples/PrototypingFlavour/Aggregate/UserDescription.php @@ -9,13 +9,13 @@ declare(strict_types=1); -namespace ProophExample\Aggregate; +namespace ProophExample\PrototypingFlavour\Aggregate; use Prooph\Common\Messaging\Message; use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\EventMachineDescription; -use ProophExample\Messaging\Command; -use ProophExample\Messaging\Event; +use ProophExample\PrototypingFlavour\Messaging\Command; +use ProophExample\PrototypingFlavour\Messaging\Event; /** * Class UserDescription @@ -66,7 +66,7 @@ private static function describeRegisterUser(EventMachine $eventMachine): void // you can use anything for aggregate state - we use a simple class with public properties ->apply(function (Message $userWasRegistered) { $user = new UserState(); - $user->id = $userWasRegistered->payload()[self::IDENTIFIER]; + $user->userId = $userWasRegistered->payload()[self::IDENTIFIER]; $user->username = $userWasRegistered->payload()['username']; $user->email = $userWasRegistered->payload()['email']; @@ -81,7 +81,7 @@ private static function describeChangeUsername(EventMachine $eventMachine): void // This time we handle command with existing aggregate, hence we get current user state injected ->handle(function (UserState $user, Message $changeUsername) { yield [Event::USERNAME_WAS_CHANGED, [ - self::IDENTIFIER => $user->id, + self::IDENTIFIER => $user->userId, 'oldName' => $user->username, 'newName' => $changeUsername->payload()['username'], ]]; diff --git a/examples/Aggregate/UserState.php b/examples/PrototypingFlavour/Aggregate/UserState.php similarity index 83% rename from examples/Aggregate/UserState.php rename to examples/PrototypingFlavour/Aggregate/UserState.php index f7a2657..7337855 100644 --- a/examples/Aggregate/UserState.php +++ b/examples/PrototypingFlavour/Aggregate/UserState.php @@ -9,11 +9,11 @@ declare(strict_types=1); -namespace ProophExample\Aggregate; +namespace ProophExample\PrototypingFlavour\Aggregate; class UserState { - public $id; + public $userId; public $username; public $email; public $failed; diff --git a/examples/Messaging/Command.php b/examples/PrototypingFlavour/Messaging/Command.php similarity index 90% rename from examples/Messaging/Command.php rename to examples/PrototypingFlavour/Messaging/Command.php index 7d3b9ac..c2b0c58 100644 --- a/examples/Messaging/Command.php +++ b/examples/PrototypingFlavour/Messaging/Command.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace ProophExample\Messaging; +namespace ProophExample\PrototypingFlavour\Messaging; final class Command { diff --git a/examples/Messaging/Event.php b/examples/PrototypingFlavour/Messaging/Event.php similarity index 91% rename from examples/Messaging/Event.php rename to examples/PrototypingFlavour/Messaging/Event.php index 602af68..2263063 100644 --- a/examples/Messaging/Event.php +++ b/examples/PrototypingFlavour/Messaging/Event.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace ProophExample\Messaging; +namespace ProophExample\PrototypingFlavour\Messaging; final class Event { diff --git a/examples/Messaging/MessageDescription.php b/examples/PrototypingFlavour/Messaging/MessageDescription.php similarity index 92% rename from examples/Messaging/MessageDescription.php rename to examples/PrototypingFlavour/Messaging/MessageDescription.php index e64c804..5ab8a99 100644 --- a/examples/Messaging/MessageDescription.php +++ b/examples/PrototypingFlavour/Messaging/MessageDescription.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace ProophExample\Messaging; +namespace ProophExample\PrototypingFlavour\Messaging; use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\EventMachineDescription; @@ -17,9 +17,9 @@ use Prooph\EventMachine\JsonSchema\Type\EmailType; use Prooph\EventMachine\JsonSchema\Type\StringType; use Prooph\EventMachine\JsonSchema\Type\UuidType; -use ProophExample\Aggregate\UserDescription; -use ProophExample\Resolver\GetUserResolver; -use ProophExample\Resolver\GetUsersResolver; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Resolver\GetUserResolver; +use ProophExample\PrototypingFlavour\Resolver\GetUsersResolver; /** * You're free to organize EventMachineDescriptions in the way that best fits your personal preferences @@ -90,7 +90,7 @@ public static function describe(EventMachine $eventMachine): void 'email' => JsonSchema::nullOr(JsonSchema::email()), ]); $eventMachine->registerQuery(Query::GET_FILTERED_USERS, JsonSchema::object([], [ - 'filter' => JsonSchema::nullOr(JsonSchema::typeRef('UserFilterInput')), + 'filter' => $filterInput, ])) ->resolveWith(GetUsersResolver::class) ->setReturnType(JsonSchema::array(JsonSchema::typeRef('User'))); diff --git a/examples/Messaging/Query.php b/examples/PrototypingFlavour/Messaging/Query.php similarity index 90% rename from examples/Messaging/Query.php rename to examples/PrototypingFlavour/Messaging/Query.php index 095f267..4a28d65 100644 --- a/examples/Messaging/Query.php +++ b/examples/PrototypingFlavour/Messaging/Query.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace ProophExample\Messaging; +namespace ProophExample\PrototypingFlavour\Messaging; final class Query { diff --git a/examples/PrototypingFlavour/ProcessManager/SendWelcomeEmail.php b/examples/PrototypingFlavour/ProcessManager/SendWelcomeEmail.php new file mode 100644 index 0000000..6d617e5 --- /dev/null +++ b/examples/PrototypingFlavour/ProcessManager/SendWelcomeEmail.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\PrototypingFlavour\ProcessManager; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageDispatcher; + +final class SendWelcomeEmail +{ + /** + * @var MessageDispatcher + */ + private $messageDispatcher; + + public function __construct(MessageDispatcher $messageDispatcher) + { + $this->messageDispatcher = $messageDispatcher; + } + + public function __invoke(Message $event) + { + $this->messageDispatcher->dispatch('SendWelcomeEmail', ['email' => $event->get('email')]); + } +} diff --git a/examples/PrototypingFlavour/Projector/RegisteredUsersProjector.php b/examples/PrototypingFlavour/Projector/RegisteredUsersProjector.php new file mode 100644 index 0000000..109f600 --- /dev/null +++ b/examples/PrototypingFlavour/Projector/RegisteredUsersProjector.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\PrototypingFlavour\Projector; + +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Projecting\Projector; +use ProophExample\PrototypingFlavour\Messaging\Event; + +final class RegisteredUsersProjector implements Projector +{ + /** + * @var DocumentStore + */ + private $documentStore; + + public function __construct(DocumentStore $documentStore) + { + $this->documentStore = $documentStore; + } + + public function handle(string $appVersion, string $projectionName, Message $event): void + { + switch ($event->messageName()) { + case Event::USER_WAS_REGISTERED: + $this->documentStore->addDoc($projectionName . '_' . $appVersion, $event->get('userId'), [ + 'userId' => $event->get('userId'), + 'username' => $event->get('username'), + 'email' => $event->get('email'), + ]); + break; + default: + throw new RuntimeException('Cannot handle event: ' . $event->messageName()); + } + } + + public function prepareForRun(string $appVersion, string $projectionName): void + { + $this->documentStore->addCollection($projectionName . '_' . $appVersion); + } + + public function deleteReadModel(string $appVersion, string $projectionName): void + { + $this->documentStore->dropCollection($projectionName . '_' . $appVersion); + } +} diff --git a/examples/PrototypingFlavour/Resolver/GetUserResolver.php b/examples/PrototypingFlavour/Resolver/GetUserResolver.php new file mode 100644 index 0000000..ab3b509 --- /dev/null +++ b/examples/PrototypingFlavour/Resolver/GetUserResolver.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\PrototypingFlavour\Resolver; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Querying\SyncResolver; + +final class GetUserResolver implements SyncResolver +{ + /** + * @var array + */ + private $cachedUserState; + + public function __construct(array $cachedUserState) + { + $this->cachedUserState = $cachedUserState; + } + + public function __invoke(Message $getUser) + { + if ($this->cachedUserState['userId'] === $getUser->get('userId')) { + return $this->cachedUserState; + } + new \RuntimeException('User not found'); + } +} diff --git a/examples/PrototypingFlavour/Resolver/GetUsersResolver.php b/examples/PrototypingFlavour/Resolver/GetUsersResolver.php new file mode 100644 index 0000000..39476b3 --- /dev/null +++ b/examples/PrototypingFlavour/Resolver/GetUsersResolver.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophExample\PrototypingFlavour\Resolver; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Querying\AsyncResolver; +use React\Promise\Deferred; + +final class GetUsersResolver implements AsyncResolver +{ + private $cachedUsers; + + public function __construct(array $cachedUsers) + { + $this->cachedUsers = $cachedUsers; + } + + public function __invoke(Message $getUsers, Deferred $deferred): void + { + $usernameFilter = $getUsers->getOrDefault('username', null); + $emailFilter = $getUsers->getOrDefault('email', null); + + $deferred->resolve(\array_filter($this->cachedUsers, function (array $user) use ($usernameFilter, $emailFilter): bool { + return (null === $usernameFilter || $user['username'] === $usernameFilter) + && (null === $emailFilter || $user['email'] === $emailFilter); + })); + } +} diff --git a/examples/Resolver/GetUserResolver.php b/examples/Resolver/GetUserResolver.php deleted file mode 100644 index 517d868..0000000 --- a/examples/Resolver/GetUserResolver.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophExample\Resolver; - -use Prooph\Common\Messaging\Message; -use Prooph\EventMachine\EventMachine; -use ProophExample\Aggregate\Aggregate; -use React\Promise\Deferred; - -final class GetUserResolver -{ - /** - * @var EventMachine - */ - private $eventMachine; - - public function __construct(EventMachine $eventMachine) - { - $this->eventMachine = $eventMachine; - } - - public function __invoke(Message $getUser, Deferred $deferred): void - { - $userState = $this->eventMachine->loadAggregateState(Aggregate::USER, $getUser->payload()['userId']); - - if ($userState) { - $deferred->resolve($userState); - } else { - $deferred->reject(new \RuntimeException('User not found')); - } - } -} diff --git a/src/Aggregate/AggregateTestHistoryEventEnricher.php b/src/Aggregate/AggregateTestHistoryEventEnricher.php index 35b432f..9860457 100644 --- a/src/Aggregate/AggregateTestHistoryEventEnricher.php +++ b/src/Aggregate/AggregateTestHistoryEventEnricher.php @@ -32,7 +32,7 @@ public static function enrichHistory(array $history, array $aggregateDefinitions $arId = $event->payload()[$aggregateDefinition['aggregateIdentifier']] ?? null; if (! $arId) { - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Event with name %s does not contain an aggregate identifier. Expected key was %s', $event->messageName(), $aggregateDefinition['aggregateIdentifier'] @@ -44,7 +44,7 @@ public static function enrichHistory(array $history, array $aggregateDefinitions $aggregateMap[$aggregateDefinition['aggregateType']][$arId][] = $event; - $event = $event->withAddedMetadata('_aggregate_version', count($aggregateMap[$aggregateDefinition['aggregateType']][$arId])); + $event = $event->withAddedMetadata('_aggregate_version', \count($aggregateMap[$aggregateDefinition['aggregateType']][$arId])); $enrichedHistory[] = $event; } @@ -55,7 +55,7 @@ public static function enrichHistory(array $history, array $aggregateDefinitions private static function getAggregateDescriptionByEvent(string $eventName, array $aggregateDescriptions): ?array { foreach ($aggregateDescriptions as $description) { - if (array_key_exists($eventName, $description['eventApplyMap'])) { + if (\array_key_exists($eventName, $description['eventApplyMap'])) { return $description; } } diff --git a/src/Aggregate/ClosureAggregateTranslator.php b/src/Aggregate/ClosureAggregateTranslator.php index f128dfb..e9bcea1 100644 --- a/src/Aggregate/ClosureAggregateTranslator.php +++ b/src/Aggregate/ClosureAggregateTranslator.php @@ -13,6 +13,7 @@ use Iterator; use Prooph\Common\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventSourcing\Aggregate\AggregateTranslator as EventStoreAggregateTranslator; use Prooph\EventSourcing\Aggregate\AggregateType; @@ -37,10 +38,16 @@ final class ClosureAggregateTranslator implements EventStoreAggregateTranslator private $eventApplyMap; - public function __construct(string $aggregateId, array $eventApplyMap) + /** + * @var Flavour + */ + private $flavour; + + public function __construct(string $aggregateId, array $eventApplyMap, Flavour $flavour) { $this->aggregateId = $aggregateId; $this->eventApplyMap = $eventApplyMap; + $this->flavour = $flavour; } /** @@ -80,8 +87,9 @@ public function reconstituteAggregateFromHistory(AggregateType $aggregateType, I if (null === $this->aggregateReconstructor) { $arId = $this->aggregateId; $eventApplyMap = $this->eventApplyMap; - $this->aggregateReconstructor = function ($historyEvents) use ($arId, $aggregateType, $eventApplyMap) { - return static::reconstituteFromHistory($arId, $aggregateType, $eventApplyMap, $historyEvents); + $flavour = $this->flavour; + $this->aggregateReconstructor = function ($historyEvents) use ($arId, $aggregateType, $eventApplyMap, $flavour) { + return static::reconstituteFromHistory($arId, $aggregateType, $eventApplyMap, $flavour, $historyEvents); }; } @@ -96,8 +104,12 @@ public function reconstituteAggregateFromHistory(AggregateType $aggregateType, I public function extractPendingStreamEvents($anEventSourcedAggregateRoot): array { if (null === $this->pendingEventsExtractor) { - $this->pendingEventsExtractor = function (): array { - return $this->popRecordedEvents(); + $callInterceptor = $this->flavour; + + $this->pendingEventsExtractor = function () use ($callInterceptor): array { + return \array_map(function (Message $event) use ($callInterceptor) { + return $callInterceptor->prepareNetworkTransmission($event); + }, $this->popRecordedEvents()); }; } diff --git a/src/Aggregate/Exception/AggregateNotFound.php b/src/Aggregate/Exception/AggregateNotFound.php index 485a2c1..606bfa8 100644 --- a/src/Aggregate/Exception/AggregateNotFound.php +++ b/src/Aggregate/Exception/AggregateNotFound.php @@ -17,7 +17,7 @@ final class AggregateNotFound extends \RuntimeException { public static function with(string $aggregateType, string $aggregateId): self { - return new self(sprintf( + return new self(\sprintf( 'Aggregate of type %s with id %s not found.', $aggregateType, $aggregateId diff --git a/src/Aggregate/GenericAggregateRoot.php b/src/Aggregate/GenericAggregateRoot.php index 004ac4c..0555475 100644 --- a/src/Aggregate/GenericAggregateRoot.php +++ b/src/Aggregate/GenericAggregateRoot.php @@ -12,6 +12,8 @@ namespace Prooph\EventMachine\Aggregate; use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventSourcing\Aggregate\AggregateType; use Prooph\EventSourcing\Aggregate\AggregateTypeProvider; use Prooph\EventSourcing\Aggregate\Exception\RuntimeException; @@ -55,30 +57,41 @@ final class GenericAggregateRoot implements AggregateTypeProvider */ private $recordedEvents = []; + /** + * @var Flavour + */ + private $flavour; + /** * @throws RuntimeException */ - protected static function reconstituteFromHistory(string $aggregateId, AggregateType $aggregateType, array $eventApplyMap, \Iterator $historyEvents): self - { - $instance = new self($aggregateId, $aggregateType, $eventApplyMap); + protected static function reconstituteFromHistory( + string $aggregateId, + AggregateType $aggregateType, + array $eventApplyMap, + Flavour $flavour, + \Iterator $historyEvents + ): self { + $instance = new self($aggregateId, $aggregateType, $eventApplyMap, $flavour); $instance->replay($historyEvents); return $instance; } - public function __construct(string $aggregateId, AggregateType $aggregateType, array $eventApplyMap) + public function __construct(string $aggregateId, AggregateType $aggregateType, array $eventApplyMap, Flavour $flavour) { $this->aggregateId = $aggregateId; $this->aggregateType = $aggregateType; $this->eventApplyMap = $eventApplyMap; + $this->flavour = $flavour; } /** * Record an aggregate changed event */ - public function recordThat(GenericJsonSchemaEvent $event): void + public function recordThat(Message $event): void { - if (! array_key_exists($event->messageName(), $this->eventApplyMap)) { + if (! \array_key_exists($event->messageName(), $this->eventApplyMap)) { throw new \RuntimeException('Wrong event recording detected. Unknown event passed to GenericAggregateRoot: ' . $event->messageName()); } @@ -122,22 +135,26 @@ protected function popRecordedEvents(): array */ protected function replay(\Iterator $historyEvents): void { + $isFirstEvent = true; foreach ($historyEvents as $pastEvent) { /** @var GenericJsonSchemaEvent $pastEvent */ $this->version = $pastEvent->version(); + $pastEvent = $this->flavour->convertMessageReceivedFromNetwork($pastEvent, $isFirstEvent); + $isFirstEvent = false; + $this->apply($pastEvent); } } - private function apply(GenericJsonSchemaEvent $event): void + private function apply(Message $event): void { $apply = $this->eventApplyMap[$event->messageName()]; if ($this->aggregateState === null) { - $newArState = $apply($event); + $newArState = $this->flavour->callApplyFirstEvent($apply, $event); } else { - $newArState = $apply($this->aggregateState, $event); + $newArState = $this->flavour->callApplySubsequentEvent($apply, $this->aggregateState, $event); } if (null === $newArState) { diff --git a/src/Commanding/CommandProcessor.php b/src/Commanding/CommandProcessor.php index 305a68b..5305179 100644 --- a/src/Commanding/CommandProcessor.php +++ b/src/Commanding/CommandProcessor.php @@ -16,7 +16,8 @@ use Prooph\EventMachine\Aggregate\ContextProvider; use Prooph\EventMachine\Aggregate\Exception\AggregateNotFound; use Prooph\EventMachine\Aggregate\GenericAggregateRoot; -use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventSourcing\Aggregate\AggregateRepository; use Prooph\EventSourcing\Aggregate\AggregateType; use Prooph\EventStore\EventStore; @@ -70,6 +71,11 @@ final class CommandProcessor */ private $aggregateFunction; + /** + * @var Flavour + */ + private $flavour; + /** * @var MessageFactory */ @@ -92,40 +98,41 @@ final class CommandProcessor public static function fromDescriptionArrayAndDependencies( array $description, + Flavour $flavour, MessageFactory $messageFactory, EventStore $eventStore, SnapshotStore $snapshotStore = null, ContextProvider $contextProvider = null ): self { - if (! array_key_exists('commandName', $description)) { + if (! \array_key_exists('commandName', $description)) { throw new \InvalidArgumentException('Missing key commandName in commandProcessorDescription'); } - if (! array_key_exists('createAggregate', $description)) { + if (! \array_key_exists('createAggregate', $description)) { throw new \InvalidArgumentException('Missing key createAggregate in commandProcessorDescription'); } - if (! array_key_exists('aggregateType', $description)) { + if (! \array_key_exists('aggregateType', $description)) { throw new \InvalidArgumentException('Missing key aggregateType in commandProcessorDescription'); } - if (! array_key_exists('aggregateIdentifier', $description)) { + if (! \array_key_exists('aggregateIdentifier', $description)) { throw new \InvalidArgumentException('Missing key aggregateIdentifier in commandProcessorDescription'); } - if (! array_key_exists('aggregateFunction', $description)) { + if (! \array_key_exists('aggregateFunction', $description)) { throw new \InvalidArgumentException('Missing key aggregateFunction in commandProcessorDescription'); } - if (! array_key_exists('eventRecorderMap', $description)) { + if (! \array_key_exists('eventRecorderMap', $description)) { throw new \InvalidArgumentException('Missing key eventRecorderMap in commandProcessorDescription'); } - if (! array_key_exists('eventApplyMap', $description)) { + if (! \array_key_exists('eventApplyMap', $description)) { throw new \InvalidArgumentException('Missing key eventApplyMap in commandProcessorDescription'); } - if (! array_key_exists('streamName', $description)) { + if (! \array_key_exists('streamName', $description)) { throw new \InvalidArgumentException('Missing key streamName in commandProcessorDescription'); } @@ -138,6 +145,7 @@ public static function fromDescriptionArrayAndDependencies( $description['eventRecorderMap'], $description['eventApplyMap'], $description['streamName'], + $flavour, $messageFactory, $eventStore, $snapshotStore, @@ -154,6 +162,7 @@ public function __construct( array $eventRecorderMap, array $eventApplyMap, string $streamName, + Flavour $flavour, MessageFactory $messageFactory, EventStore $eventStore, SnapshotStore $snapshotStore = null, @@ -167,13 +176,14 @@ public function __construct( $this->eventRecorderMap = $eventRecorderMap; $this->eventApplyMap = $eventApplyMap; $this->streamName = $streamName; + $this->flavour = $flavour; $this->messageFactory = $messageFactory; $this->eventStore = $eventStore; $this->snapshotStore = $snapshotStore; $this->contextProvider = $contextProvider; } - public function __invoke(GenericJsonSchemaCommand $command) + public function __invoke(Message $command) { if ($command->messageName() !== $this->commandName) { throw new \RuntimeException('Wrong routing detected. Command processor is responsible for ' @@ -181,23 +191,15 @@ public function __invoke(GenericJsonSchemaCommand $command) . $command->messageName() . ' received.'); } - $payload = $command->payload(); - - if (! array_key_exists($this->aggregateIdentifier, $payload)) { - throw new \RuntimeException(sprintf( - 'Missing aggregate identifier %s in payload of command %s', - $this->aggregateIdentifier, - $this->commandName - )); - } - - $arId = (string) $payload[$this->aggregateIdentifier]; + $arId = $this->flavour->getAggregateIdFromCommand($this->aggregateIdentifier, $command); $arRepository = $this->getAggregateRepository($arId); - $arFuncArgs = []; + + $aggregate = null; + $aggregateState = null; + $context = null; if ($this->createAggregate) { - $aggregate = new GenericAggregateRoot($arId, AggregateType::fromString($this->aggregateType), $this->eventApplyMap); - $arFuncArgs[] = $command; + $aggregate = new GenericAggregateRoot($arId, AggregateType::fromString($this->aggregateType), $this->eventApplyMap, $this->flavour); } else { /** @var GenericAggregateRoot $aggregate */ $aggregate = $arRepository->getAggregateRoot($arId); @@ -206,63 +208,25 @@ public function __invoke(GenericJsonSchemaCommand $command) throw AggregateNotFound::with($this->aggregateType, $arId); } - $arFuncArgs[] = $aggregate->currentState(); - $arFuncArgs[] = $command; + $aggregateState = $aggregate->currentState(); } if ($this->contextProvider) { - $arFuncArgs[] = $this->contextProvider->provide($command); + $context = $this->flavour->callContextProvider($this->contextProvider, $command); } $arFunc = $this->aggregateFunction; - $events = $arFunc(...$arFuncArgs); - - if (! $events instanceof \Generator) { - throw new \InvalidArgumentException( - 'Expected aggregateFunction to be of type Generator. ' . - 'Did you forget the yield keyword in your command handler?' - ); + if ($this->createAggregate) { + $events = $this->flavour->callAggregateFactory($this->aggregateType, $arFunc, $command, $context); + } else { + $events = $this->flavour->callSubsequentAggregateFunction($this->aggregateType, $arFunc, $aggregateState, $command, $context); } foreach ($events as $event) { if (! $event) { continue; } - - if (! is_array($event) || ! array_key_exists(0, $event) || ! array_key_exists(1, $event) - || ! is_string($event[0]) || ! is_array($event[1])) { - throw new \RuntimeException(sprintf( - 'Event returned by aggregate of type %s while handling command %s does not has the format [string eventName, array payload]!', - $this->aggregateType, - $this->commandName - )); - } - [$eventName, $payload] = $event; - - $metadata = []; - - if (array_key_exists(2, $event)) { - $metadata = $event[2]; - if (! is_array($metadata)) { - throw new \RuntimeException(sprintf( - 'Event returned by aggregate of type %s while handling command %s contains additional metadata but metadata type is not array. Detected type is: %s', - $this->aggregateType, - $this->commandName, - (is_object($metadata) ? get_class($metadata) : gettype($metadata)) - )); - } - } - - /** @var GenericJsonSchemaEvent $event */ - $event = $this->messageFactory->createMessageFromArray($eventName, [ - 'payload' => $payload, - 'metadata' => array_merge([ - '_causation_id' => $command->uuid()->toString(), - '_causation_name' => $this->commandName, - ], $metadata), - ]); - $aggregate->recordThat($event); } @@ -275,7 +239,7 @@ private function getAggregateRepository(string $aggregateId): AggregateRepositor $this->aggregateRepository = new AggregateRepository( $this->eventStore, AggregateType::fromString($this->aggregateType), - new ClosureAggregateTranslator($aggregateId, $this->eventApplyMap), + new ClosureAggregateTranslator($aggregateId, $this->eventApplyMap, $this->flavour), $this->snapshotStore, new StreamName($this->streamName) ); diff --git a/src/Commanding/CommandProcessorDescription.php b/src/Commanding/CommandProcessorDescription.php index 9a42837..1caf2e9 100644 --- a/src/Commanding/CommandProcessorDescription.php +++ b/src/Commanding/CommandProcessorDescription.php @@ -108,7 +108,7 @@ public function handle(callable $aggregateFunction): self public function recordThat(string $eventName): EventRecorderDescription { - if (array_key_exists($eventName, $this->eventRecorderMap)) { + if (\array_key_exists($eventName, $this->eventRecorderMap)) { throw new \BadMethodCallException('Method recordThat was already called for event: ' . $eventName); } diff --git a/src/Commanding/CommandToProcessorRouter.php b/src/Commanding/CommandToProcessorRouter.php index b3524f1..46b9671 100644 --- a/src/Commanding/CommandToProcessorRouter.php +++ b/src/Commanding/CommandToProcessorRouter.php @@ -14,6 +14,7 @@ use Prooph\Common\Event\ActionEvent; use Prooph\Common\Messaging\MessageFactory; use Prooph\EventMachine\Container\ContextProviderFactory; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventStore\EventStore; use Prooph\ServiceBus\MessageBus; use Prooph\ServiceBus\Plugin\AbstractPlugin; @@ -48,6 +49,11 @@ final class CommandToProcessorRouter extends AbstractPlugin */ private $contextProviderFactory; + /** + * @var Flavour + */ + private $flavour; + /** * @var SnapshotStore|null */ @@ -59,6 +65,7 @@ public function __construct( MessageFactory $messageFactory, EventStore $eventStore, ContextProviderFactory $providerFactory, + Flavour $flavour, SnapshotStore $snapshotStore = null ) { $this->routingMap = $routingMap; @@ -66,6 +73,7 @@ public function __construct( $this->messageFactory = $messageFactory; $this->eventStore = $eventStore; $this->contextProviderFactory = $providerFactory; + $this->flavour = $flavour; $this->snapshotStore = $snapshotStore; } @@ -108,6 +116,7 @@ public function onRouteMessage(ActionEvent $actionEvent): void $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->messageFactory, $this->eventStore, $this->snapshotStore, diff --git a/src/Container/ContainerChain.php b/src/Container/ContainerChain.php index 7bbf197..1e73df4 100644 --- a/src/Container/ContainerChain.php +++ b/src/Container/ContainerChain.php @@ -22,7 +22,7 @@ final class ContainerChain implements ContainerInterface public function __construct(ContainerInterface ...$chain) { - if (! count($chain)) { + if (! \count($chain)) { throw new \InvalidArgumentException('At least one container should be passed to container chain'); } diff --git a/src/Container/EventMachineContainer.php b/src/Container/EventMachineContainer.php index b7a1260..ed30699 100644 --- a/src/Container/EventMachineContainer.php +++ b/src/Container/EventMachineContainer.php @@ -48,6 +48,6 @@ public function get($id) */ public function has($id) { - return in_array($id, $this->supportedServices); + return \in_array($id, $this->supportedServices); } } diff --git a/src/Container/ReflectionBasedContainer.php b/src/Container/ReflectionBasedContainer.php index 2d3079a..8b31875 100644 --- a/src/Container/ReflectionBasedContainer.php +++ b/src/Container/ReflectionBasedContainer.php @@ -59,7 +59,7 @@ public function has($id) { $id = $this->aliasMap[$id] ?? $id; - return array_key_exists($id, $this->serviceFactoryMap); + return \array_key_exists($id, $this->serviceFactoryMap); } /** @@ -83,8 +83,8 @@ private function scanServiceFactory($serviceFactory): array if (! $returnType->isBuiltin()) { $returnTypeName = $method->getReturnType()->getName(); - if (array_key_exists($returnTypeName, $serviceFactoryMap)) { - throw new \RuntimeException(sprintf( + if (\array_key_exists($returnTypeName, $serviceFactoryMap)) { + throw new \RuntimeException(\sprintf( 'Duplicate return type in service factory detected. Method %s has the same return type like method %s. Type is %s', $method->getName(), $serviceFactoryMap[$returnTypeName], diff --git a/src/Container/TestEnvContainer.php b/src/Container/TestEnvContainer.php index 30c5d24..818a3b4 100644 --- a/src/Container/TestEnvContainer.php +++ b/src/Container/TestEnvContainer.php @@ -132,7 +132,7 @@ public function get($id) return $this->transactionManager; default: - if (! array_key_exists($id, $this->services)) { + if (! \array_key_exists($id, $this->services)) { throw ServiceNotFound::withServiceId($id); } @@ -164,7 +164,7 @@ public function has($id) case EventMachine::SERVICE_ID_TRANSACTION_MANAGER: return true; default: - return array_key_exists($id, $this->services); + return \array_key_exists($id, $this->services); } } diff --git a/src/Data/ImmutableRecordDataConverter.php b/src/Data/ImmutableRecordDataConverter.php index 3938fe4..9fb6a6d 100644 --- a/src/Data/ImmutableRecordDataConverter.php +++ b/src/Data/ImmutableRecordDataConverter.php @@ -15,7 +15,7 @@ final class ImmutableRecordDataConverter implements DataConverter { public function convertDataToArray($data): array { - if (is_array($data)) { + if (\is_array($data)) { return $data; } @@ -23,6 +23,6 @@ public function convertDataToArray($data): array return $data->toArray(); } - return (array) json_decode(json_encode($data), true); + return (array) \json_decode(\json_encode($data), true); } } diff --git a/src/Data/ImmutableRecordLogic.php b/src/Data/ImmutableRecordLogic.php index 0620a1d..0064df7 100644 --- a/src/Data/ImmutableRecordLogic.php +++ b/src/Data/ImmutableRecordLogic.php @@ -55,7 +55,7 @@ public static function fromArray(array $nativeData) public static function __type(): string { - return self::convertClassToTypeName(get_called_class()); + return self::convertClassToTypeName(\get_called_class()); } public static function __schema(): Type @@ -107,13 +107,13 @@ public function toArray(): array case ImmutableRecord::PHP_TYPE_FLOAT: case ImmutableRecord::PHP_TYPE_BOOL: case ImmutableRecord::PHP_TYPE_ARRAY: - if (array_key_exists($key, $arrayPropItemTypeMap) && ! self::isScalarType($arrayPropItemTypeMap[$key])) { + if (\array_key_exists($key, $arrayPropItemTypeMap) && ! self::isScalarType($arrayPropItemTypeMap[$key])) { if ($isNullable && $this->{$key}() === null) { $nativeData[$key] = null; continue; } - $nativeData[$key] = array_map(function ($item) use ($key, &$arrayPropItemTypeMap) { + $nativeData[$key] = \array_map(function ($item) use ($key, &$arrayPropItemTypeMap) { return $this->voTypeToNative($item, $key, $arrayPropItemTypeMap[$key]); }, $this->{$key}()); } else { @@ -147,16 +147,16 @@ private function setNativeData(array $nativeData) foreach ($nativeData as $key => $val) { if (! isset(self::$__propTypeMap[$key])) { - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Invalid property passed to Record %s. Got property with key ' . $key, - get_called_class() + \get_called_class() )); } [$type, $isNative, $isNullable] = self::$__propTypeMap[$key]; if ($val === null) { if (! $isNullable) { - throw new \RuntimeException("Got null for non nullable property $key of Record " . get_called_class()); + throw new \RuntimeException("Got null for non nullable property $key of Record " . \get_called_class()); } $recordData[$key] = null; @@ -171,8 +171,8 @@ private function setNativeData(array $nativeData) $recordData[$key] = $val; break; case ImmutableRecord::PHP_TYPE_ARRAY: - if (array_key_exists($key, $arrayPropItemTypeMap) && ! self::isScalarType($arrayPropItemTypeMap[$key])) { - $recordData[$key] = array_map(function ($item) use ($key, &$arrayPropItemTypeMap) { + if (\array_key_exists($key, $arrayPropItemTypeMap) && ! self::isScalarType($arrayPropItemTypeMap[$key])) { + $recordData[$key] = \array_map(function ($item) use ($key, &$arrayPropItemTypeMap) { return $this->fromType($item, $arrayPropItemTypeMap[$key]); }, $val); } else { @@ -191,7 +191,7 @@ private function assertAllNotNull() { foreach (self::$__propTypeMap as $key => [$type, $isNative, $isNullable]) { if (null === $this->{$key} && ! $isNullable) { - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Missing record data for key %s of record %s.', $key, __CLASS__ @@ -203,7 +203,7 @@ private function assertAllNotNull() private function assertType(string $key, $value) { if (! isset(self::$__propTypeMap[$key])) { - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Invalid property passed to Record %s. Got property with key ' . $key, __CLASS__ )); @@ -215,24 +215,24 @@ private function assertType(string $key, $value) } if (! $this->isType($type, $key, $value)) { - if ($type === ImmutableRecord::PHP_TYPE_ARRAY && gettype($value) === ImmutableRecord::PHP_TYPE_ARRAY) { + if ($type === ImmutableRecord::PHP_TYPE_ARRAY && \gettype($value) === ImmutableRecord::PHP_TYPE_ARRAY) { $arrayPropItemTypeMap = self::getArrayPropItemTypeMapFromMethodOrCache(); - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Record %s data contains invalid value for property %s. Value should be an array of %s, but at least one item of the array has the wrong type.', - get_called_class(), + \get_called_class(), $key, $arrayPropItemTypeMap[$key] )); } - throw new \InvalidArgumentException(sprintf( + throw new \InvalidArgumentException(\sprintf( 'Record %s data contains invalid value for property %s. Expected type is %s. Got type %s.', - get_called_class(), + \get_called_class(), $key, $type, - (is_object($value) - ? get_class($value) - : gettype($value)) + (\is_object($value) + ? \get_class($value) + : \gettype($value)) )); } } @@ -241,20 +241,20 @@ private function isType(string $type, string $key, $value): bool { switch ($type) { case ImmutableRecord::PHP_TYPE_STRING: - return is_string($value); + return \is_string($value); case ImmutableRecord::PHP_TYPE_INT: - return is_int($value); + return \is_int($value); case ImmutableRecord::PHP_TYPE_FLOAT: - return is_float($value) || is_int($value); + return \is_float($value) || \is_int($value); case ImmutableRecord::PHP_TYPE_BOOL: - return is_bool($value); + return \is_bool($value); case ImmutableRecord::PHP_TYPE_ARRAY: - $isType = is_array($value); + $isType = \is_array($value); if ($isType) { $arrayPropItemTypeMap = self::getArrayPropItemTypeMapFromMethodOrCache(); - if (array_key_exists($key, $arrayPropItemTypeMap)) { + if (\array_key_exists($key, $arrayPropItemTypeMap)) { foreach ($value as $item) { if (! $this->isType($arrayPropItemTypeMap[$key], $key, $item)) { return false; @@ -284,7 +284,7 @@ private static function buildPropTypeMap() if (! $refObj->hasMethod($prop->getName())) { throw new \RuntimeException( - sprintf( + \sprintf( 'No method found for Record property %s of %s that has the same name.', $prop->getName(), __CLASS__ @@ -296,7 +296,7 @@ private static function buildPropTypeMap() if (! $method->hasReturnType()) { throw new \RuntimeException( - sprintf( + \sprintf( 'Method %s of Record %s does not have a return type', $method->getName(), __CLASS__ @@ -327,12 +327,12 @@ private static function isScalarType(string $type): bool private function fromType($value, string $type) { - if (! class_exists($type)) { + if (! \class_exists($type)) { throw new \RuntimeException("Type class $type not found"); } //Note: gettype() returns "integer" and "boolean" which does not match the type hints "int", "bool" - switch (gettype($value)) { + switch (\gettype($value)) { case 'array': return $type::fromArray($value); case 'string': @@ -347,29 +347,29 @@ private function fromType($value, string $type) case 'boolean': return $type::fromBool($value); default: - throw new \RuntimeException("Cannot convert value to $type, because native type of value is not supported. Got " . gettype($value)); + throw new \RuntimeException("Cannot convert value to $type, because native type of value is not supported. Got " . \gettype($value)); } } private function voTypeToNative($value, string $key, string $type) { - if (method_exists($value, 'toArray')) { + if (\method_exists($value, 'toArray')) { return $value->toArray(); } - if (method_exists($value, 'toString')) { + if (\method_exists($value, 'toString')) { return $value->toString(); } - if (method_exists($value, 'toInt')) { + if (\method_exists($value, 'toInt')) { return $value->toInt(); } - if (method_exists($value, 'toFloat')) { + if (\method_exists($value, 'toFloat')) { return $value->toFloat(); } - if (method_exists($value, 'toBool')) { + if (\method_exists($value, 'toBool')) { return $value->toBool(); } @@ -389,7 +389,7 @@ private static function generateSchemaFromPropTypeMap(array $arrayPropTypeMap = //To keep BC, we cache arrayPropTypeMap internally. //New recommended way to provide the map is that one should override the static method self::arrayPropItemTypeMap() //Hence, we check if this method returns a non empty array and only in this case cache the map - if (count($arrayPropTypeMap) && ! count(self::arrayPropItemTypeMap())) { + if (\count($arrayPropTypeMap) && ! \count(self::arrayPropItemTypeMap())) { self::$__arrayPropItemTypeMap = $arrayPropTypeMap; } @@ -405,7 +405,7 @@ private static function generateSchemaFromPropTypeMap(array $arrayPropTypeMap = } if ($type === ImmutableRecord::PHP_TYPE_ARRAY) { - if (! array_key_exists($prop, $arrayPropTypeMap)) { + if (! \array_key_exists($prop, $arrayPropTypeMap)) { throw new \RuntimeException("Missing array item type in array property map. Please provide an array item type for property $prop."); } @@ -437,19 +437,19 @@ private static function generateSchemaFromPropTypeMap(array $arrayPropTypeMap = private static function convertClassToTypeName(string $class): string { - return substr(strrchr($class, '\\'), 1); + return \substr(\strrchr($class, '\\'), 1); } private static function getTypeFromClass(string $classOrType): string { - if (! class_exists($classOrType)) { + if (! \class_exists($classOrType)) { return $classOrType; } $refObj = new \ReflectionClass($classOrType); if ($refObj->implementsInterface(ImmutableRecord::class)) { - return call_user_func([$classOrType, '__type']); + return \call_user_func([$classOrType, '__type']); } return self::convertClassToTypeName($classOrType); diff --git a/src/EventMachine.php b/src/EventMachine.php index f11af1e..0fd237a 100644 --- a/src/EventMachine.php +++ b/src/EventMachine.php @@ -25,6 +25,7 @@ use Prooph\EventMachine\Container\ContextProviderFactory; use Prooph\EventMachine\Container\TestEnvContainer; use Prooph\EventMachine\Data\ImmutableRecord; +use Prooph\EventMachine\Eventing\EventConverterBusPlugin; use Prooph\EventMachine\Exception\InvalidArgumentException; use Prooph\EventMachine\Exception\RuntimeException; use Prooph\EventMachine\Exception\TransactionCommitFailed; @@ -36,13 +37,19 @@ use Prooph\EventMachine\JsonSchema\Type\ObjectType; use Prooph\EventMachine\Messaging\GenericJsonSchemaMessageFactory; use Prooph\EventMachine\Messaging\MessageDispatcher; +use Prooph\EventMachine\Messaging\MessageFactoryAware; +use Prooph\EventMachine\Messaging\MessageProducer; use Prooph\EventMachine\Persistence\AggregateStateStore; use Prooph\EventMachine\Persistence\Stream; use Prooph\EventMachine\Persistence\TransactionManager as BusTransactionManager; +use Prooph\EventMachine\Projecting\CustomEventProjector; use Prooph\EventMachine\Projecting\ProjectionDescription; use Prooph\EventMachine\Projecting\ProjectionRunner; use Prooph\EventMachine\Projecting\Projector; +use Prooph\EventMachine\Querying\QueryConverterBusPlugin; use Prooph\EventMachine\Querying\QueryDescription; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; use Prooph\EventSourcing\Aggregate\AggregateRepository; use Prooph\EventSourcing\Aggregate\AggregateType; use Prooph\EventStore\ActionEventEmitterEventStore; @@ -77,6 +84,7 @@ final class EventMachine implements MessageDispatcher, AggregateStateStore const SERVICE_ID_ASYNC_EVENT_PRODUCER = 'EventMachine.AsyncEventProducer'; const SERVICE_ID_MESSAGE_FACTORY = 'EventMachine.MessageFactory'; const SERVICE_ID_JSON_SCHEMA_ASSERTION = 'EventMachine.JsonSchemaAssertion'; + const SERVICE_ID_FLAVOUR = 'EventMachine.Flavour'; /** * Map of command names and corresponding json schema of payload @@ -198,6 +206,11 @@ final class EventMachine implements MessageDispatcher, AggregateStateStore private $projectionRunner; + /** + * @var Flavour + */ + private $flavour; + private $writeModelStreamName = 'event_stream'; private $immediateConsistency = false; @@ -206,19 +219,19 @@ public static function fromCachedConfig(array $config, ContainerInterface $conta { $self = new self(); - if (! array_key_exists('commandMap', $config)) { + if (! \array_key_exists('commandMap', $config)) { throw new InvalidArgumentException('Missing key commandMap in cached event machine config'); } - if (! array_key_exists('eventMap', $config)) { + if (! \array_key_exists('eventMap', $config)) { throw new InvalidArgumentException('Missing key eventMap in cached event machine config'); } - if (! array_key_exists('compiledCommandRouting', $config)) { + if (! \array_key_exists('compiledCommandRouting', $config)) { throw new InvalidArgumentException('Missing key compiledCommandRouting in cached event machine config'); } - if (! array_key_exists('aggregateDescriptions', $config)) { + if (! \array_key_exists('aggregateDescriptions', $config)) { throw new InvalidArgumentException('Missing key aggregateDescriptions in cached event machine config'); } @@ -245,7 +258,7 @@ public static function fromCachedConfig(array $config, ContainerInterface $conta public function load(string $description): void { $this->assertNotInitialized(__METHOD__); - call_user_func([$description, 'describe'], $this); + \call_user_func([$description, 'describe'], $this); } public function setWriteModelStreamName(string $streamName): self @@ -275,7 +288,7 @@ public function immediateConsistency(): bool public function registerCommand(string $commandName, ObjectType $schema): self { $this->assertNotInitialized(__METHOD__); - if (array_key_exists($commandName, $this->commandMap)) { + if (\array_key_exists($commandName, $this->commandMap)) { throw new RuntimeException("Command $commandName was already registered."); } @@ -288,7 +301,7 @@ public function registerEvent(string $eventName, ObjectType $schema): self { $this->assertNotInitialized(__METHOD__); - if (array_key_exists($eventName, $this->eventMap)) { + if (\array_key_exists($eventName, $this->eventMap)) { throw new RuntimeException("Event $eventName was already registered."); } @@ -338,8 +351,8 @@ public function registerType(string $nameOrImmutableRecordClass, ObjectType $sch throw new InvalidArgumentException("Invalid type given. $nameOrImmutableRecordClass does not implement " . ImmutableRecord::class); } - $name = call_user_func([$nameOrImmutableRecordClass, '__type']); - $schema = call_user_func([$nameOrImmutableRecordClass, '__schema']); + $name = \call_user_func([$nameOrImmutableRecordClass, '__type']); + $schema = \call_user_func([$nameOrImmutableRecordClass, '__schema']); } else { $name = $nameOrImmutableRecordClass; } @@ -378,9 +391,9 @@ public function preProcess(string $commandName, $preProcessor): self throw new InvalidArgumentException("Preprocessor attached to unknown command $commandName. You should register the command first"); } - if (! is_string($preProcessor) && ! $preProcessor instanceof CommandPreProcessor) { + if (! \is_string($preProcessor) && ! $preProcessor instanceof CommandPreProcessor) { throw new InvalidArgumentException('PreProcessor should either be a service id given as string or an instance of '.CommandPreProcessor::class.'. Got ' - . (is_object($preProcessor) ? get_class($preProcessor) : gettype($preProcessor))); + . (\is_object($preProcessor) ? \get_class($preProcessor) : \gettype($preProcessor))); } $this->commandPreProcessors[$commandName][] = $preProcessor; @@ -391,11 +404,11 @@ public function preProcess(string $commandName, $preProcessor): self public function process(string $commandName): CommandProcessorDescription { $this->assertNotInitialized(__METHOD__); - if (array_key_exists($commandName, $this->commandRouting)) { + if (\array_key_exists($commandName, $this->commandRouting)) { throw new \BadMethodCallException('Method process was called twice for the same command: ' . $commandName); } - if (! array_key_exists($commandName, $this->commandMap)) { + if (! \array_key_exists($commandName, $this->commandMap)) { throw new \BadMethodCallException("Command $commandName is unknown. You should register it first."); } @@ -412,9 +425,9 @@ public function on(string $eventName, $listener): self throw new InvalidArgumentException("Listener attached to unknown event $eventName. You should register the event first"); } - if (! is_string($listener) && ! is_callable($listener)) { + if (! \is_string($listener) && ! \is_callable($listener)) { throw new InvalidArgumentException('Listener should be either a service id given as string or a callable. Got ' - . (is_object($listener) ? get_class($listener) : gettype($listener))); + . (\is_object($listener) ? \get_class($listener) : \gettype($listener))); } $this->eventRouting[$eventName][] = $listener; @@ -433,27 +446,27 @@ public function watch(Stream $stream): ProjectionDescription public function isKnownCommand(string $commandName): bool { - return array_key_exists($commandName, $this->commandMap); + return \array_key_exists($commandName, $this->commandMap); } public function isKnownEvent(string $eventName): bool { - return array_key_exists($eventName, $this->eventMap); + return \array_key_exists($eventName, $this->eventMap); } public function isKnownQuery(string $queryName): bool { - return array_key_exists($queryName, $this->queryMap); + return \array_key_exists($queryName, $this->queryMap); } public function isKnownProjection(string $projectionName): bool { - return array_key_exists($projectionName, $this->projectionMap); + return \array_key_exists($projectionName, $this->projectionMap); } public function isKnownType(string $typeName): bool { - return array_key_exists($typeName, $this->schemaTypes); + return \array_key_exists($typeName, $this->schemaTypes); } public function isTestMode(): bool @@ -481,8 +494,8 @@ public function initialize(ContainerInterface $container, string $appVersion = ' public function bootstrap(string $env = self::ENV_PROD, $debugMode = false): self { $envModes = [self::ENV_PROD, self::ENV_DEV, self::ENV_TEST]; - if (! in_array($env, $envModes)) { - throw new InvalidArgumentException("Invalid env. Got $env but expected is one of " . implode(', ', $envModes)); + if (! \in_array($env, $envModes)) { + throw new InvalidArgumentException("Invalid env. Got $env but expected is one of " . \implode(', ', $envModes)); } $this->assertInitialized(__METHOD__); $this->assertNotBootstrapped(__METHOD__); @@ -506,13 +519,13 @@ public function dispatch($messageOrName, array $payload = []): ?Promise { $this->assertBootstrapped(__METHOD__); - if (is_string($messageOrName)) { + if (\is_string($messageOrName)) { $messageOrName = $this->messageFactory()->createMessageFromArray($messageOrName, ['payload' => $payload]); } if (! $messageOrName instanceof Message) { throw new InvalidArgumentException('Invalid message received. Must be either a known message name or an instance of prooph message. Got ' - . (is_object($messageOrName) ? get_class($messageOrName) : gettype($messageOrName))); + . (\is_object($messageOrName) ? \get_class($messageOrName) : \gettype($messageOrName))); } switch ($messageOrName->messageType()) { @@ -520,15 +533,11 @@ public function dispatch($messageOrName, array $payload = []): ?Promise $preProcessors = $this->commandPreProcessors[$messageOrName->messageName()] ?? []; foreach ($preProcessors as $preProcessorOrStr) { - if (is_string($preProcessorOrStr)) { + if (\is_string($preProcessorOrStr)) { $preProcessorOrStr = $this->container->get($preProcessorOrStr); } - if (! $preProcessorOrStr instanceof CommandPreProcessor) { - throw new RuntimeException('PreProcessor should be an instance of ' . CommandPreProcessor::class . '. Got ' . get_class($preProcessorOrStr)); - } - - $messageOrName = $preProcessorOrStr->preProcess($messageOrName); + $messageOrName = $this->flavour()->callCommandPreProcessor($preProcessorOrStr, $messageOrName); } $bus = $this->container->get(self::SERVICE_ID_COMMAND_BUS); @@ -572,7 +581,7 @@ public function loadAggregateState(string $aggregateType, string $aggregateId) { $this->assertBootstrapped(__METHOD__); - if (! array_key_exists($aggregateType, $this->aggregateDescriptions)) { + if (! \array_key_exists($aggregateType, $this->aggregateDescriptions)) { throw new InvalidArgumentException('Unknown aggregate type: ' . $aggregateType); } @@ -587,7 +596,7 @@ public function loadAggregateState(string $aggregateType, string $aggregateId) $arRepository = new AggregateRepository( $this->container->get(self::SERVICE_ID_EVENT_STORE), AggregateType::fromString($aggregateType), - new ClosureAggregateTranslator($aggregateId, $aggregateDesc['eventApplyMap']), + new ClosureAggregateTranslator($aggregateId, $aggregateDesc['eventApplyMap'], $this->flavour()), $snapshotStore, new StreamName($this->writeModelStreamName()) ); @@ -609,6 +618,7 @@ public function runProjections(bool $keepRunning = true, array $projectionOption if (null === $this->projectionRunner) { $this->projectionRunner = new ProjectionRunner( $this->container->get(self::SERVICE_ID_PROJECTION_MANAGER), + $this->flavour(), $this->compiledProjectionDescriptions, $this ); @@ -634,9 +644,26 @@ public function debugMode(): bool return $this->debugMode; } - public function loadProjector(string $projectorServiceId): Projector + /** + * @param string $projectorServiceId + * @return Projector|CustomEventProjector + */ + public function loadProjector(string $projectorServiceId) { - return $this->container->get($projectorServiceId); + $projector = $this->container->get($projectorServiceId); + + if (! $projector instanceof Projector + && ! $projector instanceof CustomEventProjector) { + throw new RuntimeException( + \sprintf( + "Projector $projectorServiceId should either be an instance of %s or %s", + Projector::class, + CustomEventProjector::class + ) + ); + } + + return $projector; } public function compileCacheableConfig(): array @@ -649,11 +676,11 @@ public function compileCacheableConfig(): array } }; - array_walk_recursive($this->compiledCommandRouting, $assertClosure); - array_walk_recursive($this->aggregateDescriptions, $assertClosure); - array_walk_recursive($this->eventRouting, $assertClosure); - array_walk_recursive($this->projectionMap, $assertClosure); - array_walk_recursive($this->compiledQueryDescriptions, $assertClosure); + \array_walk_recursive($this->compiledCommandRouting, $assertClosure); + \array_walk_recursive($this->aggregateDescriptions, $assertClosure); + \array_walk_recursive($this->eventRouting, $assertClosure); + \array_walk_recursive($this->projectionMap, $assertClosure); + \array_walk_recursive($this->compiledQueryDescriptions, $assertClosure); return [ 'commandMap' => $this->commandMap, @@ -682,6 +709,14 @@ public function messageFactory(): GenericJsonSchemaMessageFactory $this->schemaTypes, $this->container->get(self::SERVICE_ID_JSON_SCHEMA_ASSERTION) ); + + $flavour = $this->flavour(); + + //Setter injection due to circular dependency to self::callInterceptor() + $this->messageFactory->setFlavour($flavour); + if ($flavour instanceof MessageFactoryAware) { + $flavour->setMessageFactory($this->messageFactory); + } } return $this->messageFactory; @@ -702,7 +737,7 @@ public function __construct(array &$schemaTypes) public function assert(string $objectName, array $data, array $jsonSchema) { - $jsonSchema['definitions'] = array_merge($jsonSchema['definitions'] ?? [], $this->schemaTypes); + $jsonSchema['definitions'] = \array_merge($jsonSchema['definitions'] ?? [], $this->schemaTypes); $this->jsonSchemaAssertion->assert($objectName, $data, $jsonSchema); } @@ -757,7 +792,7 @@ public function messageBoxSchema(): array 'events' => $this->eventMap, 'queries' => $querySchemas, ], - 'definitions' => array_merge($this->schemaTypes, $this->schemaInputTypes), + 'definitions' => \array_merge($this->schemaTypes, $this->schemaInputTypes), ]; } @@ -784,7 +819,7 @@ public function bootstrapInTestMode(array $history, array $serviceMap = []): Con ActionEventEmitterEventStore::EVENT_APPEND_TO, function (ActionEvent $event): void { $recordedEvents = $event->getParam('streamEvents', new \ArrayIterator()); - $this->testSessionEvents = array_merge($this->testSessionEvents, iterator_to_array($recordedEvents)); + $this->testSessionEvents = \array_merge($this->testSessionEvents, \iterator_to_array($recordedEvents)); } ); @@ -793,7 +828,7 @@ function (ActionEvent $event): void { function (ActionEvent $event): void { $stream = $event->getParam('stream'); $recordedEvents = $stream->streamEvents(); - $this->testSessionEvents = array_merge($this->testSessionEvents, iterator_to_array($recordedEvents)); + $this->testSessionEvents = \array_merge($this->testSessionEvents, \iterator_to_array($recordedEvents)); } ); @@ -844,7 +879,7 @@ private function determineAggregateAndRoutingDescriptions(): void $descArr['aggregateIdentifier'] = $aggregateDesc['aggregateIdentifier']; - $aggregateDesc['eventApplyMap'] = array_merge($aggregateDesc['eventApplyMap'], $descArr['eventRecorderMap']); + $aggregateDesc['eventApplyMap'] = \array_merge($aggregateDesc['eventApplyMap'], $descArr['eventRecorderMap']); $aggregateDescriptions[$descArr['aggregateType']] = $aggregateDesc; } @@ -881,6 +916,7 @@ private function attachRouterToCommandBus(): void $this->container->get(self::SERVICE_ID_MESSAGE_FACTORY), $this->container->get(self::SERVICE_ID_EVENT_STORE), new ContextProviderFactory($this->container), + $this->flavour(), $snapshotStore ); @@ -897,12 +933,16 @@ private function setUpQueryBus(): void $queryRouter = new QueryRouter($queryRouting); + $queryConverterBusPlugin = new QueryConverterBusPlugin($this->flavour()); + + $serviceLocatorPlugin = new ServiceLocatorPlugin($this->container); + /** @var QueryBus $queryBus */ $queryBus = $this->container->get(self::SERVICE_ID_QUERY_BUS); $queryRouter->attachToMessageBus($queryBus); - $serviceLocatorPlugin = new ServiceLocatorPlugin($this->container); + $queryConverterBusPlugin->attachToMessageBus($queryBus); $serviceLocatorPlugin->attachToMessageBus($queryBus); } @@ -916,15 +956,19 @@ private function setUpEventBus(): void $eventRouter = new AsyncSwitchMessageRouter( $eventRouter, - $eventProducer + new MessageProducer($this->flavour(), $eventProducer) ); } + $eventConverterBusPlugin = new EventConverterBusPlugin($this->flavour()); + + $serviceLocatorPlugin = new ServiceLocatorPlugin($this->container); + $eventBus = $this->container->get(self::SERVICE_ID_EVENT_BUS); $eventRouter->attachToMessageBus($eventBus); - $serviceLocatorPlugin = new ServiceLocatorPlugin($this->container); + $eventConverterBusPlugin->attachToMessageBus($eventBus); $serviceLocatorPlugin->attachToMessageBus($eventBus); } @@ -949,6 +993,17 @@ private function attachEventPublisherToEventStore(): void } } + private function flavour(): Flavour + { + if (null === $this->flavour) { + $this->flavour = $this->container->has(self::SERVICE_ID_FLAVOUR) + ? $this->container->get(self::SERVICE_ID_FLAVOUR) + : new PrototypingFlavour(); + } + + return $this->flavour; + } + private function assertNotInitialized(string $method) { if ($this->initialized) { diff --git a/src/Eventing/EventConverterBusPlugin.php b/src/Eventing/EventConverterBusPlugin.php new file mode 100644 index 0000000..c3610c9 --- /dev/null +++ b/src/Eventing/EventConverterBusPlugin.php @@ -0,0 +1,68 @@ + + * + * 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\EventMachine\Eventing; + +use Prooph\Common\Event\ActionEvent; +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageProducer; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\ServiceBus\EventBus; +use Prooph\ServiceBus\MessageBus; +use Prooph\ServiceBus\Plugin\AbstractPlugin; + +final class EventConverterBusPlugin extends AbstractPlugin +{ + /** + * @var Flavour + */ + private $flavour; + + public function __construct(Flavour $flavour) + { + $this->flavour = $flavour; + } + + public function attachToMessageBus(MessageBus $messageBus): void + { + if (! $messageBus instanceof EventBus) { + throw new RuntimeException(__CLASS__ . ' can only be attached to a ' . EventBus::class); + } + + $this->listenerHandlers[] = $messageBus->attach( + EventBus::EVENT_DISPATCH, + [$this, 'decorateListeners'], + EventBus::PRIORITY_INVOKE_HANDLER + 100 + ); + } + + public function decorateListeners(ActionEvent $actionEvent): void + { + $listeners = \array_filter($actionEvent->getParam(EventBus::EVENT_PARAM_EVENT_LISTENERS, []), function ($listener) { + return \is_callable($listener); + }); + + $decoratedListeners = []; + foreach ($listeners as $listener) { + if (\is_object($listener) && $listener instanceof MessageProducer) { + $decoratedListeners[] = $listener; + continue; + } + + $decoratedListeners[] = function (Message $message) use ($listener) { + $this->flavour->callEventListener($listener, $message); + }; + } + + $actionEvent->setParam(EventBus::EVENT_PARAM_EVENT_LISTENERS, $decoratedListeners); + } +} diff --git a/src/Eventing/GenericJsonSchemaEvent.php b/src/Eventing/GenericJsonSchemaEvent.php index a46576f..030c0af 100644 --- a/src/Eventing/GenericJsonSchemaEvent.php +++ b/src/Eventing/GenericJsonSchemaEvent.php @@ -13,9 +13,8 @@ use Prooph\Common\Messaging\DomainMessage; use Prooph\EventMachine\Messaging\GenericJsonSchemaMessage; -use Prooph\ServiceBus\Async\AsyncMessage; -final class GenericJsonSchemaEvent extends GenericJsonSchemaMessage implements AsyncMessage +final class GenericJsonSchemaEvent extends GenericJsonSchemaMessage { /** * Should be one of Message::TYPE_COMMAND, Message::TYPE_EVENT or Message::TYPE_QUERY diff --git a/src/Exception/InvalidEventFormatException.php b/src/Exception/InvalidEventFormatException.php new file mode 100644 index 0000000..b011f11 --- /dev/null +++ b/src/Exception/InvalidEventFormatException.php @@ -0,0 +1,43 @@ + + * + * 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\EventMachine\Exception; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Util\DetermineVariableType; + +final class InvalidEventFormatException extends InvalidArgumentException +{ + use DetermineVariableType; + + public static function invalidEvent(string $aggregateType, Message $command): self + { + return new self( + \sprintf( + 'Event returned by aggregate of type %s while handling command %s does not have the format [string eventName, array payload]!', + $aggregateType, + $command->messageName() + ) + ); + } + + public static function invalidMetadata($metadata, string $aggregateType, Message $command): self + { + return new self( + \sprintf( + 'Event returned by aggregate of type %s while handling command %s contains additional metadata but metadata type is not array. Detected type is: %s', + $aggregateType, + $command->messageName(), + self::getType($metadata) + ) + ); + } +} diff --git a/src/Exception/MissingAggregateIdentifierException.php b/src/Exception/MissingAggregateIdentifierException.php new file mode 100644 index 0000000..89b6400 --- /dev/null +++ b/src/Exception/MissingAggregateIdentifierException.php @@ -0,0 +1,26 @@ + + * + * 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\EventMachine\Exception; + +use Prooph\EventMachine\Messaging\Message; + +final class MissingAggregateIdentifierException extends InvalidArgumentException +{ + public static function inCommand(Message $command, string $aggregateIdPayloadKey): self + { + return new self(\sprintf( + 'Missing aggregate identifier %s in payload of command %s', + $aggregateIdPayloadKey, + $command->messageName() + )); + } +} diff --git a/src/Exception/NoGeneratorException.php b/src/Exception/NoGeneratorException.php new file mode 100644 index 0000000..6e8ba77 --- /dev/null +++ b/src/Exception/NoGeneratorException.php @@ -0,0 +1,25 @@ + + * + * 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\EventMachine\Exception; + +use Prooph\EventMachine\Messaging\Message; + +final class NoGeneratorException extends InvalidArgumentException +{ + public static function forAggregateTypeAndCommand(string $aggregateType, Message $command): self + { + return new self('Expected aggregateFunction to be of type Generator. ' . + 'Did you forget the yield keyword in your command handler?' . + "Tried to handle command {$command->messageName()} for aggregate {$aggregateType}" + ); + } +} diff --git a/src/Http/MessageBox.php b/src/Http/MessageBox.php index 875cc51..bde25d7 100644 --- a/src/Http/MessageBox.php +++ b/src/Http/MessageBox.php @@ -54,7 +54,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface try { $payload = $request->getParsedBody(); - if (is_array($payload) && isset($payload['message_name'])) { + if (\is_array($payload) && isset($payload['message_name'])) { $messageName = $payload['message_name']; } diff --git a/src/JsonSchema/JsonSchema.php b/src/JsonSchema/JsonSchema.php index fcf89ac..f4ab6d1 100644 --- a/src/JsonSchema/JsonSchema.php +++ b/src/JsonSchema/JsonSchema.php @@ -138,12 +138,12 @@ public static function isObjectType(array $typeSchema): bool public static function isStringEnum(array $typeSchema): bool { - if (! array_key_exists(self::KEYWORD_ENUM, $typeSchema)) { + if (! \array_key_exists(self::KEYWORD_ENUM, $typeSchema)) { return false; } foreach ($typeSchema[self::KEYWORD_ENUM] as $val) { - if (! is_string($val)) { + if (! \is_string($val)) { return false; } } @@ -153,14 +153,14 @@ public static function isStringEnum(array $typeSchema): bool public static function isType(string $type, array $typeSchema): bool { - if (array_key_exists('type', $typeSchema)) { - if (is_array($typeSchema['type'])) { + if (\array_key_exists('type', $typeSchema)) { + if (\is_array($typeSchema['type'])) { foreach ($typeSchema['type'] as $possibleType) { if ($possibleType === $type) { return true; } } - } elseif (is_string($typeSchema['type'])) { + } elseif (\is_string($typeSchema['type'])) { return $typeSchema['type'] === $type; } } @@ -170,7 +170,7 @@ public static function isType(string $type, array $typeSchema): bool public static function extractTypeFromRef(string $ref): string { - return str_replace('#/' . JsonSchema::DEFINITIONS . '/', '', $ref); + return \str_replace('#/' . JsonSchema::DEFINITIONS . '/', '', $ref); } public static function assertAllInstanceOfType(array $types): void @@ -179,7 +179,7 @@ public static function assertAllInstanceOfType(array $types): void if (! $type instanceof Type) { throw new \InvalidArgumentException( "Invalid type at key $key. Type must implement Prooph\EventMachine\JsonSchema\Type. Got " - . ((is_object($type) ? get_class($type) : gettype($type)))); + . ((\is_object($type) ? \get_class($type) : \gettype($type)))); } } } diff --git a/src/JsonSchema/JustinRainbowJsonSchemaAssertion.php b/src/JsonSchema/JustinRainbowJsonSchemaAssertion.php index 40be9c9..26d0377 100644 --- a/src/JsonSchema/JustinRainbowJsonSchemaAssertion.php +++ b/src/JsonSchema/JustinRainbowJsonSchemaAssertion.php @@ -23,8 +23,8 @@ public function assert(string $objectName, array $data, array $jsonSchema) $data = new \stdClass(); } - $enforcedObjectData = json_decode(json_encode($data)); - $jsonSchema = json_decode(json_encode($jsonSchema)); + $enforcedObjectData = \json_decode(\json_encode($data)); + $jsonSchema = \json_decode(\json_encode($jsonSchema)); $this->jsonValidator()->validate($enforcedObjectData, $jsonSchema); @@ -34,11 +34,11 @@ public function assert(string $objectName, array $data, array $jsonSchema) $this->jsonValidator()->reset(); foreach ($errors as $i => $error) { - $errors[$i] = sprintf("[%s] %s\n", $error['property'], $error['message']); + $errors[$i] = \sprintf("[%s] %s\n", $error['property'], $error['message']); } throw new \InvalidArgumentException( - "Validation of $objectName failed: " . implode("\n", $errors), + "Validation of $objectName failed: " . \implode("\n", $errors), 400 ); } diff --git a/src/JsonSchema/Type/ArrayType.php b/src/JsonSchema/Type/ArrayType.php index acdbd8c..81ec503 100644 --- a/src/JsonSchema/Type/ArrayType.php +++ b/src/JsonSchema/Type/ArrayType.php @@ -43,7 +43,7 @@ public function __construct(Type $itemSchema, array $validation = null) public function toArray(): array { - return array_merge([ + return \array_merge([ 'type' => $this->type, 'items' => $this->itemSchema->toArray(), ], (array) $this->validation, $this->annotations()); diff --git a/src/JsonSchema/Type/BoolType.php b/src/JsonSchema/Type/BoolType.php index 89606d8..1982bb6 100644 --- a/src/JsonSchema/Type/BoolType.php +++ b/src/JsonSchema/Type/BoolType.php @@ -26,6 +26,6 @@ final class BoolType implements AnnotatedType public function toArray(): array { - return array_merge(['type' => $this->type], $this->annotations()); + return \array_merge(['type' => $this->type], $this->annotations()); } } diff --git a/src/JsonSchema/Type/EmailType.php b/src/JsonSchema/Type/EmailType.php index 47f1d90..fd68fe2 100644 --- a/src/JsonSchema/Type/EmailType.php +++ b/src/JsonSchema/Type/EmailType.php @@ -23,7 +23,7 @@ class EmailType implements AnnotatedType public function toArray(): array { - return array_merge([ + return \array_merge([ 'type' => $this->type, 'format' => 'email', ], $this->annotations()); diff --git a/src/JsonSchema/Type/EnumType.php b/src/JsonSchema/Type/EnumType.php index 2ea9492..b61dea1 100644 --- a/src/JsonSchema/Type/EnumType.php +++ b/src/JsonSchema/Type/EnumType.php @@ -36,7 +36,7 @@ public function __construct(string ...$entries) public function toArray(): array { - return array_merge([ + return \array_merge([ 'type' => $this->type, 'enum' => $this->entries, ], $this->annotations()); diff --git a/src/JsonSchema/Type/FloatType.php b/src/JsonSchema/Type/FloatType.php index c09f491..41e6b6f 100644 --- a/src/JsonSchema/Type/FloatType.php +++ b/src/JsonSchema/Type/FloatType.php @@ -36,7 +36,7 @@ public function __construct(array $validation = null) public function toArray(): array { - return array_merge(['type' => $this->type], (array) $this->validation, $this->annotations()); + return \array_merge(['type' => $this->type], (array) $this->validation, $this->annotations()); } public function withMinimum(float $min): self diff --git a/src/JsonSchema/Type/IntType.php b/src/JsonSchema/Type/IntType.php index b8aaa27..016ddd4 100644 --- a/src/JsonSchema/Type/IntType.php +++ b/src/JsonSchema/Type/IntType.php @@ -36,7 +36,7 @@ public function __construct(array $validation = null) public function toArray(): array { - return array_merge(['type' => $this->type], (array) $this->validation, $this->annotations()); + return \array_merge(['type' => $this->type], (array) $this->validation, $this->annotations()); } public function withMinimum(int $min): self diff --git a/src/JsonSchema/Type/NullableType.php b/src/JsonSchema/Type/NullableType.php index d4d03e5..31c68d2 100644 --- a/src/JsonSchema/Type/NullableType.php +++ b/src/JsonSchema/Type/NullableType.php @@ -21,10 +21,10 @@ public function asNullable(): Type $cp = clone $this; if (! isset($cp->type)) { - throw new \RuntimeException('Type cannot be converted to nullable type. No json schema type set for ' . get_class($this)); + throw new \RuntimeException('Type cannot be converted to nullable type. No json schema type set for ' . \get_class($this)); } - if (! is_string($cp->type)) { + if (! \is_string($cp->type)) { throw new \RuntimeException('Type cannot be converted to nullable type. JSON schema type is not a string'); } diff --git a/src/JsonSchema/Type/ObjectType.php b/src/JsonSchema/Type/ObjectType.php index 30f7163..7401bdd 100644 --- a/src/JsonSchema/Type/ObjectType.php +++ b/src/JsonSchema/Type/ObjectType.php @@ -44,12 +44,12 @@ class ObjectType implements AnnotatedType public function __construct(array $requiredProps = [], array $optionalProps = [], bool $allowAdditionalProperties = false) { - $props = array_merge($requiredProps, $optionalProps); + $props = \array_merge($requiredProps, $optionalProps); JsonSchema::assertAllInstanceOfType($props); $this->properties = $props; - $this->requiredProps = array_keys($requiredProps); + $this->requiredProps = \array_keys($requiredProps); $this->allowAdditionalProps = $allowAdditionalProperties; } @@ -58,7 +58,7 @@ public function withMergedOptionalProps(array $props): self JsonSchema::assertAllInstanceOfType($props); $cp = clone $this; - $cp->properties = array_merge($cp->properties, $props); + $cp->properties = \array_merge($cp->properties, $props); return $cp; } @@ -68,8 +68,8 @@ public function withMergedRequiredProps(array $props): self JsonSchema::assertAllInstanceOfType($props); $cp = clone $this; - $cp->properties = array_merge($cp->properties, $props); - $cp->requiredProps = array_unique(array_merge($cp->requiredProps, array_keys($props))); + $cp->properties = \array_merge($cp->properties, $props); + $cp->requiredProps = \array_unique(\array_merge($cp->requiredProps, \array_keys($props))); return $cp; } @@ -92,7 +92,7 @@ public function withImplementedType(TypeRef $typeRef): self public function toArray(): array { - $allOf = array_map(function (TypeRef $typeRef) { + $allOf = \array_map(function (TypeRef $typeRef) { return $typeRef->toArray(); }, $this->implementedTypes); @@ -100,15 +100,15 @@ public function toArray(): array 'type' => $this->type, 'required' => $this->requiredProps, 'additionalProperties' => $this->allowAdditionalProps, - 'properties' => array_map(function (Type $type) { + 'properties' => \array_map(function (Type $type) { return $type->toArray(); }, $this->properties), ]; - if (count($allOf)) { + if (\count($allOf)) { $schema['allOf'] = $allOf; } - return array_merge($schema, $this->annotations()); + return \array_merge($schema, $this->annotations()); } } diff --git a/src/JsonSchema/Type/StringType.php b/src/JsonSchema/Type/StringType.php index f578e1c..5ca5554 100644 --- a/src/JsonSchema/Type/StringType.php +++ b/src/JsonSchema/Type/StringType.php @@ -49,6 +49,6 @@ public function withPattern(string $pattern): self public function toArray(): array { - return array_merge(['type' => $this->type], $this->validation, $this->annotations()); + return \array_merge(['type' => $this->type], $this->validation, $this->annotations()); } } diff --git a/src/JsonSchema/Type/UuidType.php b/src/JsonSchema/Type/UuidType.php index 156b1f9..3e4ad54 100644 --- a/src/JsonSchema/Type/UuidType.php +++ b/src/JsonSchema/Type/UuidType.php @@ -24,7 +24,7 @@ class UuidType implements AnnotatedType public function toArray(): array { - return array_merge([ + return \array_merge([ 'type' => $this->type, 'pattern' => Uuid::VALID_PATTERN, ], $this->annotations()); diff --git a/src/Messaging/GenericJsonSchemaMessage.php b/src/Messaging/GenericJsonSchemaMessage.php index dbc2c47..ccbde68 100644 --- a/src/Messaging/GenericJsonSchemaMessage.php +++ b/src/Messaging/GenericJsonSchemaMessage.php @@ -41,7 +41,7 @@ protected function setPayload(array $payload): void public function get(string $key) { - if (! array_key_exists($key, $this->payload)) { + if (! \array_key_exists($key, $this->payload)) { throw new \BadMethodCallException("Message payload of {$this->messageName()} does not contain a key $key."); } @@ -50,7 +50,7 @@ public function get(string $key) public function getOrDefault(string $key, $default) { - if (! array_key_exists($key, $this->payload)) { + if (! \array_key_exists($key, $this->payload)) { return $default; } @@ -64,8 +64,17 @@ public function payload(): array public static function assertMessageName(string $messageName) { - if (! preg_match('/^[A-Za-z0-9_.-\/]+$/', $messageName)) { + if (! \preg_match('/^[A-Za-z0-9_.-\/]+$/', $messageName)) { throw new \InvalidArgumentException('Invalid message name.'); } } + + public function withPayload(array $payload, JsonSchemaAssertion $assertion, array $payloadSchema): Message + { + $assertion->assert($this->messageName, $payload, $payloadSchema); + $copy = clone $this; + $copy->payload = $payload; + + return $copy; + } } diff --git a/src/Messaging/GenericJsonSchemaMessageFactory.php b/src/Messaging/GenericJsonSchemaMessageFactory.php index 877348c..f6c372e 100644 --- a/src/Messaging/GenericJsonSchemaMessageFactory.php +++ b/src/Messaging/GenericJsonSchemaMessageFactory.php @@ -13,12 +13,13 @@ use Fig\Http\Message\StatusCodeInterface; use Prooph\Common\Messaging\DomainMessage; -use Prooph\Common\Messaging\Message; -use Prooph\Common\Messaging\MessageFactory; +use Prooph\Common\Messaging\Message as ProophMessage; use Prooph\EventMachine\Commanding\GenericJsonSchemaCommand; use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; +use Prooph\EventMachine\Exception\RuntimeException; use Prooph\EventMachine\JsonSchema\JsonSchemaAssertion; use Prooph\EventMachine\Querying\GenericJsonSchemaQuery; +use Prooph\EventMachine\Runtime\Flavour; use Ramsey\Uuid\Uuid; final class GenericJsonSchemaMessageFactory implements MessageFactory @@ -62,6 +63,11 @@ final class GenericJsonSchemaMessageFactory implements MessageFactory */ private $definitions = []; + /** + * @var Flavour + */ + private $flavour; + public function __construct(array $commandMap, array $eventMap, array $queryMap, array $definitions, JsonSchemaAssertion $jsonSchemaAssertion) { $this->jsonSchemaAssertion = $jsonSchemaAssertion; @@ -75,45 +81,16 @@ public function __construct(array $commandMap, array $eventMap, array $queryMap, /** * {@inheritdoc} */ - public function createMessageFromArray(string $messageName, array $messageData): Message + public function createMessageFromArray(string $messageName, array $messageData): ProophMessage { - $messageType = null; - $payloadSchema = null; - GenericJsonSchemaMessage::assertMessageName($messageName); - if (array_key_exists($messageName, $this->commandMap)) { - $messageType = DomainMessage::TYPE_COMMAND; - $payloadSchema = $this->commandMap[$messageName]; - } - - if ($messageType === null && array_key_exists($messageName, $this->eventMap)) { - $messageType = DomainMessage::TYPE_EVENT; - $payloadSchema = $this->eventMap[$messageName]; - } - - if ($messageType === null && array_key_exists($messageName, $this->queryMap)) { - $messageType = DomainMessage::TYPE_QUERY; - $payloadSchema = $this->queryMap[$messageName]; - } - - if (null === $messageType) { - throw new \RuntimeException( - "Unknown message received. Got message with name: $messageName", - StatusCodeInterface::STATUS_NOT_FOUND - ); - } + [$messageType, $payloadSchema] = $this->getPayloadSchemaAndMessageType($messageName); if (! isset($messageData['payload'])) { $messageData['payload'] = []; } - if (null === $payloadSchema && $messageType === DomainMessage::TYPE_QUERY) { - $payloadSchema = []; - } - - $payloadSchema['definitions'] = $this->definitions; - $this->jsonSchemaAssertion->assert($messageName, $messageData['payload'], $payloadSchema); $messageData['message_name'] = $messageName; @@ -132,11 +109,68 @@ public function createMessageFromArray(string $messageName, array $messageData): switch ($messageType) { case DomainMessage::TYPE_COMMAND: - return GenericJsonSchemaCommand::fromArray($messageData); + $message = GenericJsonSchemaCommand::fromArray($messageData); + break; case DomainMessage::TYPE_EVENT: - return GenericJsonSchemaEvent::fromArray($messageData); + $message = GenericJsonSchemaEvent::fromArray($messageData); + break; case DomainMessage::TYPE_QUERY: - return GenericJsonSchemaQuery::fromArray($messageData); + $message = GenericJsonSchemaQuery::fromArray($messageData); + break; + } + + if ($this->flavour) { + return $this->flavour->convertMessageReceivedFromNetwork($message); } + + return $message; + } + + public function setFlavour(Flavour $flavour): void + { + $this->flavour = $flavour; + } + + public function setPayloadFor(Message $message, array $payload): Message + { + [, $payloadSchema] = $this->getPayloadSchemaAndMessageType($message->messageName()); + + return $message->withPayload($payload, $this->jsonSchemaAssertion, $payloadSchema); + } + + private function getPayloadSchemaAndMessageType(string $messageName): array + { + $payloadSchema = null; + $messageType = null; + + if (\array_key_exists($messageName, $this->commandMap)) { + $messageType = DomainMessage::TYPE_COMMAND; + $payloadSchema = $this->commandMap[$messageName]; + } + + if ($messageType === null && \array_key_exists($messageName, $this->eventMap)) { + $messageType = DomainMessage::TYPE_EVENT; + $payloadSchema = $this->eventMap[$messageName]; + } + + if ($messageType === null && \array_key_exists($messageName, $this->queryMap)) { + $messageType = DomainMessage::TYPE_QUERY; + $payloadSchema = $this->queryMap[$messageName]; + } + + if (null === $messageType) { + throw new RuntimeException( + "Unknown message received. Got message with name: $messageName", + StatusCodeInterface::STATUS_NOT_FOUND + ); + } + + if (null === $payloadSchema && $messageType === DomainMessage::TYPE_QUERY) { + $payloadSchema = []; + } + + $payloadSchema['definitions'] = $this->definitions; + + return [$messageType, $payloadSchema]; } } diff --git a/src/Messaging/Message.php b/src/Messaging/Message.php index 61513a5..6ebf4bb 100644 --- a/src/Messaging/Message.php +++ b/src/Messaging/Message.php @@ -12,8 +12,10 @@ namespace Prooph\EventMachine\Messaging; use Prooph\Common\Messaging\Message as ProophMessage; +use Prooph\EventMachine\JsonSchema\JsonSchemaAssertion; +use Prooph\ServiceBus\Async\AsyncMessage; -interface Message extends ProophMessage +interface Message extends ProophMessage, AsyncMessage { /** * Get $key from message payload @@ -32,4 +34,6 @@ public function get(string $key); * @return mixed */ public function getOrDefault(string $key, $default); + + public function withPayload(array $payload, JsonSchemaAssertion $assertion, array $payloadSchema): self; } diff --git a/src/Messaging/MessageBag.php b/src/Messaging/MessageBag.php new file mode 100644 index 0000000..8bbb6cd --- /dev/null +++ b/src/Messaging/MessageBag.php @@ -0,0 +1,219 @@ + + * + * 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\EventMachine\Messaging; + +use DateTimeImmutable; +use Prooph\Common\Messaging\Message as ProophMessage; +use Prooph\EventMachine\JsonSchema\JsonSchemaAssertion; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +/** + * The MessageBag can be used to pass an arbitrary message through the Event Machine layer + * + * Class MessageBag + * @package Prooph\EventMachine\Messaging + */ +final class MessageBag implements Message +{ + public const MESSAGE = 'message'; + + /** + * @var string + */ + private $messageName; + + /** + * @var string + */ + private $messageType; + + /** + * @var UuidInterface + */ + private $messageId; + + /** + * @var mixed + */ + private $message; + + /** + * @var array + */ + private $metadata; + + /** + * @var \DateTimeImmutable + */ + private $createdAt; + + private $replacedPayload = false; + + private $payload; + + private const MSG_TYPES = [ + Message::TYPE_COMMAND, Message::TYPE_EVENT, Message::TYPE_QUERY, + ]; + + public function __construct(string $messageName, string $messageType, $message, $metadata = [], UuidInterface $messageId = null, DateTimeImmutable $createdAt = null) + { + if (! \in_array($messageType, self::MSG_TYPES)) { + throw new \InvalidArgumentException('Message type should be one of ' . \implode(', ', self::MSG_TYPES) . ". Got $messageType"); + } + + $this->messageName = $messageName; + $this->messageId = $messageId ?? Uuid::uuid4(); + $this->messageType = $messageType; + $this->message = $message; + $this->metadata = $metadata; + $this->createdAt = $createdAt ?? new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + } + + public function messageName(): string + { + return $this->messageName; + } + + public function createdAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function metadata(): array + { + return $this->metadata; + } + + public function version(): int + { + return $this->metadata['_aggregate_version'] ?? 0; + } + + /** + * Get $key from message payload or default in case key does not exist + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getOrDefault(string $key, $default) + { + if ($this->replacedPayload) { + if (! \array_key_exists($key, $this->payload)) { + return $default; + } + + return $this->payload[$key]; + } + + if ($key === self::MESSAGE) { + return $this->message; + } + + return $default; + } + + /** + * Should be one of Message::TYPE_COMMAND, Message::TYPE_EVENT or Message::TYPE_QUERY + */ + public function messageType(): string + { + return $this->messageType; + } + + public function payload(): array + { + if ($this->replacedPayload) { + return $this->payload; + } + + return [self::MESSAGE => \json_decode(\json_encode($this->message), true)]; + } + + /** + * Returns new instance of message with $key => $value added to metadata + * + * Given value must have a scalar or array type. + */ + public function withAddedMetadata(string $key, $value): ProophMessage + { + $copy = clone $this; + $copy->metadata[$key] = $value; + + return $copy; + } + + /** + * Get $key from message payload + * + * @param string $key + * @throws \BadMethodCallException if key does not exist in payload + * @return mixed + */ + public function get(string $key) + { + if ($this->replacedPayload) { + if (! \array_key_exists($key, $this->payload)) { + throw new \BadMethodCallException("Message payload of {$this->messageName()} does not contain a key $key."); + } + + return $this->payload[$key]; + } + + if ($key !== self::MESSAGE) { + throw new \BadMethodCallException(__CLASS__ . ' payload only contains a ' . self::MESSAGE . ' key.'); + } + + return $this->message; + } + + public function hasMessage(): bool + { + return ! $this->replacedPayload; + } + + public function uuid(): UuidInterface + { + return $this->messageId; + } + + public function withMetadata(array $metadata): ProophMessage + { + $copy = clone $this; + $copy->metadata = $metadata; + + return $copy; + } + + public function withMessage($message): MessageBag + { + $copy = clone $this; + $copy->message = $message; + $copy->replacedPayload = false; + $copy->payload = null; + + return $copy; + } + + public function withPayload(array $payload, JsonSchemaAssertion $assertion, array $payloadSchema): Message + { + $assertion->assert($this->messageName, $payload, $payloadSchema); + + $copy = clone $this; + $copy->message = null; + $copy->replacedPayload = true; + $copy->payload = $payload; + + return $copy; + } +} diff --git a/src/Messaging/MessageFactory.php b/src/Messaging/MessageFactory.php new file mode 100644 index 0000000..da7f9eb --- /dev/null +++ b/src/Messaging/MessageFactory.php @@ -0,0 +1,19 @@ + + * + * 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\EventMachine\Messaging; + +use Prooph\Common\Messaging\MessageFactory as ProophMessageFactory; + +interface MessageFactory extends ProophMessageFactory +{ + public function setPayloadFor(Message $message, array $payload): Message; +} diff --git a/src/Messaging/MessageFactoryAware.php b/src/Messaging/MessageFactoryAware.php new file mode 100644 index 0000000..11981db --- /dev/null +++ b/src/Messaging/MessageFactoryAware.php @@ -0,0 +1,17 @@ + + * + * 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\EventMachine\Messaging; + +interface MessageFactoryAware +{ + public function setMessageFactory(MessageFactory $messageFactory): void; +} diff --git a/src/Messaging/MessageProducer.php b/src/Messaging/MessageProducer.php new file mode 100644 index 0000000..c1ece63 --- /dev/null +++ b/src/Messaging/MessageProducer.php @@ -0,0 +1,46 @@ + + * + * 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\EventMachine\Messaging; + +use Prooph\Common\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\ServiceBus\Async\MessageProducer as ProophMessageProducer; +use React\Promise\Deferred; + +final class MessageProducer implements ProophMessageProducer +{ + /** + * @var Flavour + */ + private $flavour; + + /** + * @var + */ + private $proophProducer; + + public function __construct(Flavour $flavour, ProophMessageProducer $producer) + { + $this->flavour = $flavour; + $this->proophProducer = $producer; + } + + /** + * {@inheritdoc} + */ + public function __invoke(Message $message, Deferred $deferred = null): void + { + $message = $this->flavour->prepareNetworkTransmission($message); + + $this->proophProducer->__invoke($message, $deferred); + } +} diff --git a/src/Persistence/DocumentStore/FieldIndex.php b/src/Persistence/DocumentStore/FieldIndex.php index 5817847..554f813 100644 --- a/src/Persistence/DocumentStore/FieldIndex.php +++ b/src/Persistence/DocumentStore/FieldIndex.php @@ -52,7 +52,7 @@ private function __construct( int $sort, bool $unique ) { - if (mb_strlen($field) === 0) { + if (\mb_strlen($field) === 0) { throw new \InvalidArgumentException('Field must not be empty'); } @@ -109,6 +109,6 @@ public function equals($other): bool public function __toString(): string { - return json_encode($this->toArray()); + return \json_encode($this->toArray()); } } diff --git a/src/Persistence/DocumentStore/Filter/InArrayFilter.php b/src/Persistence/DocumentStore/Filter/InArrayFilter.php index ac6c927..db9f099 100644 --- a/src/Persistence/DocumentStore/Filter/InArrayFilter.php +++ b/src/Persistence/DocumentStore/Filter/InArrayFilter.php @@ -55,10 +55,10 @@ public function match(array $doc): bool $prop = $reader->mixedValue($this->prop, self::NOT_SET_PROPERTY); - if (! is_array($prop)) { + if (! \is_array($prop)) { return false; } - return in_array($this->val, $prop); + return \in_array($this->val, $prop); } } diff --git a/src/Persistence/DocumentStore/Filter/LikeFilter.php b/src/Persistence/DocumentStore/Filter/LikeFilter.php index c6bdfb0..afdad15 100644 --- a/src/Persistence/DocumentStore/Filter/LikeFilter.php +++ b/src/Persistence/DocumentStore/Filter/LikeFilter.php @@ -39,7 +39,7 @@ final class LikeFilter implements Filter public function __construct(string $prop, string $val) { - if (strlen($val) === 0) { + if (\strlen($val) === 0) { throw new \InvalidArgumentException('Like filter must not be empty'); } @@ -69,7 +69,7 @@ public function match(array $doc): bool $prop = $reader->mixedValue($this->prop, self::NOT_SET_PROPERTY); - if ($prop === self::NOT_SET_PROPERTY || ! is_string($prop)) { + if ($prop === self::NOT_SET_PROPERTY || ! \is_string($prop)) { return false; } @@ -78,20 +78,20 @@ public function match(array $doc): bool } $likeStart = $this->val[0] === '%'; - $likeEnd = $this->val[mb_strlen($this->val) - 1] === '%'; + $likeEnd = $this->val[\mb_strlen($this->val) - 1] === '%'; - $prop = mb_strtolower($prop); - $val = mb_strtolower($this->val); + $prop = \mb_strtolower($prop); + $val = \mb_strtolower($this->val); if ($likeStart) { - $val = mb_substr($val, 1); + $val = \mb_substr($val, 1); } if ($likeEnd) { - $val = mb_substr($val, 0, mb_strlen($val) - 2); + $val = \mb_substr($val, 0, \mb_strlen($val) - 2); } - $pos = mb_strpos($prop, $val); + $pos = \mb_strpos($prop, $val); if ($pos === false) { return false; @@ -102,7 +102,7 @@ public function match(array $doc): bool } if (! $likeEnd) { - $posRev = mb_strpos(strrev($prop), strrev($val)); + $posRev = \mb_strpos(\strrev($prop), \strrev($val)); if ($posRev !== 0) { return false; diff --git a/src/Persistence/DocumentStore/InMemoryDocumentStore.php b/src/Persistence/DocumentStore/InMemoryDocumentStore.php index 10db3dc..e8b3574 100644 --- a/src/Persistence/DocumentStore/InMemoryDocumentStore.php +++ b/src/Persistence/DocumentStore/InMemoryDocumentStore.php @@ -32,7 +32,7 @@ public function __construct(InMemoryConnection $inMemoryConnection) */ public function listCollections(): array { - return array_keys($this->inMemoryConnection['documents']); + return \array_keys($this->inMemoryConnection['documents']); } /** @@ -41,8 +41,8 @@ public function listCollections(): array */ public function filterCollectionsByPrefix(string $prefix): array { - return array_filter(array_keys($this->inMemoryConnection['documents']), function (string $colName) use ($prefix): bool { - return mb_strpos($colName, $prefix) === 0; + return \array_filter(\array_keys($this->inMemoryConnection['documents']), function (string $colName) use ($prefix): bool { + return \mb_strpos($colName, $prefix) === 0; }); } @@ -52,7 +52,7 @@ public function filterCollectionsByPrefix(string $prefix): array */ public function hasCollection(string $collectionName): bool { - return array_key_exists($collectionName, $this->inMemoryConnection['documents']); + return \array_key_exists($collectionName, $this->inMemoryConnection['documents']); } /** @@ -102,7 +102,7 @@ public function updateDoc(string $collectionName, string $docId, array $docOrSub { $this->assertDocExists($collectionName, $docId); - $this->inMemoryConnection['documents'][$collectionName][$docId] = array_merge( + $this->inMemoryConnection['documents'][$collectionName][$docId] = \array_merge( $this->inMemoryConnection['documents'][$collectionName][$docId], $docOrSubset ); @@ -206,9 +206,9 @@ public function filterDocs( } if ($skip !== null) { - $filteredDocs = array_slice($filteredDocs, $skip, $limit); + $filteredDocs = \array_slice($filteredDocs, $skip, $limit); } elseif ($limit !== null) { - $filteredDocs = array_slice($filteredDocs, 0, $limit); + $filteredDocs = \array_slice($filteredDocs, 0, $limit); } return new \ArrayIterator($filteredDocs); @@ -220,7 +220,7 @@ private function hasDoc(string $collectionName, string $docId): bool return false; } - return array_key_exists($docId, $this->inMemoryConnection['documents'][$collectionName]); + return \array_key_exists($docId, $this->inMemoryConnection['documents'][$collectionName]); } private function assertHasCollection(string $collectionName): void @@ -252,9 +252,9 @@ private function sort(&$docs, DocumentStore\OrderBy\OrderBy $orderBy) return (new ArrayReader($doc))->mixedValue($field); } - throw new \RuntimeException(sprintf( + throw new \RuntimeException(\sprintf( 'Unable to get field from doc: %s. Given OrderBy is neither an instance of %s nor %s', - json_encode($doc), + \json_encode($doc), DocumentStore\OrderBy\Asc::class, DocumentStore\OrderBy\Desc::class )); @@ -272,8 +272,8 @@ private function sort(&$docs, DocumentStore\OrderBy\OrderBy $orderBy) $valA = $getField($docA, $orderBy); $valB = $getField($docB, $orderBy); - if (is_string($valA) && is_string($valB)) { - $orderResult = strcasecmp($valA, $valB); + if (\is_string($valA) && \is_string($valB)) { + $orderResult = \strcasecmp($valA, $valB); } else { $orderResult = $defaultCmp($valA, $valB); } @@ -293,7 +293,7 @@ private function sort(&$docs, DocumentStore\OrderBy\OrderBy $orderBy) return $orderResult; }; - usort($docs, function (array $docA, array $docB) use ($orderBy, $docCmp) { + \usort($docs, function (array $docA, array $docB) use ($orderBy, $docCmp) { return $docCmp($docA, $docB, $orderBy); }); } diff --git a/src/Persistence/DocumentStore/MultiFieldIndex.php b/src/Persistence/DocumentStore/MultiFieldIndex.php index dd807a1..8dc92ff 100644 --- a/src/Persistence/DocumentStore/MultiFieldIndex.php +++ b/src/Persistence/DocumentStore/MultiFieldIndex.php @@ -33,7 +33,7 @@ public static function forFields(array $fieldNames, bool $unique = false): self public static function fromArray(array $data): self { - $fields = array_map(function (string $field): FieldIndex { + $fields = \array_map(function (string $field): FieldIndex { return FieldIndex::forFieldInMultiFieldIndex($field); }, $data['fields'] ?? []); @@ -45,7 +45,7 @@ public static function fromArray(array $data): self private function __construct(bool $unique, FieldIndex ...$fields) { - if (count($fields) <= 1) { + if (\count($fields) <= 1) { throw new \InvalidArgumentException('MultiFieldIndex should contain at least two fields'); } @@ -72,7 +72,7 @@ public function unique(): bool public function toArray(): array { return [ - 'fields' => array_map(function (FieldIndex $field): string { + 'fields' => \array_map(function (FieldIndex $field): string { return $field->field(); }, $this->fields), 'unique' => $this->unique, @@ -90,6 +90,6 @@ public function equals($other): bool public function __toString(): string { - return json_encode($this->toArray()); + return \json_encode($this->toArray()); } } diff --git a/src/Persistence/DocumentStore/OrderBy/AndOrder.php b/src/Persistence/DocumentStore/OrderBy/AndOrder.php index 4e84904..fb152cd 100644 --- a/src/Persistence/DocumentStore/OrderBy/AndOrder.php +++ b/src/Persistence/DocumentStore/OrderBy/AndOrder.php @@ -41,7 +41,7 @@ private function __construct(OrderBy $a, OrderBy $b) if ($this->orderByA instanceof AndOrder) { throw new \InvalidArgumentException( - sprintf( + \sprintf( 'First element of %s must not be again an AndOrderBy. This is only allowed for the alternative element.', __CLASS__ ) @@ -78,12 +78,12 @@ public function equals($other): bool public function __toString(): string { - return json_encode($this->toArray()); + return \json_encode($this->toArray()); } private function orderByToArray(OrderBy $orderBy): array { - switch (get_class($orderBy)) { + switch (\get_class($orderBy)) { case Asc::class: return [ 'type' => self::TYPE_DIRECTION_ASC, @@ -100,7 +100,7 @@ private function orderByToArray(OrderBy $orderBy): array 'data' => $orderBy->toArray(), ]; default: - throw new \RuntimeException('Unknown OrderBy class. Got ' . get_class($orderBy)); + throw new \RuntimeException('Unknown OrderBy class. Got ' . \get_class($orderBy)); } } diff --git a/src/Persistence/DocumentStore/OrderBy/Asc.php b/src/Persistence/DocumentStore/OrderBy/Asc.php index eec619a..fb0a080 100644 --- a/src/Persistence/DocumentStore/OrderBy/Asc.php +++ b/src/Persistence/DocumentStore/OrderBy/Asc.php @@ -37,7 +37,7 @@ public static function fromString(string $field): self private function __construct(string $prop) { - if (strlen($prop) === 0) { + if (\strlen($prop) === 0) { throw new \InvalidArgumentException('Prop must not be an empty string'); } $this->prop = $prop; diff --git a/src/Persistence/DocumentStore/OrderBy/Desc.php b/src/Persistence/DocumentStore/OrderBy/Desc.php index 0d31afd..f08e683 100644 --- a/src/Persistence/DocumentStore/OrderBy/Desc.php +++ b/src/Persistence/DocumentStore/OrderBy/Desc.php @@ -37,7 +37,7 @@ public static function fromString(string $field): self private function __construct(string $prop) { - if (strlen($prop) === 0) { + if (\strlen($prop) === 0) { throw new \InvalidArgumentException('Prop must not be an empty string'); } $this->prop = $prop; diff --git a/src/Persistence/InMemoryEventStore.php b/src/Persistence/InMemoryEventStore.php index 00cf3a1..2bd8df4 100644 --- a/src/Persistence/InMemoryEventStore.php +++ b/src/Persistence/InMemoryEventStore.php @@ -11,7 +11,6 @@ namespace Prooph\EventMachine\Persistence; -use ArrayIterator; use EmptyIterator; use Iterator; use Prooph\Common\Messaging\Message; @@ -23,6 +22,8 @@ use Prooph\EventStore\Metadata\FieldType; use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\Metadata\Operator; +use Prooph\EventStore\StreamIterator\EmptyStreamIterator; +use Prooph\EventStore\StreamIterator\InMemoryStreamIterator; use Prooph\EventStore\StreamName; use Prooph\EventStore\TransactionalEventStore; use Prooph\EventStore\Util\Assertion; @@ -50,11 +51,11 @@ public function create(\Prooph\EventStore\Stream $stream): void throw StreamExistsAlready::with($streamName); } - $pos = strpos($streamNameString, '-'); + $pos = \strpos($streamNameString, '-'); $category = null; if (false !== $pos && $pos > 0) { - $category = substr($streamNameString, 0, $pos); + $category = \substr($streamNameString, 0, $pos); } $this->inMemoryConnection['event_streams'][$streamNameString] = [ @@ -117,10 +118,10 @@ public function load( } if (0 === $found) { - return new EmptyIterator(); + return new EmptyStreamIterator(); } - return new ArrayIterator($streamEvents); + return new InMemoryStreamIterator($streamEvents); } public function loadReverse( @@ -167,10 +168,10 @@ public function loadReverse( } if (0 === $found) { - return new EmptyIterator(); + return new EmptyStreamIterator(); } - return new ArrayIterator($streamEvents); + return new InMemoryStreamIterator($streamEvents); } public function delete(StreamName $streamName): void diff --git a/src/Persistence/Stream.php b/src/Persistence/Stream.php index 7d5b8a5..fce3c1d 100644 --- a/src/Persistence/Stream.php +++ b/src/Persistence/Stream.php @@ -46,11 +46,11 @@ public static function fromArray(array $data): self private function __construct(string $serviceName, string $streamName) { - if (mb_strlen($serviceName) === 0) { + if (\mb_strlen($serviceName) === 0) { throw new \InvalidArgumentException('Service name must not be empty'); } - if (mb_strlen($streamName) === 0) { + if (\mb_strlen($streamName) === 0) { throw new \InvalidArgumentException('Stream name must not be empty'); } @@ -106,6 +106,6 @@ public function equals($other): bool public function __toString(): string { - return json_encode($this->toArray()); + return \json_encode($this->toArray()); } } diff --git a/src/Projecting/AggregateProjector.php b/src/Projecting/AggregateProjector.php index 3538b16..42ed050 100644 --- a/src/Projecting/AggregateProjector.php +++ b/src/Projecting/AggregateProjector.php @@ -12,24 +12,14 @@ namespace Prooph\EventMachine\Projecting; use Prooph\EventMachine\Aggregate\Exception\AggregateNotFound; -use Prooph\EventMachine\Data\DataConverter; -use Prooph\EventMachine\Data\ImmutableRecordDataConverter; use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\Exception\RuntimeException; use Prooph\EventMachine\Messaging\Message; use Prooph\EventMachine\Persistence\DeletableState; use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; -/** - * Note: Only aggregate events of a certain aggregate type can be handled with the projector - * - * Example usage: - * - * $eventMachine->watch(Stream::ofWriteModel()) - * ->with(AggregateProjector::generateProjectionName('My.AR'), AggregateProjector::class) - * ->filterAggregateType('My.AR') - * ->documentQuerySchema(JsonSchema::object(...)) - * - */ final class AggregateProjector implements Projector { /** @@ -48,9 +38,9 @@ final class AggregateProjector implements Projector private $indices; /** - * @var DataConverter + * @var Flavour */ - private $dataConverter; + private $flavour; public static function aggregateCollectionName(string $appVersion, string $aggregateType): string { @@ -64,7 +54,7 @@ public static function generateProjectionName(string $aggregateType): string public static function generateCollectionName(string $appVersion, string $projectionName): string { - return str_replace('.', '_', $projectionName.'_'.$appVersion); + return \str_replace('.', '_', $projectionName.'_'.$appVersion); } public function __construct(DocumentStore $documentStore, EventMachine $eventMachine, DocumentStore\Index ...$indices) @@ -74,17 +64,37 @@ public function __construct(DocumentStore $documentStore, EventMachine $eventMac $this->indices = $indices; } - public function setDataConverter(DataConverter $dataConverter): void + /** + * @TODO Turn Flavour into constructor argument for Event Machine 2.0 + * + * It's not a constructor argument due to BC + * + * @param Flavour $flavour + */ + public function setFlavour(Flavour $flavour): void { - if (null !== $this->dataConverter) { - throw new \BadMethodCallException('Cannot set data converter because another instance is already set.'); + if (null !== $this->flavour) { + throw new RuntimeException('Cannot set another Flavour for ' . __CLASS__ . '. A flavour was already set bevor.'); } - $this->dataConverter = $dataConverter; + $this->flavour = $flavour; + } + + private function flavour(): Flavour + { + if (null === $this->flavour) { + $this->flavour = new PrototypingFlavour(); + } + + return $this->flavour; } public function handle(string $appVersion, string $projectionName, Message $event): void { + if (! $event instanceof Message) { + throw new RuntimeException(__METHOD__ . ' can only handle events of type: ' . Message::class); + } + $aggregateId = $event->metadata()['_aggregate_id'] ?? null; if (! $aggregateId) { @@ -117,7 +127,7 @@ public function handle(string $appVersion, string $projectionName, Message $even $this->documentStore->upsertDoc( $this->generateCollectionName($appVersion, $projectionName), (string) $aggregateId, - $this->convertAggregateStateToArray($aggregateState) + $this->flavour()->convertAggregateStateToArray($aggregateState) ); } @@ -138,7 +148,7 @@ public function deleteReadModel(string $appVersion, string $projectionName): voi private function assertProjectionNameMatchesWithAggregateType(string $projectionName, string $aggregateType): void { if ($projectionName !== self::generateProjectionName($aggregateType)) { - throw new \RuntimeException(sprintf( + throw new \RuntimeException(\sprintf( 'Wrong projection name configured for %s. Should be %s but got %s', __CLASS__, self::generateProjectionName($aggregateType), @@ -146,13 +156,4 @@ private function assertProjectionNameMatchesWithAggregateType(string $projection )); } } - - private function convertAggregateStateToArray($aggregateState): array - { - if (null === $this->dataConverter) { - $this->dataConverter = new ImmutableRecordDataConverter(); - } - - return $this->dataConverter->convertDataToArray($aggregateState); - } } diff --git a/src/Projecting/CustomEventProjector.php b/src/Projecting/CustomEventProjector.php new file mode 100644 index 0000000..f8883df --- /dev/null +++ b/src/Projecting/CustomEventProjector.php @@ -0,0 +1,28 @@ + + * + * 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\EventMachine\Projecting; + +/** + * Interface CustomEventProjector + * + * Similar interface like Projector, but handles mixed $event + * + * @package Prooph\EventMachine\Projecting + */ +interface CustomEventProjector +{ + public function prepareForRun(string $appVersion, string $projectionName): void; + + public function handle(string $appVersion, string $projectionName, $event): void; + + public function deleteReadModel(string $appVersion, string $projectionName): void; +} diff --git a/src/Projecting/InMemory/InMemoryEventStoreProjector.php b/src/Projecting/InMemory/InMemoryEventStoreProjector.php index 2957320..930b466 100644 --- a/src/Projecting/InMemory/InMemoryEventStoreProjector.php +++ b/src/Projecting/InMemory/InMemoryEventStoreProjector.php @@ -20,6 +20,7 @@ use Prooph\EventStore\EventStore; use Prooph\EventStore\EventStoreDecorator; use Prooph\EventStore\Exception; +use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\Projection\ProjectionStatus; use Prooph\EventStore\Projection\Projector; use Prooph\EventStore\Stream; @@ -103,6 +104,11 @@ final class InMemoryEventStoreProjector implements Projector */ private $streamCreated = false; + /** + * @var MetadataMatcher|null + */ + private $metadataMatcher; + public function __construct( EventStore $eventStore, InMemoryConnection $inMemoryConnection, @@ -159,13 +165,14 @@ public function init(Closure $callback): Projector return $this; } - public function fromStream(string $streamName): Projector + public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): Projector { if (null !== $this->query) { throw new Exception\RuntimeException('From was already called'); } $this->query['streams'][] = $streamName; + $this->metadataMatcher = $metadataMatcher; return $this; } @@ -337,7 +344,7 @@ public function run(bool $keepRunning = true): void foreach ($this->streamPositions as $streamName => $position) { try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1); + $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); } catch (Exception\StreamNotFound $e) { // ignore continue; diff --git a/src/Projecting/InMemory/InMemoryEventStoreQuery.php b/src/Projecting/InMemory/InMemoryEventStoreQuery.php index 3a1bbab..493c1de 100644 --- a/src/Projecting/InMemory/InMemoryEventStoreQuery.php +++ b/src/Projecting/InMemory/InMemoryEventStoreQuery.php @@ -19,6 +19,7 @@ use Prooph\EventStore\EventStore; use Prooph\EventStore\EventStoreDecorator; use Prooph\EventStore\Exception; +use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\Projection\Query; use Prooph\EventStore\StreamName; @@ -79,6 +80,11 @@ final class InMemoryEventStoreQuery implements Query */ private $triggerPcntlSignalDispatch; + /** + * @var MetadataMatcher|null + */ + private $metadataMatcher; + public function __construct( EventStore $eventStore, InMemoryConnection $inMemoryConnection, @@ -116,13 +122,14 @@ public function init(Closure $callback): Query return $this; } - public function fromStream(string $streamName): Query + public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): Query { if (null !== $this->query) { throw new Exception\RuntimeException('From was already called'); } $this->query['streams'][] = $streamName; + $this->metadataMatcher = $metadataMatcher; return $this; } @@ -239,7 +246,7 @@ public function run(): void foreach ($this->streamPositions as $streamName => $position) { try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1); + $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); } catch (Exception\StreamNotFound $e) { // ignore continue; diff --git a/src/Projecting/InMemory/InMemoryEventStoreReadModelProjector.php b/src/Projecting/InMemory/InMemoryEventStoreReadModelProjector.php index da9bc85..e70e424 100644 --- a/src/Projecting/InMemory/InMemoryEventStoreReadModelProjector.php +++ b/src/Projecting/InMemory/InMemoryEventStoreReadModelProjector.php @@ -19,6 +19,7 @@ use Prooph\EventStore\EventStore; use Prooph\EventStore\EventStoreDecorator; use Prooph\EventStore\Exception; +use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\Projection\ProjectionStatus; use Prooph\EventStore\Projection\ReadModel; use Prooph\EventStore\Projection\ReadModelProjector; @@ -107,6 +108,11 @@ final class InMemoryEventStoreReadModelProjector implements ReadModelProjector */ private $triggerPcntlSignalDispatch; + /** + * @var MetadataMatcher|null + */ + private $metadataMatcher; + /** * @var array|null */ @@ -176,13 +182,14 @@ public function init(Closure $callback): ReadModelProjector return $this; } - public function fromStream(string $streamName): ReadModelProjector + public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): ReadModelProjector { if (null !== $this->query) { throw new Exception\RuntimeException('From was already called'); } $this->query['streams'][] = $streamName; + $this->metadataMatcher = $metadataMatcher; return $this; } @@ -304,7 +311,7 @@ public function run(bool $keepRunning = true): void foreach ($this->streamPositions as $streamName => $position) { try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1); + $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); } catch (Exception\StreamNotFound $e) { // ignore continue; diff --git a/src/Projecting/ProjectionDescription.php b/src/Projecting/ProjectionDescription.php index 18b6ba6..c273e7a 100644 --- a/src/Projecting/ProjectionDescription.php +++ b/src/Projecting/ProjectionDescription.php @@ -12,7 +12,6 @@ namespace Prooph\EventMachine\Projecting; use Prooph\EventMachine\EventMachine; -use Prooph\EventMachine\JsonSchema\Type; use Prooph\EventMachine\Persistence\Stream; final class ProjectionDescription @@ -48,11 +47,6 @@ final class ProjectionDescription */ private $eventsFilter; - /** - * @var array|null - */ - private $documentSchema; - /** * @var EventMachine */ @@ -66,11 +60,11 @@ public function __construct(Stream $stream, EventMachine $eventMachine) public function with(string $projectionName, string $projectorServiceId): self { - if (mb_strlen($projectionName) === 0) { + if (\mb_strlen($projectionName) === 0) { throw new \InvalidArgumentException('Projection name must not be empty'); } - if (mb_strlen($projectorServiceId) === 0) { + if (\mb_strlen($projectorServiceId) === 0) { throw new \InvalidArgumentException('Projector service id must not be empty'); } @@ -96,7 +90,7 @@ public function filterAggregateType(string $aggregateType): self { $this->assertWithProjectionIsCalled(__METHOD__); - if (mb_strlen($aggregateType) === 0) { + if (\mb_strlen($aggregateType) === 0) { throw new \InvalidArgumentException('Aggregate type filter must not be empty'); } @@ -110,8 +104,8 @@ public function filterEvents(array $listOfEvents): self $this->assertWithProjectionIsCalled(__METHOD__); foreach ($listOfEvents as $event) { - if (! is_string($event)) { - throw new \InvalidArgumentException('Event filter must be a list of event names. Got a ' . (is_object($event) ? get_class($event) : gettype($event))); + if (! \is_string($event)) { + throw new \InvalidArgumentException('Event filter must be a list of event names. Got a ' . (\is_object($event) ? \get_class($event) : \gettype($event))); } } diff --git a/src/Projecting/ProjectionRunner.php b/src/Projecting/ProjectionRunner.php index bce76e2..be36403 100644 --- a/src/Projecting/ProjectionRunner.php +++ b/src/Projecting/ProjectionRunner.php @@ -14,6 +14,7 @@ use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\Messaging\Message; use Prooph\EventMachine\Persistence\Stream; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventStore\Projection\ProjectionManager; use Prooph\EventStore\Projection\ReadModelProjector; @@ -26,6 +27,11 @@ final class ProjectionRunner */ private $projection; + /** + * @var Flavour + */ + private $flavour; + /** * @var bool */ @@ -33,11 +39,12 @@ final class ProjectionRunner public static function eventMachineProjectionName(string $appVersion): string { - return self::EVENT_MACHINE_PROJECTION . '_' . str_replace('.', '_', $appVersion); + return self::EVENT_MACHINE_PROJECTION . '_' . \str_replace('.', '_', $appVersion); } public function __construct( ProjectionManager $projectionManager, + Flavour $flavour, array $projectionDescriptions, EventMachine $eventMachine, array $projectionOptions = null) @@ -48,6 +55,8 @@ public function __construct( ]; } + $this->flavour = $flavour; + $this->testMode = $eventMachine->isTestMode(); $sourceStreams = []; @@ -60,9 +69,9 @@ public function __construct( } } - $sourceStreams = array_keys($sourceStreams); + $sourceStreams = \array_keys($sourceStreams); - $totalSourceStreams = count($sourceStreams); + $totalSourceStreams = \count($sourceStreams); if ($totalSourceStreams === 0) { return; @@ -71,6 +80,7 @@ public function __construct( $this->projection = $projectionManager->createReadModelProjection( self::eventMachineProjectionName($eventMachine->appVersion()), new ReadModelProxy( + $this->flavour, $projectionDescriptions, $eventMachine ), diff --git a/src/Projecting/Projector.php b/src/Projecting/Projector.php index c47b1bb..27d9e8d 100644 --- a/src/Projecting/Projector.php +++ b/src/Projecting/Projector.php @@ -18,7 +18,7 @@ * * A projector should always include the app version in table/collection names. * - * A blue/green deployment strategy is used. + * A blue/green deployment strategy can be used: * This means that the read model for the new app version is built during deployment. * The old read model remains active. In case of a rollback it is still available and can be accessed. * diff --git a/src/Projecting/ReadModel.php b/src/Projecting/ReadModel.php index da7e64f..0cf6172 100644 --- a/src/Projecting/ReadModel.php +++ b/src/Projecting/ReadModel.php @@ -14,6 +14,7 @@ use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\Messaging\Message; use Prooph\EventMachine\Persistence\Stream; +use Prooph\EventMachine\Runtime\Flavour; final class ReadModel { @@ -28,26 +29,32 @@ final class ReadModel private $sourceStream; /** - * @var Projector + * @var Projector|CustomEventProjector */ private $projector; + /** + * @var Flavour + */ + private $flavour; + /** * @var string */ private $appVersion; - public static function fromProjectionDescription(array $desc, EventMachine $eventMachine): ReadModel + public static function fromProjectionDescription(array $desc, Flavour $flavour, EventMachine $eventMachine): ReadModel { $projector = $eventMachine->loadProjector($desc[ProjectionDescription::PROJECTOR_SERVICE_ID]); - return new self($desc, $projector, $eventMachine->appVersion()); + return new self($desc, $projector, $flavour, $eventMachine->appVersion()); } - private function __construct(array $desc, Projector $projector, string $appVersion) + private function __construct(array $desc, $projector, Flavour $flavour, string $appVersion) { $this->desc = $desc; $this->sourceStream = Stream::fromArray($this->desc[ProjectionDescription::SOURCE_STREAM]); + $this->flavour = $flavour; $this->projector = $projector; $this->appVersion = $appVersion; } @@ -71,7 +78,7 @@ public function isInterestedIn(string $sourceStreamName, Message $event): bool } if ($this->desc[ProjectionDescription::EVENTS_FILTER]) { - if (! in_array($event->messageName(), $this->desc[ProjectionDescription::EVENTS_FILTER])) { + if (! \in_array($event->messageName(), $this->desc[ProjectionDescription::EVENTS_FILTER])) { return false; } } @@ -86,7 +93,7 @@ public function prepareForRun(): void public function handle(Message $event): void { - $this->projector->handle($this->appVersion, $this->desc[ProjectionDescription::PROJECTION_NAME], $event); + $this->flavour->callProjector($this->projector, $this->appVersion, $this->desc[ProjectionDescription::PROJECTION_NAME], $event); } public function delete(): void diff --git a/src/Projecting/ReadModelProxy.php b/src/Projecting/ReadModelProxy.php index 2173809..06db06b 100644 --- a/src/Projecting/ReadModelProxy.php +++ b/src/Projecting/ReadModelProxy.php @@ -14,6 +14,7 @@ use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\Messaging\Message; use Prooph\EventMachine\Persistence\Stream; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventStore\Projection\AbstractReadModel; final class ReadModelProxy extends AbstractReadModel @@ -33,10 +34,17 @@ final class ReadModelProxy extends AbstractReadModel */ private $readModels; + /** + * @var Flavour + */ + private $flavour; + public function __construct( + Flavour $flavour, array $projectionDescriptions, EventMachine $eventMachine) { + $this->flavour = $flavour; $this->projectionDescriptions = $projectionDescriptions; $this->eventMachine = $eventMachine; } @@ -58,7 +66,7 @@ public function init(): void $stream = Stream::fromArray($desc[ProjectionDescription::SOURCE_STREAM]); if ($stream->isLocalService()) { - $readModel = ReadModel::fromProjectionDescription($desc, $this->eventMachine); + $readModel = ReadModel::fromProjectionDescription($desc, $this->flavour, $this->eventMachine); $readModel->prepareForRun(); $this->readModels[] = $readModel; } diff --git a/src/Querying/AsyncResolver.php b/src/Querying/AsyncResolver.php new file mode 100644 index 0000000..5a9e609 --- /dev/null +++ b/src/Querying/AsyncResolver.php @@ -0,0 +1,30 @@ + + * + * 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\EventMachine\Querying; + +use React\Promise\Deferred; + +/** + * Interface AsyncResolver + * + * Prooph-like query resolver interface, that can handle a query + * and resolves the passed $deffered instead of returning a result. + * + * @package Prooph\EventMachine\Querying + */ +interface AsyncResolver +{ + /** + * Method is commented out. It only shows the basic idea of the expected __invoke signature + */ + //public function __invoke( $query, Deferred $deferred): void; +} diff --git a/src/Querying/QueryConverterBusPlugin.php b/src/Querying/QueryConverterBusPlugin.php new file mode 100644 index 0000000..7dc9744 --- /dev/null +++ b/src/Querying/QueryConverterBusPlugin.php @@ -0,0 +1,56 @@ + + * + * 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\EventMachine\Querying; + +use Prooph\Common\Event\ActionEvent; +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\ServiceBus\MessageBus; +use Prooph\ServiceBus\Plugin\AbstractPlugin; +use Prooph\ServiceBus\QueryBus; +use React\Promise\Deferred; + +final class QueryConverterBusPlugin extends AbstractPlugin +{ + /** + * @var Flavour + */ + private $flavour; + + public function __construct(Flavour $flavour) + { + $this->flavour = $flavour; + } + + public function attachToMessageBus(MessageBus $messageBus): void + { + if (! $messageBus instanceof QueryBus) { + throw new RuntimeException(__CLASS__ . ' can only be attached to a ' . QueryBus::class); + } + + $this->listenerHandlers[] = $messageBus->attach( + QueryBus::EVENT_DISPATCH, + [$this, 'decorateResolver'], + QueryBus::PRIORITY_INVOKE_HANDLER + 100 + ); + } + + public function decorateResolver(ActionEvent $actionEvent): void + { + $resolver = $actionEvent->getParam(QueryBus::EVENT_PARAM_MESSAGE_HANDLER); + + $actionEvent->setParam(QueryBus::EVENT_PARAM_MESSAGE_HANDLER, function (Message $query, Deferred $deferred) use ($resolver): void { + $this->flavour->callQueryResolver($resolver, $query, $deferred); + }); + } +} diff --git a/src/Querying/QueryDescription.php b/src/Querying/QueryDescription.php index 5f0124f..8e4b176 100644 --- a/src/Querying/QueryDescription.php +++ b/src/Querying/QueryDescription.php @@ -56,9 +56,9 @@ public function __invoke(): array public function resolveWith($resolver): self { - if (! is_string($resolver) && ! is_callable($resolver)) { + if (! \is_string($resolver) && ! \is_callable($resolver)) { throw new \InvalidArgumentException('Resolver should be either a service id string or a callable function. Got ' - . (is_object($resolver) ? get_class($resolver) : gettype($resolver))); + . (\is_object($resolver) ? \get_class($resolver) : \gettype($resolver))); } $this->resolver = $resolver; diff --git a/src/Querying/SyncResolver.php b/src/Querying/SyncResolver.php new file mode 100644 index 0000000..235eaed --- /dev/null +++ b/src/Querying/SyncResolver.php @@ -0,0 +1,28 @@ + + * + * 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\EventMachine\Querying; + +/** + * Interface SyncResolver + * + * Marker interface to tell Event Machine Flavours that the query resolver is blocking and returns a value when invoked. + * + * @package Prooph\EventMachine\Querying + */ +interface SyncResolver +{ + /** + * Method is commented out, because resolvers should be able to type hint a query them self. + * It only shows the expected method signature + */ + //public function __invoke( $query): ; +} diff --git a/src/Runtime/Flavour.php b/src/Runtime/Flavour.php new file mode 100644 index 0000000..784f32e --- /dev/null +++ b/src/Runtime/Flavour.php @@ -0,0 +1,147 @@ + + * + * 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\EventMachine\Runtime; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Projecting\CustomEventProjector; +use Prooph\EventMachine\Projecting\Projector; +use React\Promise\Deferred; + +/** + * Create your own Flavour by implementing the Flavour interface. + * + * With a Flavour you can tell Event Machine how it should communicate with your domain model. + * Check the three available Flavours shipped with Event Machine. If they don't meet your personal + * Flavour, mix and match them or create your very own Flavour. + * + * Interface Flavour + * @package Prooph\EventMachine\Runtime + */ +interface Flavour +{ + /** + * @param Message $command + * @param mixed $preProcessor A callable or object pulled from app container + * @return Message + */ + public function callCommandPreProcessor($preProcessor, Message $command): Message; + + /** + * Invoked by Event Machine after CommandPreProcessor to load aggregate in case it should exist + * + * @param string $aggregateIdPayloadKey + * @param Message $command + * @return string + */ + public function getAggregateIdFromCommand(string $aggregateIdPayloadKey, Message $command): string; + + /** + * @param Message $command + * @param mixed $contextProvider A callable or object pulled from app container + * @return mixed Context that gets passed as argument to corresponding aggregate function + */ + public function callContextProvider($contextProvider, Message $command); + + /** + * An aggregate factory usually starts the lifecycle of an aggregate by producing the first event(s). + * + * @param string $aggregateType + * @param callable $aggregateFunction + * @param Message $command + * @param null|mixed $context + * @return \Generator Message[] yield events + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFunction, Message $command, $context = null): \Generator; + + /** + * Subsequent aggregate functions receive current state of the aggregate as an argument. + * + * In case of the OopFlavour $aggregateState is the aggregate instance itself. Check implementation of the OopFlavour for details. + * + * @param string $aggregateType + * @param callable $aggregateFunction + * @param mixed $aggregateState + * @param Message $command + * @param null|mixed $context + * @return \Generator Message[] yield events + */ + public function callSubsequentAggregateFunction(string $aggregateType, callable $aggregateFunction, $aggregateState, Message $command, $context = null): \Generator; + + /** + * First event apply function does not receive aggregate state as an argument but should return the first version + * of aggregate state derived from the first recorded event. + * + * @param callable $applyFunction + * @param Message $event + * @return mixed New aggregate state + */ + public function callApplyFirstEvent(callable $applyFunction, Message $event); + + /** + * All subsequent apply functions receive aggregate state as an argument and should return a modified version of it. + * + * @param callable $applyFunction + * @param mixed $aggregateState + * @param Message $event + * @return mixed Modified aggregae state + */ + public function callApplySubsequentEvent(callable $applyFunction, $aggregateState, Message $event); + + /** + * Use this hook to convert a custom message decorated by a MessageBag into an Event Machine message (serialize payload) + * + * @param Message $message + * @return Message + */ + public function prepareNetworkTransmission(Message $message): Message; + + /** + * Use this hook to convert an Event Machine message into a custom message and decorate it with a MessageBag + * + * Always invoked after raw message data is deserialized into Event Machine Message: + * + * - EventMachine::dispatch() is called + * - EventMachine::messageFactory()->createMessageFromArray() is called + * + * Create a type safe message from given Event Machine message and put it into a Prooph\EventMachine\Messaging\MessageBag + * to pass it through the Event Machine layer. + * + * Use MessageBag::get(MessageBag::MESSAGE) in call-interceptions to access your type safe message. + * + * It might be important for a Flavour implementation to know that an event is loaded from event store and + * that it is the first event of an aggregate history. + * In this case the flag $firstAggregateEvent is TRUE. + * + * @param Message $message + * @param bool $firstAggregateEvent + * @return Message + */ + public function convertMessageReceivedFromNetwork(Message $message, $firstAggregateEvent = false): Message; + + /** + * @param Projector|CustomEventProjector $projector The projector instance + * @param string $appVersion Configured in Event Machine + * @param string $projectionName Used to register projection in Event Machine + * @param Message $event + */ + public function callProjector($projector, string $appVersion, string $projectionName, Message $event): void; + + /** + * @param mixed $aggregateState + * @return array + */ + public function convertAggregateStateToArray($aggregateState): array; + + public function callEventListener(callable $listener, Message $event): void; + + public function callQueryResolver(callable $resolver, Message $query, Deferred $deferred): void; +} diff --git a/src/Runtime/Functional/Port.php b/src/Runtime/Functional/Port.php new file mode 100644 index 0000000..6f18a0a --- /dev/null +++ b/src/Runtime/Functional/Port.php @@ -0,0 +1,57 @@ + + * + * 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\EventMachine\Runtime\Functional; + +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageBag; + +interface Port +{ + /** + * @param Message $message + * @return mixed The custom message + */ + public function deserialize(Message $message); + + /** + * @param mixed $customMessage + * @return array + */ + public function serializePayload($customMessage): array; + + /** + * @param mixed $customEvent + * @return MessageBag + */ + public function decorateEvent($customEvent): MessageBag; + + /** + * @param string $aggregateIdPayloadKey + * @param mixed $customCommand + * @return string + */ + public function getAggregateIdFromCustomCommand(string $aggregateIdPayloadKey, $customCommand): string; + + /** + * @param mixed $customCommand + * @param mixed $preProcessor Custom preprocessor + * @return mixed Custom message + */ + public function callCommandPreProcessor($customCommand, $preProcessor); + + /** + * @param mixed $customCommand + * @param mixed $contextProvider + * @return mixed + */ + public function callContextProvider($customCommand, $contextProvider); +} diff --git a/src/Runtime/FunctionalFlavour.php b/src/Runtime/FunctionalFlavour.php new file mode 100644 index 0000000..44df5b8 --- /dev/null +++ b/src/Runtime/FunctionalFlavour.php @@ -0,0 +1,305 @@ + + * + * 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\EventMachine\Runtime; + +use Prooph\EventMachine\Data\DataConverter; +use Prooph\EventMachine\Data\ImmutableRecordDataConverter; +use Prooph\EventMachine\Exception\NoGeneratorException; +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageBag; +use Prooph\EventMachine\Messaging\MessageFactory; +use Prooph\EventMachine\Messaging\MessageFactoryAware; +use Prooph\EventMachine\Projecting\AggregateProjector; +use Prooph\EventMachine\Projecting\CustomEventProjector; +use Prooph\EventMachine\Querying\SyncResolver; +use Prooph\EventMachine\Runtime\Functional\Port; +use Prooph\EventMachine\Util\MapIterator; +use React\Promise\Deferred; + +/** + * Class FunctionalFlavour + * + * Similar to the PrototypingFlavour pure aggregate functions + immutable data types are used. + * Once you leave the prototyping or experimentation phase of a project behind, you'll likely want to harden the domain model. + * This includes dedicated command, event and query types. If you find yourself in this situation the FunctionalFlavour + * is for you. All parts of the system that handle messages will receive your own message types when using the + * FunctionalFlavour. + * + * Implement a Functional\Port to map between Event Machine's generic messages and your type-safe counterparts. + * + * @package Prooph\EventMachine\Runtime + */ +final class FunctionalFlavour implements Flavour, MessageFactoryAware +{ + /** + * @var MessageFactory + */ + private $messageFactory; + + /** + * @var Port + */ + private $port; + + /** + * @var DataConverter + */ + private $dataConverter; + + public function __construct(Port $port, DataConverter $dataConverter = null) + { + $this->port = $port; + + if (null === $dataConverter) { + $dataConverter = new ImmutableRecordDataConverter(); + } + + $this->dataConverter = $dataConverter; + } + + public function setMessageFactory(MessageFactory $messageFactory): void + { + $this->messageFactory = $messageFactory; + } + + /** + * {@inheritdoc} + */ + public function callCommandPreProcessor($preProcessor, Message $command): Message + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + return $command->withMessage($this->port->callCommandPreProcessor($command->get(MessageBag::MESSAGE), $preProcessor)); + } + + /** + * {@inheritdoc} + */ + public function getAggregateIdFromCommand(string $aggregateIdPayloadKey, Message $command): string + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + return $this->port->getAggregateIdFromCustomCommand($aggregateIdPayloadKey, $command->get(MessageBag::MESSAGE)); + } + + /** + * {@inheritdoc} + */ + public function callContextProvider($contextProvider, Message $command) + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + return $this->port->callContextProvider($command->get(MessageBag::MESSAGE), $contextProvider); + } + + /** + * {@inheritdoc} + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFunction, Message $command, $context = null): \Generator + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $events = $aggregateFunction($command->get(MessageBag::MESSAGE), $context); + + if (! $events instanceof \Generator) { + throw NoGeneratorException::forAggregateTypeAndCommand($aggregateType, $command); + } + + yield from new MapIterator($events, function ($event) use ($command) { + if (null === $event) { + return null; + } + + return $this->port->decorateEvent($event) + ->withAddedMetadata('_causation_id', $command->uuid()->toString()) + ->withAddedMetadata('_causation_name', $command->messageName()); + }); + } + + /** + * {@inheritdoc} + */ + public function callSubsequentAggregateFunction(string $aggregateType, callable $aggregateFunction, $aggregateState, Message $command, $context = null): \Generator + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $events = $aggregateFunction($aggregateState, $command->get(MessageBag::MESSAGE), $context); + + if (! $events instanceof \Generator) { + throw NoGeneratorException::forAggregateTypeAndCommand($aggregateType, $command); + } + + yield from new MapIterator($events, function ($event) use ($command) { + if (null === $event) { + return null; + } + + return $this->port->decorateEvent($event) + ->withAddedMetadata('_causation_id', $command->uuid()->toString()) + ->withAddedMetadata('_causation_name', $command->messageName()); + }); + } + + /** + * {@inheritdoc} + */ + public function callApplyFirstEvent(callable $applyFunction, Message $event) + { + if (! $event instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + return $applyFunction($event->get(MessageBag::MESSAGE)); + } + + /** + * {@inheritdoc} + */ + public function callApplySubsequentEvent(callable $applyFunction, $aggregateState, Message $event) + { + if (! $event instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + return $applyFunction($aggregateState, $event->get(MessageBag::MESSAGE)); + } + + /** + * {@inheritdoc} + */ + public function prepareNetworkTransmission(Message $message): Message + { + if ($message instanceof MessageBag && $message->hasMessage()) { + $payload = $this->port->serializePayload($message->get(MessageBag::MESSAGE)); + + return $this->messageFactory->setPayloadFor($message, $payload); + } + + return $message; + } + + /** + * {@inheritdoc} + */ + public function convertMessageReceivedFromNetwork(Message $message, $firstAggregateEvent = false): Message + { + if ($message instanceof MessageBag && $message->hasMessage()) { + //Message is already decorated + return $message; + } + + return new MessageBag( + $message->messageName(), + $message->messageType(), + $this->port->deserialize($message), + $message->metadata(), + $message->uuid(), + $message->createdAt() + ); + } + + public function decorateEvent($customEvent): MessageBag + { + return $this->port->decorateEvent($customEvent); + } + + /** + * {@inheritdoc} + */ + public function callProjector($projector, string $appVersion, string $projectionName, Message $event): void + { + if ($projector instanceof AggregateProjector) { + $projector->handle($appVersion, $projectionName, $event); + + return; + } + + if (! $projector instanceof CustomEventProjector) { + throw new RuntimeException(__METHOD__ . ' can only call instances of ' . CustomEventProjector::class); + } + + if (! $event instanceof MessageBag) { + //Normalize event if possible + if ($event instanceof Message) { + $event = $this->port->decorateEvent($this->port->deserialize($event)); + } else { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + } + + //Normalize MessageBag if possible + //MessageBag can contain payload instead of custom event, if projection is called with in-memory recorded event + if (! $event->hasMessage()) { + $event = $this->port->decorateEvent($this->port->deserialize($event)); + } + + $projector->handle($appVersion, $projectionName, $event->get(MessageBag::MESSAGE)); + } + + /** + * @param mixed $aggregateState + * @return array + */ + public function convertAggregateStateToArray($aggregateState): array + { + return $this->dataConverter->convertDataToArray($aggregateState); + } + + public function callEventListener(callable $listener, Message $event): void + { + if (! $event instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + //Normalize MessageBag if possible + ////MessageBag can contain payload instead of custom event, if listener is called with in-memory recorded event + if (! $event->hasMessage()) { + $event = $this->port->decorateEvent($this->port->deserialize($event)); + } + + $listener($event->get(MessageBag::MESSAGE)); + } + + public function callQueryResolver(callable $resolver, Message $query, Deferred $deferred): void + { + if (! $query instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $query = $query->get(MessageBag::MESSAGE); + + if (\is_object($resolver) && $resolver instanceof SyncResolver) { + try { + $result = $resolver($query); + } catch (\Throwable $err) { + $deferred->reject($err); + } + + $deferred->resolve($result); + + return; + } + + $resolver($query, $deferred); + } +} diff --git a/src/Runtime/Oop/AggregateAndEventBag.php b/src/Runtime/Oop/AggregateAndEventBag.php new file mode 100644 index 0000000..529db97 --- /dev/null +++ b/src/Runtime/Oop/AggregateAndEventBag.php @@ -0,0 +1,56 @@ + + * + * 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\EventMachine\Runtime\Oop; + +/** + * Class AggregateAndEventBag + * + * Immutable DTO used by the OopFlavour to pass a newly created aggregate instance together with the first + * event to the first apply method. The DTO is put into a MessageBag, because Event Machine only takes care of events + * produced by aggregate factories. + * + * @package Prooph\EventMachine\Runtime\Oop + */ +final class AggregateAndEventBag +{ + /** + * @var mixed + */ + private $aggregate; + + /** + * @var mixed + */ + private $event; + + public function __construct($aggregate, $event) + { + $this->aggregate = $aggregate; + $this->event = $event; + } + + /** + * @return mixed + */ + public function aggregate() + { + return $this->aggregate; + } + + /** + * @return mixed + */ + public function event() + { + return $this->event; + } +} diff --git a/src/Runtime/Oop/InterceptorHint.php b/src/Runtime/Oop/InterceptorHint.php new file mode 100644 index 0000000..3502ad3 --- /dev/null +++ b/src/Runtime/Oop/InterceptorHint.php @@ -0,0 +1,22 @@ + + * + * 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\EventMachine\Runtime\Oop; + +use Prooph\EventMachine\Runtime\OopFlavour; + +final class InterceptorHint +{ + public static function useAggregate() + { + throw new \BadMethodCallException(__METHOD__ . ' should never be called. Check that EventMachine uses ' . OopFlavour::class); + } +} diff --git a/src/Runtime/Oop/Port.php b/src/Runtime/Oop/Port.php new file mode 100644 index 0000000..ef03a62 --- /dev/null +++ b/src/Runtime/Oop/Port.php @@ -0,0 +1,56 @@ + + * + * 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\EventMachine\Runtime\Oop; + +interface Port +{ + /** + * @param string $aggregateType + * @param callable $aggregateFactory + * @param $customCommand + * @param null|mixed $context + * @return mixed Created aggregate + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFactory, $customCommand, $context = null); + + /** + * @param mixed $aggregate + * @param mixed $customCommand + * @param null|mixed $context + */ + public function callAggregateWithCommand($aggregate, $customCommand, $context = null): void; + + /** + * @param mixed $aggregate + * @return array of custom events + */ + public function popRecordedEvents($aggregate): array; + + /** + * @param mixed $aggregate + * @param mixed $customEvent + */ + public function applyEvent($aggregate, $customEvent): void; + + /** + * @param mixed $aggregate + * @return array + */ + public function serializeAggregate($aggregate): array; + + /** + * @param string $aggregateType + * @param iterable $events history + * @return mixed Aggregate instance + */ + public function reconstituteAggregate(string $aggregateType, iterable $events); +} diff --git a/src/Runtime/OopFlavour.php b/src/Runtime/OopFlavour.php new file mode 100644 index 0000000..0848d4a --- /dev/null +++ b/src/Runtime/OopFlavour.php @@ -0,0 +1,242 @@ + + * + * 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\EventMachine\Runtime; + +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageBag; +use Prooph\EventMachine\Messaging\MessageFactory; +use Prooph\EventMachine\Messaging\MessageFactoryAware; +use Prooph\EventMachine\Runtime\Oop\AggregateAndEventBag; +use Prooph\EventMachine\Runtime\Oop\Port; +use Prooph\EventMachine\Util\DetermineVariableType; +use Prooph\EventMachine\Util\MapIterator; +use React\Promise\Deferred; + +/** + * Class OopFlavour + * + * Event Sourcing can be implemented using either a functional programming approach (pure aggregate functions + immutable data types) + * or an object-oriented approach with stateful aggregates. The latter is supported by the OopFlavour. + * + * Aggregates manage their state internally. Event Machine takes over the rest like history replays and event persistence. + * You can focus on the business logic with a 100% decoupled domain model. + * + * Decoupling is achieved by implementing the Oop\Port tailored to your domain model. + * + * The OopFlavour uses a FunctionalFlavour internally. This is because the OopFlavour also requires type-safe messages. + * + * + * @package Prooph\EventMachine\Runtime + */ +final class OopFlavour implements Flavour, MessageFactoryAware +{ + use DetermineVariableType; + + private const META_AGGREGATE_TYPE = '_aggregate_type'; + + /** + * @var Port + */ + private $port; + + /** + * @var FunctionalFlavour + */ + private $functionalFlavour; + + public function __construct(Port $port, FunctionalFlavour $flavour) + { + $this->port = $port; + $this->functionalFlavour = $flavour; + } + + /** + * {@inheritdoc} + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFunction, Message $command, $context = null): \Generator + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $aggregate = $this->port->callAggregateFactory($aggregateType, $aggregateFunction, $command->get(MessageBag::MESSAGE), $context); + + $events = $this->port->popRecordedEvents($aggregate); + + yield from new MapIterator(new \ArrayIterator($events), function ($event) use ($command, $aggregate, $aggregateType) { + if (null === $event) { + return null; + } + + return $this->functionalFlavour->decorateEvent($event) + ->withMessage(new AggregateAndEventBag($aggregate, $event)) + ->withAddedMetadata('_causation_id', $command->uuid()->toString()) + ->withAddedMetadata('_causation_name', $command->messageName()); + }); + } + + /** + * {@inheritdoc} + */ + public function callSubsequentAggregateFunction(string $aggregateType, callable $aggregateFunction, $aggregateState, Message $command, $context = null): \Generator + { + if (! $command instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $this->port->callAggregateWithCommand($aggregateState, $command->get(MessageBag::MESSAGE), $context); + + $events = $this->port->popRecordedEvents($aggregateState); + + yield from new MapIterator(new \ArrayIterator($events), function ($event) use ($command) { + if (null === $event) { + return null; + } + + return $this->functionalFlavour->decorateEvent($event) + ->withAddedMetadata('_causation_id', $command->uuid()->toString()) + ->withAddedMetadata('_causation_name', $command->messageName()); + }); + } + + /** + * {@inheritdoc} + */ + public function callApplyFirstEvent(callable $applyFunction, Message $event) + { + if (! $event instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $aggregateAndEventBag = $event->get(MessageBag::MESSAGE); + + if (! $aggregateAndEventBag instanceof AggregateAndEventBag) { + throw new RuntimeException('MessageBag passed to ' . __METHOD__ . ' should contain a ' . AggregateAndEventBag::class . ' message.'); + } + + $aggregate = $aggregateAndEventBag->aggregate(); + $event = $aggregateAndEventBag->event(); + + $this->port->applyEvent($aggregate, $event); + + return $aggregate; + } + + public function callApplySubsequentEvent(callable $applyFunction, $aggregateState, Message $event) + { + if (! $event instanceof MessageBag) { + throw new RuntimeException('Message passed to ' . __METHOD__ . ' should be of type ' . MessageBag::class); + } + + $this->port->applyEvent($aggregateState, $event->get(MessageBag::MESSAGE)); + + return $aggregateState; + } + + /** + * {@inheritdoc} + */ + public function callCommandPreProcessor($preProcessor, Message $command): Message + { + return $this->functionalFlavour->callCommandPreProcessor($preProcessor, $command); + } + + /** + * {@inheritdoc} + */ + public function getAggregateIdFromCommand(string $aggregateIdPayloadKey, Message $command): string + { + return $this->functionalFlavour->getAggregateIdFromCommand($aggregateIdPayloadKey, $command); + } + + /** + * {@inheritdoc} + */ + public function callContextProvider($contextProvider, Message $command) + { + return $this->functionalFlavour->callContextProvider($contextProvider, $command); + } + + /** + * {@inheritdoc} + */ + public function prepareNetworkTransmission(Message $message): Message + { + if ($message instanceof MessageBag) { + $innerEvent = $message->getOrDefault(MessageBag::MESSAGE, new \stdClass()); + + if ($innerEvent instanceof AggregateAndEventBag) { + $message = $message->withMessage($innerEvent->event()); + } + } + + return $this->functionalFlavour->prepareNetworkTransmission($message); + } + + /** + * {@inheritdoc} + */ + public function convertMessageReceivedFromNetwork(Message $message, $firstAggregateEvent = false): Message + { + $customMessageInBag = $this->functionalFlavour->convertMessageReceivedFromNetwork($message); + + if ($firstAggregateEvent && $message->messageType() === Message::TYPE_EVENT) { + $aggregateType = $message->metadata()[self::META_AGGREGATE_TYPE] ?? null; + + if (null === $aggregateType) { + throw new RuntimeException('Event passed to ' . __METHOD__ . ' should have a metadata key: ' . self::META_AGGREGATE_TYPE); + } + + if (! $customMessageInBag instanceof MessageBag) { + throw new RuntimeException('FunctionalFlavour is expected to return a ' . MessageBag::class); + } + + $aggregate = $this->port->reconstituteAggregate((string) $aggregateType, [$customMessageInBag->get(MessageBag::MESSAGE)]); + + $customMessageInBag = $customMessageInBag->withMessage(new AggregateAndEventBag($aggregate, $customMessageInBag->get(MessageBag::MESSAGE))); + } + + return $customMessageInBag; + } + + /** + * {@inheritdoc} + */ + public function callProjector($projector, string $appVersion, string $projectionName, Message $event): void + { + $this->functionalFlavour->callProjector($projector, $appVersion, $projectionName, $event); + } + + /** + * {@inheritdoc} + */ + public function convertAggregateStateToArray($aggregateState): array + { + return $this->port->serializeAggregate($aggregateState); + } + + public function setMessageFactory(MessageFactory $messageFactory): void + { + $this->functionalFlavour->setMessageFactory($messageFactory); + } + + public function callEventListener(callable $listener, Message $event): void + { + $this->functionalFlavour->callEventListener($listener, $event); + } + + public function callQueryResolver(callable $resolver, Message $query, Deferred $deferred): void + { + $this->functionalFlavour->callQueryResolver($resolver, $query, $deferred); + } +} diff --git a/src/Runtime/PrototypingFlavour.php b/src/Runtime/PrototypingFlavour.php new file mode 100644 index 0000000..0b4c3f7 --- /dev/null +++ b/src/Runtime/PrototypingFlavour.php @@ -0,0 +1,274 @@ + + * + * 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\EventMachine\Runtime; + +use Prooph\EventMachine\Aggregate\ContextProvider; +use Prooph\EventMachine\Commanding\CommandPreProcessor; +use Prooph\EventMachine\Data\DataConverter; +use Prooph\EventMachine\Data\ImmutableRecordDataConverter; +use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; +use Prooph\EventMachine\Exception\InvalidEventFormatException; +use Prooph\EventMachine\Exception\MissingAggregateIdentifierException; +use Prooph\EventMachine\Exception\NoGeneratorException; +use Prooph\EventMachine\Exception\RuntimeException; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageFactory; +use Prooph\EventMachine\Messaging\MessageFactoryAware; +use Prooph\EventMachine\Projecting\AggregateProjector; +use Prooph\EventMachine\Projecting\Projector; +use Prooph\EventMachine\Querying\SyncResolver; +use Prooph\EventMachine\Util\DetermineVariableType; +use Prooph\EventMachine\Util\MapIterator; +use React\Promise\Deferred; + +/** + * Class PrototypingFlavour + * + * Default Flavour used by Event Machine if no other Flavour is configured. + * + * This Flavour is tailored to rapid prototyping of event sourced domain models. Event Machine passes + * generic messages directly into pure aggregate functions, command preprocessors, context providers and so on. + * + * Aggregate functions can use a short array syntax to describe events that should be recorded by Event Machine. + * Check the tutorial at: https://proophsoftware.github.io/event-machine/tutorial/ + * It uses the PrototypingFlavour. + * + * @package Prooph\EventMachine\Runtime + */ +final class PrototypingFlavour implements Flavour, MessageFactoryAware +{ + use DetermineVariableType; + + /** + * @var MessageFactory + */ + private $messageFactory; + + /** + * @var DataConverter + */ + private $stateConverter; + + public function __construct(DataConverter $dataConverter = null) + { + if (null === $dataConverter) { + $dataConverter = new ImmutableRecordDataConverter(); + } + + $this->stateConverter = $dataConverter; + } + + public function setMessageFactory(MessageFactory $messageFactory): void + { + $this->messageFactory = $messageFactory; + } + + /** + * {@inheritdoc} + */ + public function callCommandPreProcessor($preProcessor, Message $command): Message + { + if (! $preProcessor instanceof CommandPreProcessor) { + throw new RuntimeException( + 'By default a CommandPreProcessor should implement the interface: ' + . CommandPreProcessor::class . '. Got ' . self::getType($preProcessor) + ); + } + + $command = $preProcessor->preProcess($command); + + //@TODO: Remove check after fixing CommandPreProcessor interface in v2.0 + if (! $command instanceof Message) { + //Turn prooph message into Event Machine message (which extends prooph message in v1.0) + $command = $this->messageFactory->createMessageFromArray( + $command->messageName(), + [ + 'uuid' => $command->uuid(), + 'created_at' => $command->createdAt(), + 'payload' => $command->payload(), + 'metadata' => $command->metadata(), + ] + ); + } + + return $command; + } + + public function getAggregateIdFromCommand(string $aggregateIdPayloadKey, Message $command): string + { + $payload = $command->payload(); + + if (! \array_key_exists($aggregateIdPayloadKey, $payload)) { + throw MissingAggregateIdentifierException::inCommand($command, $aggregateIdPayloadKey); + } + + return (string) $payload[$aggregateIdPayloadKey]; + } + + /** + * {@inheritdoc} + */ + public function callContextProvider($contextProvider, Message $command) + { + if (! $contextProvider instanceof ContextProvider) { + throw new RuntimeException( + 'By default a ContextProvider should implement the interface: ' + . ContextProvider::class . '. Got ' . self::getType($contextProvider) + ); + } + + return $contextProvider->provide($command); + } + + /** + * {@inheritdoc} + */ + public function callAggregateFactory(string $aggregateType, callable $aggregateFunction, Message $command, $context = null): \Generator + { + $events = $aggregateFunction($command, $context); + + if (! $events instanceof \Generator) { + throw NoGeneratorException::forAggregateTypeAndCommand($aggregateType, $command); + } + + yield from new MapIterator($events, function ($event) use ($aggregateType, $command) { + if (null === $event) { + return null; + } + + return $this->mapToMessage($event, $aggregateType, $command); + }); + } + + /** + * {@inheritdoc} + */ + public function callSubsequentAggregateFunction(string $aggregateType, callable $aggregateFunction, $aggregateState, Message $command, $context = null): \Generator + { + $events = $aggregateFunction($aggregateState, $command, $context); + + if (! $events instanceof \Generator) { + throw NoGeneratorException::forAggregateTypeAndCommand($aggregateType, $command); + } + + yield from new MapIterator($events, function ($event) use ($aggregateType, $command) { + if (null === $event) { + return null; + } + + return $this->mapToMessage($event, $aggregateType, $command); + }); + } + + /** + * {@inheritdoc} + */ + public function callApplyFirstEvent(callable $applyFunction, Message $event) + { + return $applyFunction($event); + } + + /** + * {@inheritdoc} + */ + public function callApplySubsequentEvent(callable $applyFunction, $aggregateState, Message $event) + { + return $applyFunction($aggregateState, $event); + } + + /** + * {@inheritdoc} + */ + public function prepareNetworkTransmission(Message $message): Message + { + return $message; + } + + /** + * {@inheritdoc} + */ + public function convertMessageReceivedFromNetwork(Message $message, $firstAggregateEvent = false): Message + { + return $message; + } + + /** + * {@inheritdoc} + */ + public function callProjector($projector, string $appVersion, string $projectionName, Message $event): void + { + if (! $projector instanceof Projector && ! $projector instanceof AggregateProjector) { + throw new RuntimeException(__METHOD__ . ' can only call instances of ' . Projector::class); + } + + $projector->handle($appVersion, $projectionName, $event); + } + + /** + * {@inheritdoc} + */ + public function convertAggregateStateToArray($aggregateState): array + { + return $this->stateConverter->convertDataToArray($aggregateState); + } + + public function callEventListener(callable $listener, Message $event): void + { + $listener($event); + } + + public function callQueryResolver(callable $resolver, Message $query, Deferred $deferred): void + { + if (\is_object($resolver) && $resolver instanceof SyncResolver) { + try { + $result = $resolver($query); + } catch (\Throwable $err) { + $deferred->reject($err); + } + + $deferred->resolve($result); + + return; + } + + $resolver($query, $deferred); + } + + private function mapToMessage($event, string $aggregateType, Message $command): Message + { + if (! \is_array($event) || ! \array_key_exists(0, $event) || ! \array_key_exists(1, $event) + || ! \is_string($event[0]) || ! \is_array($event[1])) { + throw InvalidEventFormatException::invalidEvent($aggregateType, $command); + } + [$eventName, $payload] = $event; + + $metadata = []; + + if (\array_key_exists(2, $event)) { + $metadata = $event[2]; + if (! \is_array($metadata)) { + throw InvalidEventFormatException::invalidMetadata($metadata, $aggregateType, $command); + } + } + + /** @var GenericJsonSchemaEvent $event */ + $event = $this->messageFactory->createMessageFromArray($eventName, [ + 'payload' => $payload, + 'metadata' => \array_merge([ + '_causation_id' => $command->uuid()->toString(), + '_causation_name' => $command->messageName(), + ], $metadata), + ]); + + return $event; + } +} diff --git a/src/Util/DetermineVariableType.php b/src/Util/DetermineVariableType.php new file mode 100644 index 0000000..9b416e0 --- /dev/null +++ b/src/Util/DetermineVariableType.php @@ -0,0 +1,20 @@ + + * + * 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\EventMachine\Util; + +trait DetermineVariableType +{ + private static function getType($var): string + { + return \is_object($var) ? \get_class($var) : \gettype($var); + } +} diff --git a/src/Util/MapIterator.php b/src/Util/MapIterator.php new file mode 100644 index 0000000..b24bb3a --- /dev/null +++ b/src/Util/MapIterator.php @@ -0,0 +1,33 @@ + + * + * 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\EventMachine\Util; + +use Traversable; + +final class MapIterator extends \IteratorIterator +{ + /** + * @var callable + */ + private $callback; + + public function __construct(Traversable $iterator, callable $callback) + { + parent::__construct($iterator); + $this->callback = $callback; + } + + public function current() + { + return \call_user_func($this->callback, parent::current()); + } +} diff --git a/tests/Aggregate/GenericAggregateRootTest.php b/tests/Aggregate/GenericAggregateRootTest.php index 9ba5226..c12bc34 100644 --- a/tests/Aggregate/GenericAggregateRootTest.php +++ b/tests/Aggregate/GenericAggregateRootTest.php @@ -16,12 +16,26 @@ use Prooph\EventMachine\Aggregate\GenericAggregateRoot; use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; use Prooph\EventMachine\JsonSchema\JsonSchema; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; use Prooph\EventMachineTest\BasicTestCase; use Prooph\EventSourcing\Aggregate\AggregateType; use Ramsey\Uuid\Uuid; class GenericAggregateRootTest extends BasicTestCase { + /** + * @var Flavour + */ + private $flavour; + + protected function setUp() + { + parent::setUp(); + $this->flavour = new PrototypingFlavour(); + $this->flavour->setMessageFactory($this->getMockedEventMessageFactory()); + } + /** * @test */ @@ -42,7 +56,7 @@ public function it_records_events_and_can_be_reconstituted_by_them() $arId = Uuid::uuid4()->toString(); - $user = new GenericAggregateRoot($arId, AggregateType::fromString('User'), $eventApplyMap); + $user = new GenericAggregateRoot($arId, AggregateType::fromString('User'), $eventApplyMap, $this->flavour); $userWasRegistered = new GenericJsonSchemaEvent( 'UserWasRegistered', @@ -68,7 +82,7 @@ public function it_records_events_and_can_be_reconstituted_by_them() self::assertCount(2, $recordedEvents); - $translator = new ClosureAggregateTranslator($arId, $eventApplyMap); + $translator = new ClosureAggregateTranslator($arId, $eventApplyMap, $this->flavour); $sameUser = $translator->reconstituteAggregateFromHistory(AggregateType::fromString('User'), new \ArrayIterator([$recordedEvents[0]])); diff --git a/tests/BasicTestCase.php b/tests/BasicTestCase.php index 66d6fa4..bc6b858 100644 --- a/tests/BasicTestCase.php +++ b/tests/BasicTestCase.php @@ -12,13 +12,14 @@ namespace Prooph\EventMachineTest; use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\MessageFactory; use Prooph\EventMachine\Aggregate\ClosureAggregateTranslator; use Prooph\EventMachine\Aggregate\GenericAggregateRoot; use Prooph\EventMachine\Commanding\GenericJsonSchemaCommand; use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; use Prooph\EventMachine\JsonSchema\JsonSchemaAssertion; use Prooph\EventMachine\JsonSchema\JustinRainbowJsonSchemaAssertion; +use Prooph\EventMachine\Messaging\MessageFactory; +use Prooph\EventMachine\Runtime\PrototypingFlavour; use Prophecy\Argument; class BasicTestCase extends TestCase @@ -44,7 +45,13 @@ class BasicTestCase extends TestCase */ protected function extractRecordedEvents(GenericAggregateRoot $aggregateRoot): array { - $aggregateRootTranslator = new ClosureAggregateTranslator('unknown', []); + $interceptor = new PrototypingFlavour(); + $interceptor->setMessageFactory($this->getMockedEventMessageFactory()); + $aggregateRootTranslator = new ClosureAggregateTranslator( + 'unknown', + [], + $interceptor + ); return $aggregateRootTranslator->extractPendingStreamEvents($aggregateRoot); } diff --git a/tests/Commanding/CommandProcessorTest.php b/tests/Commanding/CommandProcessorTest.php index 4c9bac6..5766983 100644 --- a/tests/Commanding/CommandProcessorTest.php +++ b/tests/Commanding/CommandProcessorTest.php @@ -16,23 +16,37 @@ use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; use Prooph\EventMachine\EventMachine; use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; use Prooph\EventMachineTest\Aggregate\Stub\ContextAwareAggregateDescription; use Prooph\EventMachineTest\BasicTestCase; use Prooph\EventStore\EventStore; use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\StreamName; -use ProophExample\Aggregate\Aggregate; -use ProophExample\Aggregate\CacheableUserDescription; -use ProophExample\Aggregate\UserDescription; -use ProophExample\Messaging\Command; -use ProophExample\Messaging\Event; -use ProophExample\Messaging\MessageDescription; +use ProophExample\PrototypingFlavour\Aggregate\Aggregate; +use ProophExample\PrototypingFlavour\Aggregate\CacheableUserDescription; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Messaging\Command; +use ProophExample\PrototypingFlavour\Messaging\Event; +use ProophExample\PrototypingFlavour\Messaging\MessageDescription; use Prophecy\Argument; use Psr\Container\ContainerInterface; use Ramsey\Uuid\Uuid; final class CommandProcessorTest extends BasicTestCase { + /** + * @var Flavour + */ + private $flavour; + + protected function setUp() + { + parent::setUp(); + $this->flavour = new PrototypingFlavour(); + $this->flavour->setMessageFactory($this->getMockedEventMessageFactory()); + } + /** * @test */ @@ -57,7 +71,7 @@ public function it_processes_command_that_creates_new_aggregate() $eventStore = $this->prophesize(EventStore::class); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting[Command::REGISTER_USER]; @@ -65,6 +79,7 @@ public function it_processes_command_that_creates_new_aggregate() $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal() ); @@ -140,7 +155,7 @@ public function it_processes_command_with_existing_aggregate() }); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting[Command::CHANGE_USERNAME]; @@ -148,6 +163,7 @@ public function it_processes_command_with_existing_aggregate() $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal() ); @@ -196,7 +212,7 @@ public function it_prcoesses_alternative_event_recording() $eventStore = $this->prophesize(EventStore::class); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting[Command::REGISTER_USER]; @@ -204,6 +220,7 @@ public function it_prcoesses_alternative_event_recording() $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal() ); @@ -281,7 +298,7 @@ public function it_does_nothing_if_aggregate_function_yields_null_to_indicate_th }); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting[Command::DO_NOTHING]; @@ -289,6 +306,7 @@ public function it_does_nothing_if_aggregate_function_yields_null_to_indicate_th $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal() ); @@ -325,7 +343,7 @@ public function it_provides_context_using_context_provider() $eventStore = $this->prophesize(EventStore::class); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting['AddCAA']; @@ -341,6 +359,7 @@ public function it_provides_context_using_context_provider() $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal(), null, @@ -385,7 +404,7 @@ public function it_adds_additional_metadata_to_event() $eventStore = $this->prophesize(EventStore::class); $eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $processorDesc = $commandRouting[Command::REGISTER_USER]; @@ -395,13 +414,14 @@ public function it_adds_additional_metadata_to_event() //Wrap ar function to add additional metadata for this test $processorDesc['aggregateFunction'] = function (Message $registerUser) use ($arFunc): \Generator { - [$event] = iterator_to_array($arFunc($registerUser)); + [$event] = \iterator_to_array($arFunc($registerUser)); [$eventName, $payload] = $event; yield [$eventName, $payload, ['additional' => 'metadata']]; }; $commandProcessor = CommandProcessor::fromDescriptionArrayAndDependencies( $processorDesc, + $this->flavour, $this->getMockedEventMessageFactory(), $eventStore->reveal() ); diff --git a/tests/Commanding/CommandToProcessorRouterTest.php b/tests/Commanding/CommandToProcessorRouterTest.php index ca1b657..b21e8c5 100644 --- a/tests/Commanding/CommandToProcessorRouterTest.php +++ b/tests/Commanding/CommandToProcessorRouterTest.php @@ -17,6 +17,8 @@ use Prooph\EventMachine\Commanding\CommandProcessor; use Prooph\EventMachine\Commanding\CommandToProcessorRouter; use Prooph\EventMachine\Container\ContextProviderFactory; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; use Prooph\EventMachineTest\BasicTestCase; use Prooph\EventStore\EventStore; use Prooph\ServiceBus\MessageBus; @@ -25,6 +27,18 @@ final class CommandToProcessorRouterTest extends BasicTestCase { + /** + * @var Flavour + */ + private $flavour; + + protected function setUp() + { + parent::setUp(); + $this->flavour = new PrototypingFlavour(); + $this->flavour->setMessageFactory($this->getMockedEventMessageFactory()); + } + /** * @test */ @@ -66,6 +80,7 @@ public function it_sets_command_processor_as_command_handler() $messageFactory->reveal(), $eventStore->reveal(), $contextProviderFactory->reveal(), + $this->flavour, $snapshotStore->reveal() ); diff --git a/tests/Container/ReflectionBasedContainerTest.php b/tests/Container/ReflectionBasedContainerTest.php index 2451ec5..6988155 100644 --- a/tests/Container/ReflectionBasedContainerTest.php +++ b/tests/Container/ReflectionBasedContainerTest.php @@ -20,8 +20,8 @@ use Prooph\EventMachine\JsonSchema\JustinRainbowJsonSchemaAssertion; use Prooph\EventMachine\Messaging\GenericJsonSchemaMessageFactory; use Prooph\EventMachineTest\BasicTestCase; -use ProophExample\Aggregate\UserDescription; -use ProophExample\Messaging\Command; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Messaging\Command; final class ReflectionBasedContainerTest extends BasicTestCase { diff --git a/tests/Data/ImmutableRecordTest.php b/tests/Data/ImmutableRecordTest.php index b66b737..74d801e 100644 --- a/tests/Data/ImmutableRecordTest.php +++ b/tests/Data/ImmutableRecordTest.php @@ -211,10 +211,10 @@ public function it_calls_from_float_when_from_int_does_not_exist($two) $this->assertEquals(2.0, $amount); $this->assertInternalType('float', $amount); - $json = json_encode($productPrice->toArray()); + $json = \json_encode($productPrice->toArray()); $this->assertEquals('{"amount":2,"currency":"EUR"}', $json); - $productPrice = TestProductPriceVO::fromArray(json_decode($json, true)); + $productPrice = TestProductPriceVO::fromArray(\json_decode($json, true)); $amount = $productPrice->toArray()['amount']; $this->assertEquals(2.0, $amount); $this->assertInternalType('float', $amount); @@ -238,7 +238,6 @@ public function it_validates_float_type_when_input_is_integer($two) $this->assertInternalType('float', $amount); } - /** * @test */ diff --git a/tests/Data/Stubs/TestIdentityCollectionVO.php b/tests/Data/Stubs/TestIdentityCollectionVO.php index cb29825..5a9126a 100644 --- a/tests/Data/Stubs/TestIdentityCollectionVO.php +++ b/tests/Data/Stubs/TestIdentityCollectionVO.php @@ -25,7 +25,7 @@ public static function fromIdentities(TestIdentityVO ...$identities): self public static function fromArray(array $data): self { - $identities = array_map(function ($item) { + $identities = \array_map(function ($item) { return TestIdentityVO::fromArray($item); }, $data); @@ -44,7 +44,7 @@ public function first(): TestIdentityVO public function toArray(): array { - return array_map(function (TestIdentityVO $item) { + return \array_map(function (TestIdentityVO $item) { return $item->toArray(); }, $this->identities); } @@ -60,6 +60,6 @@ public function equals($other): bool public function __toString(): string { - return json_encode($this->toArray()); + return \json_encode($this->toArray()); } } diff --git a/tests/EventMachineFunctionalFlavourTest.php b/tests/EventMachineFunctionalFlavourTest.php new file mode 100644 index 0000000..8df676b --- /dev/null +++ b/tests/EventMachineFunctionalFlavourTest.php @@ -0,0 +1,66 @@ + + * + * 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\EventMachineTest; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\Messaging\MessageDispatcher; +use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\FunctionalFlavour; +use ProophExample\FunctionalFlavour\Aggregate\UserDescription; +use ProophExample\FunctionalFlavour\Aggregate\UserState; +use ProophExample\FunctionalFlavour\Api\MessageDescription; +use ProophExample\FunctionalFlavour\ExampleFunctionalPort; +use ProophExample\FunctionalFlavour\ProcessManager\SendWelcomeEmail; +use ProophExample\FunctionalFlavour\Projector\RegisteredUsersProjector; +use ProophExample\FunctionalFlavour\Resolver\GetUserResolver; +use ProophExample\FunctionalFlavour\Resolver\GetUsersResolver; + +class EventMachineFunctionalFlavourTest extends EventMachineTestAbstract +{ + protected function loadEventMachineDescriptions(EventMachine $eventMachine) + { + $eventMachine->load(MessageDescription::class); + $eventMachine->load(UserDescription::class); + } + + protected function getFlavour(): Flavour + { + return new FunctionalFlavour(new ExampleFunctionalPort()); + } + + protected function getRegisteredUsersProjector(DocumentStore $documentStore) + { + return new RegisteredUsersProjector($documentStore); + } + + protected function getUserRegisteredListener(MessageDispatcher $messageDispatcher) + { + return new SendWelcomeEmail($messageDispatcher); + } + + protected function getUserResolver(array $cachedUserState): callable + { + return new GetUserResolver($cachedUserState); + } + + protected function getUsersResolver(array $cachedUsers): callable + { + return new GetUsersResolver($cachedUsers); + } + + protected function assertLoadedUserState($userState): void + { + self::assertInstanceOf(UserState::class, $userState); + self::assertEquals('Tester', $userState->username); + } +} diff --git a/tests/EventMachineOopFlavourTest.php b/tests/EventMachineOopFlavourTest.php new file mode 100644 index 0000000..a2080d3 --- /dev/null +++ b/tests/EventMachineOopFlavourTest.php @@ -0,0 +1,71 @@ + + * + * 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\EventMachineTest; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\Messaging\MessageDispatcher; +use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\FunctionalFlavour; +use Prooph\EventMachine\Runtime\OopFlavour; +use ProophExample\FunctionalFlavour\Api\MessageDescription; +use ProophExample\FunctionalFlavour\ExampleFunctionalPort; +use ProophExample\FunctionalFlavour\ProcessManager\SendWelcomeEmail; +use ProophExample\FunctionalFlavour\Projector\RegisteredUsersProjector; +use ProophExample\FunctionalFlavour\Resolver\GetUserResolver; +use ProophExample\FunctionalFlavour\Resolver\GetUsersResolver; +use ProophExample\OopFlavour\Aggregate\User; +use ProophExample\OopFlavour\Aggregate\UserDescription; +use ProophExample\OopFlavour\ExampleOopPort; + +class EventMachineOopFlavourTest extends EventMachineTestAbstract +{ + protected function loadEventMachineDescriptions(EventMachine $eventMachine) + { + $eventMachine->load(MessageDescription::class); + $eventMachine->load(UserDescription::class); + } + + protected function getFlavour(): Flavour + { + return new OopFlavour( + new ExampleOopPort(), + new FunctionalFlavour(new ExampleFunctionalPort()) + ); + } + + protected function getRegisteredUsersProjector(DocumentStore $documentStore) + { + return new RegisteredUsersProjector($documentStore); + } + + protected function getUserRegisteredListener(MessageDispatcher $messageDispatcher) + { + return new SendWelcomeEmail($messageDispatcher); + } + + protected function getUserResolver(array $cachedUserState): callable + { + return new GetUserResolver($cachedUserState); + } + + protected function getUsersResolver(array $cachedUsers): callable + { + return new GetUsersResolver($cachedUsers); + } + + protected function assertLoadedUserState($userState): void + { + self::assertInstanceOf(User::class, $userState); + self::assertEquals('Tester', $userState->toArray()['username']); + } +} diff --git a/tests/EventMachinePrototypingFlavourTest.php b/tests/EventMachinePrototypingFlavourTest.php new file mode 100644 index 0000000..06739f5 --- /dev/null +++ b/tests/EventMachinePrototypingFlavourTest.php @@ -0,0 +1,86 @@ + + * + * 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\EventMachineTest; + +use Prooph\EventMachine\EventMachine; +use Prooph\EventMachine\Messaging\MessageDispatcher; +use Prooph\EventMachine\Persistence\DocumentStore; +use Prooph\EventMachine\Runtime\Flavour; +use Prooph\EventMachine\Runtime\PrototypingFlavour; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Aggregate\UserState; +use ProophExample\PrototypingFlavour\Messaging\MessageDescription; +use ProophExample\PrototypingFlavour\ProcessManager\SendWelcomeEmail; +use ProophExample\PrototypingFlavour\Projector\RegisteredUsersProjector; +use ProophExample\PrototypingFlavour\Resolver\GetUserResolver; +use ProophExample\PrototypingFlavour\Resolver\GetUsersResolver; +use Psr\Container\ContainerInterface; + +class EventMachinePrototypingFlavourTest extends EventMachineTestAbstract +{ + protected function loadEventMachineDescriptions(EventMachine $eventMachine) + { + $eventMachine->load(MessageDescription::class); + $eventMachine->load(UserDescription::class); + } + + protected function getFlavour(): Flavour + { + return new PrototypingFlavour(); + } + + protected function getRegisteredUsersProjector(DocumentStore $documentStore) + { + return new RegisteredUsersProjector($documentStore); + } + + protected function getUserRegisteredListener(MessageDispatcher $messageDispatcher) + { + return new SendWelcomeEmail($messageDispatcher); + } + + protected function getUserResolver(array $cachedUserState): callable + { + return new GetUserResolver($cachedUserState); + } + + protected function getUsersResolver(array $cachedUsers): callable + { + return new GetUsersResolver($cachedUsers); + } + + protected function assertLoadedUserState($userState): void + { + self::assertInstanceOf(UserState::class, $userState); + self::assertEquals('Tester', $userState->username); + } + + /** + * @test + */ + public function it_throws_exception_if_config_should_be_cached_but_contains_closures() + { + $eventMachine = new EventMachine(); + + $eventMachine->load(MessageDescription::class); + $eventMachine->load(UserDescription::class); + + $container = $this->prophesize(ContainerInterface::class); + + $eventMachine->initialize($container->reveal()); + + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('At least one EventMachineDescription contains a Closure and is therefor not cacheable!'); + + $eventMachine->compileCacheableConfig(); + } +} diff --git a/tests/EventMachineTest.php b/tests/EventMachineTestAbstract.php similarity index 71% rename from tests/EventMachineTest.php rename to tests/EventMachineTestAbstract.php index cce1e79..18c875e 100644 --- a/tests/EventMachineTest.php +++ b/tests/EventMachineTestAbstract.php @@ -12,8 +12,7 @@ namespace Prooph\EventMachineTest; use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\Common\Messaging\Message; -use Prooph\EventMachine\Commanding\GenericJsonSchemaCommand; +use Prooph\Common\Messaging\Message as ProophMessage; use Prooph\EventMachine\Container\ContainerChain; use Prooph\EventMachine\Container\EventMachineContainer; use Prooph\EventMachine\Eventing\GenericJsonSchemaEvent; @@ -25,17 +24,22 @@ use Prooph\EventMachine\JsonSchema\Type\EnumType; use Prooph\EventMachine\JsonSchema\Type\StringType; use Prooph\EventMachine\JsonSchema\Type\UuidType; +use Prooph\EventMachine\Messaging\Message; +use Prooph\EventMachine\Messaging\MessageBag; +use Prooph\EventMachine\Messaging\MessageDispatcher; +use Prooph\EventMachine\Messaging\MessageFactoryAware; use Prooph\EventMachine\Persistence\DocumentStore; -use Prooph\EventMachine\Persistence\DocumentStore\InMemoryDocumentStore; use Prooph\EventMachine\Persistence\InMemoryConnection; use Prooph\EventMachine\Persistence\InMemoryEventStore; use Prooph\EventMachine\Persistence\Stream; use Prooph\EventMachine\Persistence\TransactionManager; use Prooph\EventMachine\Projecting\AggregateProjector; use Prooph\EventMachine\Projecting\InMemory\InMemoryProjectionManager; +use Prooph\EventMachine\Runtime\Flavour; use Prooph\EventMachineTest\Data\Stubs\TestIdentityVO; use Prooph\EventStore\ActionEventEmitterEventStore; use Prooph\EventStore\EventStore; +use Prooph\EventStore\Metadata\MetadataMatcher; use Prooph\EventStore\StreamName; use Prooph\EventStore\TransactionalActionEventEmitterEventStore; use Prooph\EventStore\TransactionalEventStore; @@ -43,25 +47,36 @@ use Prooph\ServiceBus\CommandBus; use Prooph\ServiceBus\EventBus; use Prooph\ServiceBus\QueryBus; -use ProophExample\Aggregate\Aggregate; -use ProophExample\Aggregate\CacheableUserDescription; -use ProophExample\Aggregate\UserDescription; -use ProophExample\Aggregate\UserState; -use ProophExample\Messaging\Command; -use ProophExample\Messaging\Event; -use ProophExample\Messaging\MessageDescription; -use ProophExample\Messaging\Query; -use ProophExample\Resolver\GetUserResolver; -use ProophExample\Resolver\GetUsersResolver; +use ProophExample\FunctionalFlavour\Api\Command; +use ProophExample\FunctionalFlavour\Api\Event; +use ProophExample\FunctionalFlavour\Api\Query; +use ProophExample\FunctionalFlavour\Event\UsernameChanged; +use ProophExample\FunctionalFlavour\Event\UserRegistered; +use ProophExample\OopFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Aggregate\Aggregate; use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use Ramsey\Uuid\Uuid; -use React\Promise\Deferred; -class EventMachineTest extends BasicTestCase +abstract class EventMachineTestAbstract extends BasicTestCase { + abstract protected function loadEventMachineDescriptions(EventMachine $eventMachine); + + abstract protected function getFlavour(): Flavour; + + abstract protected function getRegisteredUsersProjector(DocumentStore $documentStore); + + abstract protected function getUserRegisteredListener(MessageDispatcher $messageDispatcher); + + abstract protected function getUserResolver(array $cachedUserState): callable; + + abstract protected function getUsersResolver(array $cachedUsers): callable; + + abstract protected function assertLoadedUserState($userState): void; + /** - * @var EventStore + * @var ObjectProphecy */ private $eventStore; @@ -111,12 +126,16 @@ class EventMachineTest extends BasicTestCase */ private $inMemoryConnection; + /** + * @var Flavour + */ + private $flavour; + protected function setUp() { $this->eventMachine = new EventMachine(); - $this->eventMachine->load(MessageDescription::class); - $this->eventMachine->load(CacheableUserDescription::class); + $this->loadEventMachineDescriptions($this->eventMachine); $this->eventStore = $this->prophesize(EventStore::class); @@ -128,6 +147,7 @@ protected function setUp() $this->eventBus = new EventBus(); $this->queryBus = new QueryBus(); $this->inMemoryConnection = new InMemoryConnection(); + $this->flavour = $this->getFlavour(); $this->appContainer = $this->prophesize(ContainerInterface::class); @@ -163,6 +183,10 @@ protected function setUp() $this->appContainer->has(EventMachine::SERVICE_ID_ASYNC_EVENT_PRODUCER)->willReturn(false); $this->appContainer->has(EventMachine::SERVICE_ID_PROJECTION_MANAGER)->willReturn(false); $this->appContainer->has(EventMachine::SERVICE_ID_DOCUMENT_STORE)->willReturn(false); + $this->appContainer->has(EventMachine::SERVICE_ID_FLAVOUR)->willReturn(true); + $this->appContainer->get(EventMachine::SERVICE_ID_FLAVOUR)->will(function ($args) use ($self) { + return $self->flavour; + }); $this->containerChain = new ContainerChain( $this->appContainer->reveal(), @@ -180,6 +204,7 @@ protected function tearDown() $this->appContainer = null; $this->transactionManager = null; $this->inMemoryConnection = null; + $this->flavour = null; } protected function setUpAggregateProjector( @@ -189,6 +214,8 @@ protected function setUpAggregateProjector( ): void { $aggregateProjector = new AggregateProjector($documentStore, $this->eventMachine); + $aggregateProjector->setFlavour($this->getFlavour()); + $eventStore->create(new \Prooph\EventStore\Stream($streamName, new \ArrayIterator([]))); $this->appContainer->has(EventMachine::SERVICE_ID_PROJECTION_MANAGER)->willReturn(true); @@ -210,6 +237,36 @@ protected function setUpAggregateProjector( ->filterAggregateType(Aggregate::USER); } + protected function setUpRegisteredUsersProjector( + DocumentStore $documentStore, + EventStore $eventStore, + StreamName $streamName + ): void { + $projector = $this->getRegisteredUsersProjector($documentStore); + + $eventStore->create(new \Prooph\EventStore\Stream($streamName, new \ArrayIterator([]))); + + $this->appContainer->has(EventMachine::SERVICE_ID_PROJECTION_MANAGER)->willReturn(true); + $this->appContainer->get(EventMachine::SERVICE_ID_PROJECTION_MANAGER)->willReturn(new InMemoryProjectionManager( + $eventStore, + $this->inMemoryConnection + )); + $this->appContainer->get(EventMachine::SERVICE_ID_EVENT_STORE)->will(function ($args) use ($eventStore) { + return $eventStore; + }); + + $this->appContainer->has('Test.Projector.RegisteredUsers')->willReturn(true); + $this->appContainer->get('Test.Projector.RegisteredUsers')->will(function ($args) use ($projector) { + return $projector; + }); + + $this->eventMachine->watch(Stream::ofWriteModel()) + ->with('registered_users', 'Test.Projector.RegisteredUsers') + ->filterEvents([ + \ProophExample\PrototypingFlavour\Messaging\Event::USER_WAS_REGISTERED, + ]); + } + /** * @test */ @@ -218,13 +275,13 @@ public function it_dispatches_a_known_command() $recordedEvents = []; $this->eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { - $publishedEvents[] = $event; + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { + $publishedEvents[] = $this->convertToEventMachineMessage($event); }); $this->eventMachine->initialize($this->containerChain); @@ -248,8 +305,6 @@ public function it_dispatches_a_known_command() $event = $recordedEvents[0]; $this->assertUserWasRegistered($event, $registerUser, $userId); - - self::assertSame($event, $publishedEvents[0]); } /** @@ -257,25 +312,20 @@ public function it_dispatches_a_known_command() */ public function it_dispatches_a_known_query() { - $getUserResolver = new class() { - public function __invoke(Message $getUser, Deferred $deferred) - { - $deferred->resolve([ - UserDescription::IDENTIFIER => $getUser->payload()[UserDescription::IDENTIFIER], - UserDescription::USERNAME => 'Alex', - ]); - } - }; - - $this->appContainer->has(GetUserResolver::class)->willReturn(true); - $this->appContainer->get(GetUserResolver::class)->will(function ($args) use ($getUserResolver) { + $userId = Uuid::uuid4()->toString(); + + $getUserResolver = $this->getUserResolver([ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => 'Alex', + ]); + + $this->appContainer->has(\get_class($getUserResolver))->willReturn(true); + $this->appContainer->get(\get_class($getUserResolver))->will(function ($args) use ($getUserResolver) { return $getUserResolver; }); $this->eventMachine->initialize($this->containerChain); - $userId = Uuid::uuid4()->toString(); - $getUser = $this->eventMachine->messageFactory()->createMessageFromArray( Query::GET_USER, ['payload' => [ @@ -302,20 +352,16 @@ public function __invoke(Message $getUser, Deferred $deferred) */ public function it_allows_queries_without_payload() { - $getUsersResolver = new class() { - public function __invoke(Message $getUsers, Deferred $deferred) - { - $deferred->resolve([ - [ - UserDescription::IDENTIFIER => '123', - UserDescription::USERNAME => 'Alex', - ], - ]); - } - }; + $getUsersResolver = $this->getUsersResolver([ + [ + UserDescription::IDENTIFIER => '123', + UserDescription::USERNAME => 'Alex', + UserDescription::EMAIL => 'contact@prooph.de', + ], + ]); - $this->appContainer->has(GetUsersResolver::class)->willReturn(true); - $this->appContainer->get(GetUsersResolver::class)->will(function ($args) use ($getUsersResolver) { + $this->appContainer->has(\get_class($getUsersResolver))->willReturn(true); + $this->appContainer->get(\get_class($getUsersResolver))->will(function ($args) use ($getUsersResolver) { return $getUsersResolver; }); @@ -336,8 +382,9 @@ public function __invoke(Message $getUsers, Deferred $deferred) self::assertEquals([ [ - UserDescription::IDENTIFIER => '123', - UserDescription::USERNAME => 'Alex', + UserDescription::IDENTIFIER => '123', + UserDescription::USERNAME => 'Alex', + UserDescription::EMAIL => 'contact@prooph.de', ], ], $userList); } @@ -350,13 +397,13 @@ public function it_creates_message_on_dispatch_if_only_name_and_payload_is_given $recordedEvents = []; $this->eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { - $publishedEvents[] = $event; + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { + $publishedEvents[] = $this->convertToEventMachineMessage($event); }); $this->eventMachine->initialize($this->containerChain); @@ -374,7 +421,53 @@ public function it_creates_message_on_dispatch_if_only_name_and_payload_is_given /** @var GenericJsonSchemaEvent $event */ $event = $recordedEvents[0]; self::assertEquals(Event::USER_WAS_REGISTERED, $event->messageName()); - self::assertSame($event, $publishedEvents[0]); + } + + /** + * @test + */ + public function it_can_handle_command_for_existing_aggregate() + { + $recordedEvents = []; + + $this->eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { + $recordedEvents = \array_merge($recordedEvents, \iterator_to_array($args[1])); + }); + + $this->eventStore->load(new StreamName('event_stream'), 1, null, Argument::type(MetadataMatcher::class))->will(function ($args) use (&$recordedEvents) { + return new \ArrayIterator([$recordedEvents[0]]); + }); + + $publishedEvents = []; + + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { + $publishedEvents[] = $this->convertToEventMachineMessage($event); + }); + + $this->eventMachine->on(Event::USERNAME_WAS_CHANGED, function ($event) use (&$publishedEvents) { + $publishedEvents[] = $this->convertToEventMachineMessage($event); + }); + + $this->eventMachine->initialize($this->containerChain); + + $userId = Uuid::uuid4()->toString(); + + $this->eventMachine->bootstrap()->dispatch(Command::REGISTER_USER, [ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => 'Alex', + UserDescription::EMAIL => 'contact@prooph.de', + ]); + + $this->eventMachine->dispatch(Command::CHANGE_USERNAME, [ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => 'John', + ]); + + self::assertCount(2, $recordedEvents); + self::assertCount(2, $publishedEvents); + /** @var GenericJsonSchemaEvent $event */ + $event = $recordedEvents[1]; + self::assertEquals(Event::USERNAME_WAS_CHANGED, $event->messageName()); } /** @@ -387,7 +480,7 @@ public function it_enables_async_switch_message_router_if_container_has_a_produc $eventMachine = $this->eventMachine; $messageProducer = $this->prophesize(MessageProducer::class); - $messageProducer->__invoke(Argument::type(Message::class)) + $messageProducer->__invoke(Argument::type(ProophMessage::class), Argument::exact(null)) ->will(function ($args) use (&$producedEvents, $eventMachine) { $producedEvents[] = $args[0]; $eventMachine->dispatch($args[0]); @@ -401,13 +494,13 @@ public function it_enables_async_switch_message_router_if_container_has_a_produc $recordedEvents = []; $this->eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { - $publishedEvents[] = $event; + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { + $publishedEvents[] = $this->convertToEventMachineMessage($event); }); $this->eventMachine->initialize($this->containerChain); @@ -433,31 +526,8 @@ public function it_enables_async_switch_message_router_if_container_has_a_produc $this->assertUserWasRegistered($event, $registerUser, $userId); - //Event should have modified metadata (async switch) and therefor be another instance (as it is immutable) - self::assertNotSame($event, $publishedEvents[0]); - self::assertTrue($publishedEvents[0]->metadata()['handled-async']); + self::assertEquals(Event::USER_WAS_REGISTERED, $producedEvents[0]->messageName()); self::assertEquals(Event::USER_WAS_REGISTERED, $publishedEvents[0]->messageName()); - self::assertSame($publishedEvents[0], $producedEvents[0]); - } - - /** - * @test - */ - public function it_throws_exception_if_config_should_be_cached_but_contains_closures() - { - $eventMachine = new EventMachine(); - - $eventMachine->load(MessageDescription::class); - $eventMachine->load(UserDescription::class); - - $container = $this->prophesize(ContainerInterface::class); - - $eventMachine->initialize($container->reveal()); - - self::expectException(\RuntimeException::class); - self::expectExceptionMessage('At least one EventMachineDescription contains a Closure and is therefor not cacheable!'); - - $eventMachine->compileCacheableConfig(); } /** @@ -471,26 +541,24 @@ public function it_can_load_aggregate_state() $this->eventStore->load(new StreamName('event_stream'), 1, null, Argument::any())->will(function ($args) use ($userId, $eventMachine) { return new \ArrayIterator([ - $eventMachine->messageFactory()->createMessageFromArray(Event::USER_WAS_REGISTERED, [ - 'payload' => [ - 'userId' => $userId, - 'username' => 'Tester', - 'email' => 'tester@test.com', - ], - 'metadata' => [ - '_aggregate_id' => $userId, - '_aggregate_type' => Aggregate::USER, - '_aggregate_version' => 1, - ], - ]), - ]); - }); - - /** @var UserState $userState */ + $eventMachine->messageFactory()->createMessageFromArray(Event::USER_WAS_REGISTERED, [ + 'payload' => [ + 'userId' => $userId, + 'username' => 'Tester', + 'email' => 'tester@test.com', + ], + 'metadata' => [ + '_aggregate_id' => $userId, + '_aggregate_type' => Aggregate::USER, + '_aggregate_version' => 1, + ], + ]), + ]); + }); + $userState = $eventMachine->bootstrap()->loadAggregateState(Aggregate::USER, $userId); - self::assertInstanceOf(UserState::class, $userState); - self::assertEquals('Tester', $userState->username); + $this->assertLoadedUserState($userState); } /** @@ -512,14 +580,14 @@ public function it_sets_up_transaction_manager_if_event_store_supports_transacti $this->eventStore->inTransaction()->willReturn(true); $this->eventStore->appendTo(new StreamName('event_stream'), Argument::any())->will(function ($args) use (&$recordedEvents) { - $recordedEvents = iterator_to_array($args[1]); + $recordedEvents = \iterator_to_array($args[1]); }); $this->eventStore->commit()->shouldBeCalled(); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { $publishedEvents[] = $event; }); @@ -538,7 +606,6 @@ public function it_sets_up_transaction_manager_if_event_store_supports_transacti /** @var GenericJsonSchemaEvent $event */ $event = $recordedEvents[0]; self::assertEquals(Event::USER_WAS_REGISTERED, $event->messageName()); - self::assertSame($event, $publishedEvents[0]); } /** @@ -593,10 +660,10 @@ public function it_provides_message_schemas() ])->toArray(), Query::GET_USERS => JsonSchema::object([])->toArray(), Query::GET_FILTERED_USERS => JsonSchema::object([], [ - 'filter' => JsonSchema::nullOr(JsonSchema::typeRef('UserFilterInput')), + 'filter' => $filterInput, ])->toArray(), ], - ], + ], $this->eventMachine->messageSchemas() ); } @@ -633,7 +700,7 @@ public function it_builds_a_message_box_schema(): void ])->toArray(), Query::GET_USERS => JsonSchema::object([])->toArray(), Query::GET_FILTERED_USERS => JsonSchema::object([], [ - 'filter' => JsonSchema::nullOr(JsonSchema::typeRef('UserFilterInput')), + 'filter' => $filterInput, ])->toArray(), ]; @@ -681,7 +748,7 @@ public function it_builds_a_message_box_schema(): void */ public function it_watches_write_model_stream() { - $documentStore = new InMemoryDocumentStore(new InMemoryConnection()); + $documentStore = new DocumentStore\InMemoryDocumentStore(new InMemoryConnection()); $eventStore = new ActionEventEmitterEventStore( new InMemoryEventStore($this->inMemoryConnection), @@ -715,13 +782,104 @@ public function it_watches_write_model_stream() $this->assertNotNull($userState); $this->assertEquals([ - 'id' => $userId, + 'userId' => $userId, 'username' => 'Alex', 'email' => 'contact@prooph.de', 'failed' => null, ], $userState); } + /** + * @test + */ + public function it_forwards_projector_call_to_flavour() + { + $documentStore = new DocumentStore\InMemoryDocumentStore(new InMemoryConnection()); + + $eventStore = new ActionEventEmitterEventStore( + new InMemoryEventStore($this->inMemoryConnection), + new ProophActionEventEmitter(ActionEventEmitterEventStore::ALL_EVENTS) + ); + + $this->setUpRegisteredUsersProjector($documentStore, $eventStore, new StreamName('event_stream')); + + $this->eventMachine->initialize($this->containerChain); + + $userId = Uuid::uuid4()->toString(); + + $registerUser = $this->eventMachine->messageFactory()->createMessageFromArray( + Command::REGISTER_USER, + ['payload' => [ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => 'Alex', + UserDescription::EMAIL => 'contact@prooph.de', + ]] + ); + + $this->eventMachine->bootstrap()->dispatch($registerUser); + + $this->eventMachine->runProjections(false); + + //We expect RegisteredUsersProjector to use collection naming convention: _ + $userState = $documentStore->getDoc( + 'registered_users_0.1.0', + $userId + ); + + $this->assertNotNull($userState); + + $this->assertEquals([ + 'userId' => $userId, + 'username' => 'Alex', + 'email' => 'contact@prooph.de', + ], $userState); + } + + /** + * @test + */ + public function it_invokes_event_listener_using_flavour() + { + $messageDispatcher = $this->prophesize(MessageDispatcher::class); + + $newCmdName = null; + $newCmdPayload = null; + $messageDispatcher->dispatch(Argument::any(), Argument::any())->will(function ($args) use (&$newCmdName, &$newCmdPayload) { + $newCmdName = $args[0] ?? null; + $newCmdPayload = $args[1] ?? null; + }); + + $this->eventMachine->on(Event::USER_WAS_REGISTERED, 'Test.Listener.UserRegistered'); + + $listener = $this->getUserRegisteredListener($messageDispatcher->reveal()); + + $this->appContainer->has('Test.Listener.UserRegistered')->willReturn(true); + $this->appContainer->get('Test.Listener.UserRegistered')->will(function ($args) use ($listener) { + return $listener; + }); + + $this->eventMachine->initialize($this->containerChain); + + $userId = Uuid::uuid4()->toString(); + + $registerUser = $this->eventMachine->messageFactory()->createMessageFromArray( + Command::REGISTER_USER, + ['payload' => [ + UserDescription::IDENTIFIER => $userId, + UserDescription::USERNAME => 'Alex', + UserDescription::EMAIL => 'contact@prooph.de', + ]] + ); + + $this->eventMachine->bootstrap()->dispatch($registerUser); + + $this->assertNotNull($newCmdName); + $this->assertNotNull($newCmdPayload); + + $this->assertEquals('SendWelcomeEmail', $newCmdName); + $this->assertEquals(['email' => 'contact@prooph.de'], $newCmdPayload); + } + /** * @test */ @@ -815,7 +973,7 @@ public function it_sets_app_version() */ public function it_dispatches_a_known_command_with_immediate_consistency(): void { - $documentStore = new InMemoryDocumentStore($this->inMemoryConnection); + $documentStore = new DocumentStore\InMemoryDocumentStore($this->inMemoryConnection); $inMemoryEventStore = new InMemoryEventStore($this->inMemoryConnection); @@ -831,7 +989,7 @@ public function it_dispatches_a_known_command_with_immediate_consistency(): void $this->transactionManager = new TransactionManager($this->inMemoryConnection); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { $publishedEvents[] = $event; }); @@ -859,8 +1017,6 @@ public function it_dispatches_a_known_command_with_immediate_consistency(): void $event = $recordedEvents[0]; $this->assertUserWasRegistered($event, $registerUser, $userId); - self::assertSame($event, $publishedEvents[0]); - $userState = $documentStore->getDoc( $this->getAggregateCollectionName(Aggregate::USER), $userId @@ -869,7 +1025,7 @@ public function it_dispatches_a_known_command_with_immediate_consistency(): void $this->assertNotNull($userState); $this->assertEquals([ - 'id' => $userId, + 'userId' => $userId, 'username' => 'Alex', 'email' => 'contact@prooph.de', 'failed' => null, @@ -902,7 +1058,7 @@ public function it_rolls_back_events_and_projection_with_immediate_consistency() $this->transactionManager = new TransactionManager($this->inMemoryConnection); $publishedEvents = []; - $this->eventMachine->on(Event::USER_WAS_REGISTERED, function (Message $event) use (&$publishedEvents) { + $this->eventMachine->on(Event::USER_WAS_REGISTERED, function ($event) use (&$publishedEvents) { $publishedEvents[] = $event; }); @@ -929,7 +1085,7 @@ public function it_rolls_back_events_and_projection_with_immediate_consistency() $exceptionThrown = true; } $this->assertTrue($exceptionThrown); - $this->assertEmpty(iterator_to_array($eventStore->load($streamName))); + $this->assertEmpty(\iterator_to_array($eventStore->load($streamName))); } /** @@ -937,7 +1093,7 @@ public function it_rolls_back_events_and_projection_with_immediate_consistency() */ public function it_switches_action_event_emitter_with_immediate_consistency(): void { - $documentStore = new InMemoryDocumentStore($this->inMemoryConnection); + $documentStore = new DocumentStore\InMemoryDocumentStore($this->inMemoryConnection); $inMemoryEventStore = new InMemoryEventStore($this->inMemoryConnection); @@ -966,8 +1122,8 @@ public function it_switches_action_event_emitter_with_immediate_consistency(): v } private function assertUserWasRegistered( - GenericJsonSchemaEvent $event, - GenericJsonSchemaCommand $registerUser, + Message $event, + Message $registerUser, string $userId ): void { self::assertEquals(Event::USER_WAS_REGISTERED, $event->messageName()); @@ -987,4 +1143,29 @@ private function getAggregateCollectionName(string $aggregate): string AggregateProjector::generateProjectionName($aggregate) ); } + + private function convertToEventMachineMessage($event): Message + { + $flavour = $this->getFlavour(); + if ($flavour instanceof MessageFactoryAware) { + $flavour->setMessageFactory($this->eventMachine->messageFactory()); + } + + switch (\get_class($event)) { + case UserRegistered::class: + return $flavour->prepareNetworkTransmission(new MessageBag( + Event::USER_WAS_REGISTERED, + Message::TYPE_EVENT, + $event + )); + case UsernameChanged::class: + return $flavour->prepareNetworkTransmission(new MessageBag( + Event::USERNAME_WAS_CHANGED, + Message::TYPE_EVENT, + $event + )); + default: + return $event; + } + } } diff --git a/tests/EventMachineTestModeTest.php b/tests/EventMachineTestModeTest.php index e6bb5cf..543b138 100644 --- a/tests/EventMachineTestModeTest.php +++ b/tests/EventMachineTestModeTest.php @@ -13,11 +13,11 @@ use Prooph\EventMachine\Container\EventMachineContainer; use Prooph\EventMachine\EventMachine; -use ProophExample\Aggregate\CacheableUserDescription; -use ProophExample\Aggregate\UserDescription; -use ProophExample\Messaging\Command; -use ProophExample\Messaging\Event; -use ProophExample\Messaging\MessageDescription; +use ProophExample\PrototypingFlavour\Aggregate\CacheableUserDescription; +use ProophExample\PrototypingFlavour\Aggregate\UserDescription; +use ProophExample\PrototypingFlavour\Messaging\Command; +use ProophExample\PrototypingFlavour\Messaging\Event; +use ProophExample\PrototypingFlavour\Messaging\MessageDescription; use Ramsey\Uuid\Uuid; final class EventMachineTestModeTest extends BasicTestCase diff --git a/tests/Http/MessageBoxTest.php b/tests/Http/MessageBoxTest.php index ab9e1b2..b43f042 100644 --- a/tests/Http/MessageBoxTest.php +++ b/tests/Http/MessageBoxTest.php @@ -22,8 +22,8 @@ use Prooph\ServiceBus\CommandBus; use Prooph\ServiceBus\EventBus; use Prooph\ServiceBus\QueryBus; -use ProophExample\Aggregate\CacheableUserDescription; -use ProophExample\Messaging\MessageDescription; +use ProophExample\PrototypingFlavour\Aggregate\CacheableUserDescription; +use ProophExample\PrototypingFlavour\Messaging\MessageDescription; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use Ramsey\Uuid\Uuid; @@ -118,6 +118,7 @@ protected function setUp() $this->appContainer->has(EventMachine::SERVICE_ID_ASYNC_EVENT_PRODUCER)->willReturn(false); $this->appContainer->has(EventMachine::SERVICE_ID_PROJECTION_MANAGER)->willReturn(false); $this->appContainer->has(EventMachine::SERVICE_ID_DOCUMENT_STORE)->willReturn(false); + $this->appContainer->has(EventMachine::SERVICE_ID_FLAVOUR)->willReturn(false); $this->containerChain = new ContainerChain( $this->appContainer->reveal(), diff --git a/tests/Persistence/DocumentStore/Filter/AndFilterTest.php b/tests/Persistence/DocumentStore/Filter/AndFilterTest.php index 5c74d5f..30f7448 100644 --- a/tests/Persistence/DocumentStore/Filter/AndFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/AndFilterTest.php @@ -32,8 +32,8 @@ public function it_filters_docs_with_and_filter() new AndFilter(new LtFilter('age', 5), new GtFilter('age', 1)) ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Tiger', implode(', ', $names)); + $this->assertEquals('Tiger', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/EqFilterTest.php b/tests/Persistence/DocumentStore/Filter/EqFilterTest.php index e380fc6..1b25a74 100644 --- a/tests/Persistence/DocumentStore/Filter/EqFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/EqFilterTest.php @@ -27,9 +27,9 @@ public function it_filters_docs_with_eq_filter() $animals = $this->store->filterDocs($this->collection, new EqFilter('animal', 'duck')); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Quak', implode(', ', $names)); + $this->assertEquals('Quak', \implode(', ', $names)); } /** @@ -41,8 +41,8 @@ public function it_filters_docs_with_eq_null_filter() $animals = $this->store->filterDocs($this->collection, new EqFilter('race', null)); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Quak', implode(', ', $names)); + $this->assertEquals('Quak', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/ExistsFilterTest.php b/tests/Persistence/DocumentStore/Filter/ExistsFilterTest.php index 94a4fca..195214e 100644 --- a/tests/Persistence/DocumentStore/Filter/ExistsFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/ExistsFilterTest.php @@ -27,8 +27,8 @@ public function it_filters_docs_with_exists_filter() $animals = $this->store->filterDocs($this->collection, new ExistsFilter('race')); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Hasso, Quak', implode(', ', $names)); + $this->assertEquals('Hasso, Quak', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/FilterTestHelperTrait.php b/tests/Persistence/DocumentStore/Filter/FilterTestHelperTrait.php index 09ee9da..c7bc684 100644 --- a/tests/Persistence/DocumentStore/Filter/FilterTestHelperTrait.php +++ b/tests/Persistence/DocumentStore/Filter/FilterTestHelperTrait.php @@ -35,7 +35,7 @@ protected function setUp() private function extractFieldIntoList(string $field, \Traversable $docs): \Generator { foreach ($docs as $doc) { - if (array_key_exists($field, $doc)) { + if (\array_key_exists($field, $doc)) { yield $doc[$field]; continue; } diff --git a/tests/Persistence/DocumentStore/Filter/GtFilterTest.php b/tests/Persistence/DocumentStore/Filter/GtFilterTest.php index 261d204..a118c3f 100644 --- a/tests/Persistence/DocumentStore/Filter/GtFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/GtFilterTest.php @@ -30,8 +30,8 @@ public function it_filters_docs_with_gt_filter() new GtFilter('age', 5) ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso', implode(', ', $names)); + $this->assertEquals('Jack, Hasso', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/GteFilterTest.php b/tests/Persistence/DocumentStore/Filter/GteFilterTest.php index 3c758d9..469e5d5 100644 --- a/tests/Persistence/DocumentStore/Filter/GteFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/GteFilterTest.php @@ -30,8 +30,8 @@ public function it_filters_docs_with_gte_filter() new GteFilter('age', 5) ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso, Gini', implode(', ', $names)); + $this->assertEquals('Jack, Hasso, Gini', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/InArrayFilterTest.php b/tests/Persistence/DocumentStore/Filter/InArrayFilterTest.php index 5c4b6eb..9adcf7c 100644 --- a/tests/Persistence/DocumentStore/Filter/InArrayFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/InArrayFilterTest.php @@ -30,8 +30,8 @@ public function it_filters_docs_with_in_array_filter() new InArrayFilter('status', 'hungry') ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Tiger', implode(', ', $names)); + $this->assertEquals('Tiger', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/LtFilterTest.php b/tests/Persistence/DocumentStore/Filter/LtFilterTest.php index e80e492..7fee63e 100644 --- a/tests/Persistence/DocumentStore/Filter/LtFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/LtFilterTest.php @@ -27,8 +27,8 @@ public function it_filters_docs_with_lt_filter() $animals = $this->store->filterDocs($this->collection, new LtFilter('age', 5)); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Tiger, Quak', implode(', ', $names)); + $this->assertEquals('Tiger, Quak', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/LteFilterTest.php b/tests/Persistence/DocumentStore/Filter/LteFilterTest.php index 1ac53b8..4da213d 100644 --- a/tests/Persistence/DocumentStore/Filter/LteFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/LteFilterTest.php @@ -27,8 +27,8 @@ public function it_filters_docs_with_lte_filter() $animals = $this->store->filterDocs($this->collection, new LteFilter('age', 5)); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Gini, Tiger, Quak', implode(', ', $names)); + $this->assertEquals('Gini, Tiger, Quak', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/NotFilterTest.php b/tests/Persistence/DocumentStore/Filter/NotFilterTest.php index 0252e44..8021722 100644 --- a/tests/Persistence/DocumentStore/Filter/NotFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/NotFilterTest.php @@ -28,8 +28,8 @@ public function it_filters_docs_with_not_filter() $animals = $this->store->filterDocs($this->collection, new NotFilter(new LtFilter('age', 5))); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso, Gini', implode(', ', $names)); + $this->assertEquals('Jack, Hasso, Gini', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/Filter/OrFilterTest.php b/tests/Persistence/DocumentStore/Filter/OrFilterTest.php index 71ad236..4962ffb 100644 --- a/tests/Persistence/DocumentStore/Filter/OrFilterTest.php +++ b/tests/Persistence/DocumentStore/Filter/OrFilterTest.php @@ -32,8 +32,8 @@ public function it_filters_docs_with_or_filter() new OrFilter(new LtFilter('age', 2), new GtFilter('age', 5)) ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso, Quak', implode(', ', $names)); + $this->assertEquals('Jack, Hasso, Quak', \implode(', ', $names)); } } diff --git a/tests/Persistence/DocumentStore/InMemoryDocumentStoreTest.php b/tests/Persistence/DocumentStore/InMemoryDocumentStoreTest.php index b3b7a56..0e09642 100644 --- a/tests/Persistence/DocumentStore/InMemoryDocumentStoreTest.php +++ b/tests/Persistence/DocumentStore/InMemoryDocumentStoreTest.php @@ -43,9 +43,9 @@ public function it_filters_docs() $dogs = $this->store->filterDocs(self::COLLECTION, new EqFilter('animal', 'dog')); - $dogNames = iterator_to_array($this->extractFieldIntoList('name', $dogs)); + $dogNames = \iterator_to_array($this->extractFieldIntoList('name', $dogs)); - $this->assertEquals('Jack, Hasso', implode(', ', $dogNames)); + $this->assertEquals('Jack, Hasso', \implode(', ', $dogNames)); } /** @@ -57,9 +57,9 @@ public function it_orders_docs_ASC() $animals = $this->store->filterDocs(self::COLLECTION, new AnyFilter(), null, null, Asc::fromString('name')); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Gini, Hasso, Jack, Quak, Tiger', implode(', ', $names)); + $this->assertEquals('Gini, Hasso, Jack, Quak, Tiger', \implode(', ', $names)); } /** @@ -71,9 +71,9 @@ public function it_orders_docs_DESC() $animals = $this->store->filterDocs(self::COLLECTION, new AnyFilter(), null, null, Desc::fromString('name')); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Tiger, Quak, Jack, Hasso, Gini', implode(', ', $names)); + $this->assertEquals('Tiger, Quak, Jack, Hasso, Gini', \implode(', ', $names)); } /** @@ -91,9 +91,9 @@ public function it_orders_by_multiple_fields() AndOrder::by(Asc::byProp('animal'), Desc::byProp('age')) ); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Gini, Tiger, Hasso, Jack, Quak', implode(', ', $names)); + $this->assertEquals('Gini, Tiger, Hasso, Jack, Quak', \implode(', ', $names)); } /** @@ -105,9 +105,9 @@ public function it_skips_docs_after_ordering() $animals = $this->store->filterDocs(self::COLLECTION, new AnyFilter(), 2, null, AndOrder::by(Asc::byProp('animal'), Asc::byProp('age'))); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso, Quak', implode(', ', $names)); + $this->assertEquals('Jack, Hasso, Quak', \implode(', ', $names)); } /** @@ -119,9 +119,9 @@ public function it_limits_docs_after_ordering() $animals = $this->store->filterDocs(self::COLLECTION, new AnyFilter(), null, 3, AndOrder::by(Asc::byProp('animal'), Asc::byProp('age'))); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Tiger, Gini, Jack', implode(', ', $names)); + $this->assertEquals('Tiger, Gini, Jack', \implode(', ', $names)); } /** @@ -133,15 +133,15 @@ public function it_skips_and_limits_docs_after_ordering() $animals = $this->store->filterDocs(self::COLLECTION, new AnyFilter(), 2, 2, AndOrder::by(Asc::byProp('animal'), Asc::byProp('age'))); - $names = iterator_to_array($this->extractFieldIntoList('name', $animals)); + $names = \iterator_to_array($this->extractFieldIntoList('name', $animals)); - $this->assertEquals('Jack, Hasso', implode(', ', $names)); + $this->assertEquals('Jack, Hasso', \implode(', ', $names)); } private function extractFieldIntoList(string $field, \Traversable $docs): \Generator { foreach ($docs as $doc) { - if (array_key_exists($field, $doc)) { + if (\array_key_exists($field, $doc)) { yield $doc[$field]; continue; } diff --git a/tests/Persistence/DocumentStore/MultiFieldIndexTest.php b/tests/Persistence/DocumentStore/MultiFieldIndexTest.php index 75c8072..3b9b6f8 100644 --- a/tests/Persistence/DocumentStore/MultiFieldIndexTest.php +++ b/tests/Persistence/DocumentStore/MultiFieldIndexTest.php @@ -28,7 +28,7 @@ public function it_creates_multi_field_index_for_fields() '{"field":"testField1","sort":1,"unique":false}', '{"field":"testField2","sort":1,"unique":false}', ], - array_map( + \array_map( function ($index) { return (string) $index; },