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;
},