diff --git a/Controller/ArticleController.php b/Controller/ArticleController.php index 4ec607965..fa1ea3aa3 100644 --- a/Controller/ArticleController.php +++ b/Controller/ArticleController.php @@ -23,15 +23,40 @@ use ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery; use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery; use ONGR\ElasticsearchDSL\Sort\FieldSort; +use Prooph\Common\Event\ProophActionEventEmitter; +use Prooph\Common\Messaging\FQCNMessageFactory; +use Prooph\EventStore\ActionEventEmitterEventStore; +use Prooph\EventStore\Pdo\MySqlEventStore; +use Prooph\EventStore\Pdo\PersistenceStrategy\MySqlAggregateStreamStrategy; +use Prooph\EventStore\Pdo\Projection\MySqlProjectionManager; +use Prooph\EventStoreBusBridge\EventPublisher; +use Prooph\ServiceBus\CommandBus; +use Prooph\ServiceBus\EventBus; +use Prooph\ServiceBus\Plugin\Router\CommandRouter; +use Prooph\ServiceBus\Plugin\Router\EventRouter; +use Prooph\SnapshotStore\Pdo\PdoSnapshotStore; +use Ramsey\Uuid\Uuid; use Sulu\Bundle\ArticleBundle\Admin\ArticleAdmin; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; use Sulu\Bundle\ArticleBundle\ListBuilder\ElasticSearchFieldDescriptor; use Sulu\Bundle\ArticleBundle\Metadata\ArticleViewDocumentIdTrait; -use Sulu\Component\Content\Form\Exception\InvalidFormException; +use Sulu\Bundle\ArticleBundle\Prooph\Infrastruture\ArticleRepository; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\CreateArticle; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\CreateArticleHandler; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\PublishArticle; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\PublishArticleHandler; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\UnpublishArticle; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\UnpublishArticleHandler; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\UpdateArticle; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Command\UpdateArticleHandler; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Event\ArticleCreated; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Event\ArticlePublished; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Event\ArticleUnpublished; +use Sulu\Bundle\ArticleBundle\Prooph\Model\Event\ArticleUpdated; +use Sulu\Bundle\ArticleBundle\Prooph\Projection\ArticleDocumentProjector; use Sulu\Component\Content\Mapper\ContentMapperInterface; use Sulu\Component\DocumentManager\DocumentManagerInterface; use Sulu\Component\DocumentManager\Metadata\BaseMetadataFactory; -use Sulu\Component\Rest\Exception\MissingParameterException; use Sulu\Component\Rest\Exception\RestException; use Sulu\Component\Rest\ListBuilder\ListRepresentation; use Sulu\Component\Rest\RequestParametersTrait; @@ -279,14 +304,24 @@ public function getAction($uuid, Request $request) */ public function postAction(Request $request) { - $action = $request->get('action'); - $document = $this->getDocumentManager()->create(self::DOCUMENT_TYPE); + $id = Uuid::uuid4()->toString(); + $locale = $this->getRequestParameter($request, 'locale', true); $data = $request->request->all(); + $this->getCommandBus()->dispatch( + new CreateArticle( + [ + 'id' => $id, + 'locale' => $locale, + 'data' => $data, + 'userId' => $this->getUser()->getId(), + ] + ) + ); - $this->persistDocument($data, $document, $locale); - $this->handleActionParameter($action, $document, $locale); - $this->getDocumentManager()->flush(); + $this->handleActionParameter($id, $request->get('action'), $locale); + + $document = $this->getDocumentManager()->find($id, $locale); return $this->handleView( $this->view($document)->setSerializationContext( @@ -322,9 +357,20 @@ public function putAction(Request $request, $uuid) $this->get('sulu_hash.request_hash_checker')->checkHash($request, $document, $document->getUuid()); - $this->persistDocument($data, $document, $locale); - $this->handleActionParameter($action, $document, $locale); - $this->getDocumentManager()->flush(); + $this->getCommandBus()->dispatch( + new UpdateArticle( + [ + 'id' => $uuid, + 'locale' => $locale, + 'data' => $data, + 'userId' => $this->getUser()->getId(), + ] + ) + ); + + $this->handleActionParameter($uuid, $action, $locale); + + $document = $this->getDocumentManager()->find($uuid, $locale); return $this->handleView( $this->view($document)->setSerializationContext( @@ -378,7 +424,7 @@ public function deleteAction($id) * * @Post("/articles/{uuid}") * - * @param string $uuid + * @param string $uuid * @param Request $request * * @return Response @@ -397,9 +443,15 @@ public function postTriggerAction($uuid, Request $request) try { switch ($action) { case 'unpublish': - $document = $this->getDocumentManager()->find($uuid, $locale); - $this->getDocumentManager()->unpublish($document, $locale); - $this->getDocumentManager()->flush(); + $this->getCommandBus()->dispatch( + new UnpublishArticle( + [ + 'id' => $uuid, + 'locale' => $locale, + 'userId' => $this->getUser()->getId(), + ] + ) + ); $data = $this->getDocumentManager()->find($uuid, $locale); @@ -486,47 +538,6 @@ public function getSecurityContext() return ArticleAdmin::SECURITY_CONTEXT; } - /** - * Persists the document using the given information. - * - * @param array $data - * @param object $document - * @param string $locale - * - * @throws InvalidFormException - * @throws MissingParameterException - */ - private function persistDocument($data, $document, $locale) - { - $formType = $this->getMetadataFactory()->getMetadataForAlias('article')->getFormType(); - $form = $this->createForm( - $formType, - $document, - [ - // disable csrf protection, since we can't produce a token, because the form is cached on the client - 'csrf_protection' => false, - ] - ); - $form->submit($data, false); - - if (!$form->isValid()) { - throw new InvalidFormException($form); - } - - if (array_key_exists('author', $data) && null === $data['author']) { - $document->setAuthor(null); - } - - $this->getDocumentManager()->persist( - $document, - $locale, - [ - 'user' => $this->getUser()->getId(), - 'clear_missing_content' => false, - ] - ); - } - /** * Returns document-manager. * @@ -545,20 +556,18 @@ protected function getMapper() return $this->get('sulu.content.mapper'); } - /** - * Delegates actions by given actionParameter, which can be retrieved from the request. - * - * @param string $actionParameter - * @param object $document - * @param string $locale - */ - private function handleActionParameter($actionParameter, $document, $locale) + private function handleActionParameter(string $id, ?string $action, string $locale) { - switch ($actionParameter) { - case 'publish': - $this->getDocumentManager()->publish($document, $locale); - - break; + if ($action === 'publish') { + $this->getCommandBus()->dispatch( + new PublishArticle( + [ + 'id' => $id, + 'locale' => $locale, + 'userId' => $this->getUser()->getId(), + ] + ) + ); } } @@ -604,4 +613,45 @@ protected function getMetadataFactory() { return $this->get('sulu_document_manager.metadata_factory.base'); } + + protected function getCommandBus() + { + $pdo = new \PDO('mysql:dbname=su_article_prooph;host=127.0.0.1', 'root', ''); + $eventStore = new MySqlEventStore(new FQCNMessageFactory(), $pdo, new MySqlAggregateStreamStrategy()); + $eventEmitter = new ProophActionEventEmitter(); + $eventStore = new ActionEventEmitterEventStore($eventStore, $eventEmitter); + + $eventBus = new EventBus($eventEmitter); + $eventPublisher = new EventPublisher($eventBus); + $eventPublisher->attachToEventStore($eventStore); + + $pdoSnapshotStore = new PdoSnapshotStore($pdo); + $userRepository = new ArticleRepository($eventStore, $pdoSnapshotStore); + + $projectionManager = new MySqlProjectionManager($eventStore, $pdo); + + $commandBus = new CommandBus(); + $router = new CommandRouter(); + $router->route(CreateArticle::class)->to( + new CreateArticleHandler($userRepository, $this->get('sulu_content.structure.factory')) + ); + $router->route(PublishArticle::class)->to(new PublishArticleHandler($userRepository)); + $router->route(UnpublishArticle::class)->to(new UnpublishArticleHandler($userRepository)); + $router->route(UpdateArticle::class)->to( + new UpdateArticleHandler($userRepository, $this->get('sulu_content.structure.factory')) + ); + $router->attachToMessageBus($commandBus); + + $userProjector = new ArticleDocumentProjector( + $this->getDocumentManager(), $this->getMetadataFactory(), $this->get('form.factory') + ); + $eventRouter = new EventRouter(); + $eventRouter->route(ArticleCreated::class)->to([$userProjector, 'onArticleCreated']); + $eventRouter->route(ArticlePublished::class)->to([$userProjector, 'onArticlePublished']); + $eventRouter->route(ArticleUnpublished::class)->to([$userProjector, 'onArticleUnpublished']); + $eventRouter->route(ArticleUpdated::class)->to([$userProjector, 'onArticleUpdated']); + $eventRouter->attachToMessageBus($eventBus); + + return $commandBus; + } } diff --git a/Document/Subscriber/ArticleSubscriber.php b/Document/Subscriber/ArticleSubscriber.php index ce14d4830..24c62e49c 100644 --- a/Document/Subscriber/ArticleSubscriber.php +++ b/Document/Subscriber/ArticleSubscriber.php @@ -109,6 +109,7 @@ public static function getSubscribedEvents() ['hydratePageData', -2000], ], Events::PERSIST => [ + ['handleUuid', 480], ['handleScheduleIndex', -500], ['setChildrenStructureType', 0], ['persistPageData', -2000], @@ -134,6 +135,17 @@ public static function getSubscribedEvents() ]; } + public function handleUuid(PersistEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticleDocument) { + return; + } + + $event->getNode()->addMixin('mix:referenceable'); + $event->getNode()->setProperty('jcr:uuid', $document->getUuid()); + } + /** * Schedule article document for index. * diff --git a/Prooph/Infrastruture/ArticleRepository.php b/Prooph/Infrastruture/ArticleRepository.php new file mode 100644 index 000000000..22b80e20e --- /dev/null +++ b/Prooph/Infrastruture/ArticleRepository.php @@ -0,0 +1,38 @@ +saveAggregateRoot($article); + } + + public function get(string $id): ?Article + { + return $this->getAggregateRoot($id); + } +} diff --git a/Prooph/Model/Article.php b/Prooph/Model/Article.php new file mode 100644 index 000000000..f6702fe8d --- /dev/null +++ b/Prooph/Model/Article.php @@ -0,0 +1,224 @@ +recordThat( + ArticleCreated::occur( + $id, + [ + 'locale' => $locale, + 'structureType' => $structureType, + 'structureData' => $structureData, + 'creator' => $userId, + 'created' => (new \DateTime())->format(\DateTime::ATOM), + 'changer' => $userId, + 'changed' => (new \DateTime())->format(\DateTime::ATOM), + 'data' => $data, + ] + ) + ); + + return $obj; + } + + public function publish(string $locale, int $userId) + { + $this->recordThat( + ArticlePublished::occur( + $this->id, + [ + 'locale' => $locale, + 'changer' => $userId, + 'changed' => (new \DateTime())->format(\DateTime::ATOM), + 'published' => (new \DateTime())->format(\DateTime::ATOM), + ] + ) + ); + + return $this; + } + + public function unpublish(string $locale, int $userId) + { + $this->recordThat( + ArticleUnpublished::occur( + $this->id, + [ + 'locale' => $locale, + 'changer' => $userId, + 'changed' => (new \DateTime())->format(\DateTime::ATOM), + ] + ) + ); + + return $this; + } + + public function updateWithData( + string $locale, + string $structureType, + array $structureData, + array $data, + int $userId + ): self { + $this->recordThat( + ArticleUpdated::occur( + $this->id, + [ + 'locale' => $locale, + 'structureType' => $structureType, + 'structureData' => $structureData, + 'changer' => $userId, + 'changed' => (new \DateTime())->format(\DateTime::ATOM), + 'data' => $data, + ] + ) + ); + + return $this; + } + + protected function aggregateId(): string + { + return $this->id; + } + + protected function data(): array + { + return $this->data; + } + + protected function apply(AggregateChanged $event): void + { + switch (get_class($event)) { + case ArticleCreated::class: + /** @var ArticleCreated $event */ + $this->id = $event->aggregateId(); + $this->locale = $event->locale(); + $this->structureType = $event->structureType(); + $this->structureData = $event->structureData(); + $this->changed = $event->changed(); + $this->changer = $event->changer(); + $this->authored = $this->created = $event->created(); + $this->author = $this->creator = $event->creator(); + $this->title = $event->structureData()['title']; + break; + case ArticlePublished::class: + /** @var ArticlePublished $event */ + $this->id = $event->aggregateId(); + $this->locale = $event->locale(); + $this->workflowStage = WorkflowStage::PUBLISHED; + $this->changed = $event->changed(); + $this->changer = $event->changer(); + $this->published = $event->published(); + break; + case ArticleUnpublished::class: + /** @var ArticleUnpublished $event */ + $this->id = $event->aggregateId(); + $this->locale = $event->locale(); + $this->workflowStage = WorkflowStage::TEST; + $this->changed = $event->changed(); + $this->changer = $event->changer(); + break; + case ArticleUpdated::class: + /** @var ArticleUpdated $event */ + $this->id = $event->aggregateId(); + $this->locale = $event->locale(); + $this->data = $event->data(); + break; + } + } +} diff --git a/Prooph/Model/ArticleRepository.php b/Prooph/Model/ArticleRepository.php new file mode 100644 index 000000000..d77c9cf7d --- /dev/null +++ b/Prooph/Model/ArticleRepository.php @@ -0,0 +1,11 @@ +payload()['id']; + } + + public function locale(): string + { + return $this->payload()['locale']; + } + + public function data(): array + { + return $this->payload()['data']; + } + + public function userId(): int + { + return $this->payload()['userId']; + } +} diff --git a/Prooph/Model/Command/CreateArticleHandler.php b/Prooph/Model/Command/CreateArticleHandler.php new file mode 100644 index 000000000..c522a9caa --- /dev/null +++ b/Prooph/Model/Command/CreateArticleHandler.php @@ -0,0 +1,51 @@ +repository = $repository; + $this->metadataFactory = $metadataFactory; + } + + public function __invoke(CreateArticle $command): void + { + $structureType = $command->data()['template']; + $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType); + + $structureData = []; + foreach ($metadata->getProperties() as $property) { + if(array_key_exists($property->getName(), $command->data())) { + $structureData[$property->getName()] = $command->data()[$property->getName()]; + } + } + + $article = Article::createWithData( + $command->id(), + $command->locale(), + $structureType, + $structureData, + $command->data(), + $command->userId() + ); + $this->repository->save($article); + } +} diff --git a/Prooph/Model/Command/PublishArticle.php b/Prooph/Model/Command/PublishArticle.php new file mode 100644 index 000000000..271829143 --- /dev/null +++ b/Prooph/Model/Command/PublishArticle.php @@ -0,0 +1,28 @@ +payload()['id']; + } + + public function locale(): string + { + return $this->payload()['locale']; + } + + public function userId(): int + { + return $this->payload()['userId']; + } +} diff --git a/Prooph/Model/Command/PublishArticleHandler.php b/Prooph/Model/Command/PublishArticleHandler.php new file mode 100644 index 000000000..ffa152183 --- /dev/null +++ b/Prooph/Model/Command/PublishArticleHandler.php @@ -0,0 +1,32 @@ +repository = $repository; + } + + public function __invoke(PublishArticle $command): void + { + $article = $this->repository->get($command->id()); + + $article = $article->publish($command->locale(), $command->userId()); + $this->repository->save($article); + + // TODO create URL + // TODO create version + } +} diff --git a/Prooph/Model/Command/UnpublishArticle.php b/Prooph/Model/Command/UnpublishArticle.php new file mode 100644 index 000000000..3cc0e1142 --- /dev/null +++ b/Prooph/Model/Command/UnpublishArticle.php @@ -0,0 +1,28 @@ +payload()['id']; + } + + public function locale(): string + { + return $this->payload()['locale']; + } + + public function userId(): int + { + return $this->payload()['userId']; + } +} diff --git a/Prooph/Model/Command/UnpublishArticleHandler.php b/Prooph/Model/Command/UnpublishArticleHandler.php new file mode 100644 index 000000000..d1d3bb800 --- /dev/null +++ b/Prooph/Model/Command/UnpublishArticleHandler.php @@ -0,0 +1,32 @@ +repository = $repository; + } + + public function __invoke(UnpublishArticle $command): void + { + $article = $this->repository->get($command->id()); + + $article = $article->unpublish($command->locale(), $command->userId()); + $this->repository->save($article); + + // TODO create URL + // TODO create version + } +} diff --git a/Prooph/Model/Command/UpdateArticle.php b/Prooph/Model/Command/UpdateArticle.php new file mode 100644 index 000000000..b12dbb5c7 --- /dev/null +++ b/Prooph/Model/Command/UpdateArticle.php @@ -0,0 +1,33 @@ +payload()['id']; + } + + public function locale(): string + { + return $this->payload()['locale']; + } + + public function data(): array + { + return $this->payload()['data']; + } + + public function userId(): int + { + return $this->payload()['userId']; + } +} diff --git a/Prooph/Model/Command/UpdateArticleHandler.php b/Prooph/Model/Command/UpdateArticleHandler.php new file mode 100644 index 000000000..3df0259e2 --- /dev/null +++ b/Prooph/Model/Command/UpdateArticleHandler.php @@ -0,0 +1,52 @@ +repository = $repository; + $this->metadataFactory = $metadataFactory; + } + + public function __invoke(UpdateArticle $command): void + { + $structureType = $command->data()['template']; + $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType); + + $structureData = []; + foreach ($metadata->getProperties() as $property) { + if(array_key_exists($property->getName(), $command->data())) { + $structureData[$property->getName()] = $command->data()[$property->getName()]; + } + } + + $article = $this->repository->get($command->id()); + $article = $article->updateWithData( + $command->locale(), + $structureType, + $structureData, + $command->data(), + $command->userId() + ); + $this->repository->save($article); + } +} diff --git a/Prooph/Model/Event/ArticleCreated.php b/Prooph/Model/Event/ArticleCreated.php new file mode 100644 index 000000000..f4bbb2d3d --- /dev/null +++ b/Prooph/Model/Event/ArticleCreated.php @@ -0,0 +1,20 @@ +payload['data']; + } +} diff --git a/Prooph/Model/Event/ArticlePublished.php b/Prooph/Model/Event/ArticlePublished.php new file mode 100644 index 000000000..e1dee8b92 --- /dev/null +++ b/Prooph/Model/Event/ArticlePublished.php @@ -0,0 +1,12 @@ +payload['data']; + } +} diff --git a/Prooph/Model/Event/AuthoredTrait.php b/Prooph/Model/Event/AuthoredTrait.php new file mode 100644 index 000000000..7c7eb8cce --- /dev/null +++ b/Prooph/Model/Event/AuthoredTrait.php @@ -0,0 +1,16 @@ +payload['author']; + } + + public function authored(): \DateTime + { + return new \DateTime($this->payload['authored']); + } +} diff --git a/Prooph/Model/Event/ChangedTrait.php b/Prooph/Model/Event/ChangedTrait.php new file mode 100644 index 000000000..fe1811339 --- /dev/null +++ b/Prooph/Model/Event/ChangedTrait.php @@ -0,0 +1,16 @@ +payload['changer']; + } + + public function changed(): \DateTime + { + return new \DateTime($this->payload['changed']); + } +} diff --git a/Prooph/Model/Event/CreatedTrait.php b/Prooph/Model/Event/CreatedTrait.php new file mode 100644 index 000000000..8339bf41a --- /dev/null +++ b/Prooph/Model/Event/CreatedTrait.php @@ -0,0 +1,16 @@ +payload['creator']; + } + + public function created(): \DateTime + { + return new \DateTime($this->payload['created']); + } +} diff --git a/Prooph/Model/Event/LocaleTrait.php b/Prooph/Model/Event/LocaleTrait.php new file mode 100644 index 000000000..e6878f8af --- /dev/null +++ b/Prooph/Model/Event/LocaleTrait.php @@ -0,0 +1,11 @@ +payload['locale']; + } +} diff --git a/Prooph/Model/Event/PublishedTrait.php b/Prooph/Model/Event/PublishedTrait.php new file mode 100644 index 000000000..4573635b4 --- /dev/null +++ b/Prooph/Model/Event/PublishedTrait.php @@ -0,0 +1,11 @@ +payload['published']); + } +} diff --git a/Prooph/Model/Event/StructureTrait.php b/Prooph/Model/Event/StructureTrait.php new file mode 100644 index 000000000..d167655c3 --- /dev/null +++ b/Prooph/Model/Event/StructureTrait.php @@ -0,0 +1,17 @@ +payload['structureType']; + } + + public function structureData(): array + { + return $this->payload['structureData']; + } + +} diff --git a/Prooph/Projection/ArticleDocumentProjector.php b/Prooph/Projection/ArticleDocumentProjector.php new file mode 100644 index 000000000..320e44dad --- /dev/null +++ b/Prooph/Projection/ArticleDocumentProjector.php @@ -0,0 +1,109 @@ +documentManager = $documentManager; + $this->metadataFactory = $metadataFactory; + $this->formFactory = $formFactory; + } + + public function onArticleCreated(ArticleCreated $event): void + { + /** @var ArticleDocument $document */ + $document = $this->documentManager->create(ArticleController::DOCUMENT_TYPE); + $document->setUuid($event->aggregateId()); + + $this->persistDocument($document, $event->data(), $event->locale(), $event->creator()); + $this->documentManager->flush(); + } + + public function onArticlePublished(ArticlePublished $event): void + { + $document = $this->documentManager->find($event->aggregateId(), $event->locale()); + $this->documentManager->publish($document, $event->locale()); + $this->documentManager->flush(); + } + + public function onArticleUnpublished(ArticleUnpublished $event): void + { + $document = $this->documentManager->find($event->aggregateId(), $event->locale()); + $this->documentManager->unpublish($document, $event->locale()); + $this->documentManager->flush(); + } + + public function onArticleUpdated(ArticleUpdated $event): void + { + $document = $this->documentManager->find($event->aggregateId(), $event->locale()); + + $this->persistDocument($document, $event->data(), $event->locale(), $event->changer()); + $this->documentManager->flush(); + } + + private function persistDocument(ArticleDocument $document, array $data, string $locale, int $userId) + { + $formType = $this->metadataFactory->getMetadataForAlias('article')->getFormType(); + $form = $this->createForm( + $formType, + $document, + [ + // disable csrf protection, since we can't produce a token, because the form is cached on the client + 'csrf_protection' => false, + ] + ); + $form->submit($data, false); + + if (!$form->isValid()) { + throw new InvalidFormException($form); + } + + if (array_key_exists('author', $data) && null === $data['author']) { + $document->setAuthor(null); + } + + $this->documentManager->persist( + $document, + $locale, + [ + 'user' => $userId, + 'clear_missing_content' => false, + ] + ); + } + + protected function createForm($type, $data = null, array $options = array()) + { + return $this->formFactory->create($type, $data, $options); + } +} diff --git a/Tests/app/config/config.yml b/Tests/app/config/config.yml index 18dd0f349..bc94cb817 100644 --- a/Tests/app/config/config.yml +++ b/Tests/app/config/config.yml @@ -1,6 +1,7 @@ # Doctrine Configuration doctrine: dbal: + host: 127.0.0.1 dbname: "su_articles_test" sulu_core: diff --git a/composer.json b/composer.json index 866dbb7ea..34ea0af3e 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,14 @@ "ongr/elasticsearch-bundle": "^1.2.9 || ~5.0", "ongr/elasticsearch-dsl": "^2.2.2 || ~5.0", "jackalope/jackalope": "^1.2.8 || >=1.3.3", - "jms/serializer-bundle": "^1.1 || ^2.0" + "jms/serializer-bundle": "^1.1 || ^2.0", + "prooph/service-bus": "^6.2", + "prooph/event-sourcing": "^5.3", + "prooph/event-store": "^7.3", + "prooph/pdo-event-store": "^1.6", + "prooph/event-store-bus-bridge": "^3.1", + "prooph/snapshotter": "^2.1", + "prooph/pdo-snapshot-store": "^1.4" }, "require-dev": { "massive/build-bundle": "~0.3.0",