├── .coveralls.yml ├── .laminas-ci.json ├── .php-cs-fixer.php ├── .gitignore ├── .github └── workflows │ └── continuous-integration.yml ├── docs ├── bookdown.json ├── event.md └── messaging.md ├── .docheader ├── tests ├── Mock │ ├── InvalidMessage.php │ ├── AskSomething.php │ ├── DoSomething.php │ ├── SomethingWasDone.php │ ├── ActionEventListenerMock.php │ └── ActionListenerAggregateMock.php ├── Messaging │ ├── QueryTest.php │ ├── NoOpMessageConverterTest.php │ ├── DomainEventTest.php │ ├── FQCNMessageFactoryTest.php │ ├── CommandTest.php │ └── MessageDataAssertionTest.php └── Event │ ├── DefaultActionEventTest.php │ └── ProophActionEventEmitterTest.php ├── phpunit.xml.dist ├── src ├── Event │ ├── ListenerHandler.php │ ├── ActionEventListenerAggregate.php │ ├── DefaultListenerHandler.php │ ├── DetachAggregateHandlers.php │ ├── ActionEventEmitter.php │ ├── ActionEvent.php │ ├── DefaultActionEvent.php │ └── ProophActionEventEmitter.php └── Messaging │ ├── PayloadConstructable.php │ ├── HasMessageName.php │ ├── DomainEvent.php │ ├── Query.php │ ├── Command.php │ ├── NoOpMessageConverter.php │ ├── MessageConverter.php │ ├── PayloadTrait.php │ ├── Message.php │ ├── MessageFactory.php │ ├── FQCNMessageFactory.php │ ├── MessageDataAssertion.php │ └── DomainMessage.php ├── composer.json ├── LICENSE ├── README.md └── CHANGELOG.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | coverage_clover: clover.xml 3 | json_path: coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.laminas-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional_composer_arguments": [ 3 | "--prefer-stable" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | getFinder()->in(__DIR__); 5 | 6 | $config->setCacheFile(__DIR__ . '/.php_cs.cache'); 7 | 8 | return $config; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | /vendor 5 | .idea 6 | .php_cs.cache 7 | /docs/html 8 | nbproject 9 | composer.lock 10 | .phpunit.result.cache 11 | .phpunit.cache 12 | .phpcs-cache 13 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | tags: 8 | 9 | jobs: 10 | ci: 11 | uses: laminas/workflow-continuous-integration/.github/workflows/continuous-integration.yml@1.x 12 | -------------------------------------------------------------------------------- /docs/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Prooph Common", 3 | "content": [ 4 | {"intro": "../README.md"}, 5 | {"messaging": "messaging.md"}, 6 | {"event": "event.md"} 7 | ], 8 | "target": "./html", 9 | "template": "../vendor/prooph/bookdown-template/templates/main.php" 10 | } 11 | -------------------------------------------------------------------------------- /.docheader: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of %package%. 3 | * (c) 2014-%year% Alexander Miertsch 4 | * (c) 2015-%year% Sascha-Oliver Prolic 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | -------------------------------------------------------------------------------- /tests/Mock/InvalidMessage.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | final class InvalidMessage 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/ 5 | 6 | 7 | 8 | ./src 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Event/ListenerHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | interface ListenerHandler 17 | { 18 | public function getActionEventListener(): callable; 19 | } 20 | -------------------------------------------------------------------------------- /src/Messaging/PayloadConstructable.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | interface PayloadConstructable 17 | { 18 | public function __construct(array $payload = []); 19 | } 20 | -------------------------------------------------------------------------------- /src/Messaging/HasMessageName.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * A message implementing this interface is aware of its name. 18 | */ 19 | interface HasMessageName 20 | { 21 | public function messageName(): string; 22 | } 23 | -------------------------------------------------------------------------------- /src/Messaging/DomainEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * This is the base class for domain events. 18 | */ 19 | abstract class DomainEvent extends DomainMessage 20 | { 21 | public function messageType(): string 22 | { 23 | return self::TYPE_EVENT; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Messaging/Query.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * This is the base class for queries used to fetch data from read model. 18 | */ 19 | abstract class Query extends DomainMessage 20 | { 21 | public function messageType(): string 22 | { 23 | return self::TYPE_QUERY; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Mock/AskSomething.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | use Prooph\Common\Messaging\PayloadConstructable; 17 | use Prooph\Common\Messaging\PayloadTrait; 18 | use Prooph\Common\Messaging\Query; 19 | 20 | final class AskSomething extends Query implements PayloadConstructable 21 | { 22 | use PayloadTrait; 23 | } 24 | -------------------------------------------------------------------------------- /tests/Mock/DoSomething.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | use Prooph\Common\Messaging\Command; 17 | use Prooph\Common\Messaging\PayloadConstructable; 18 | use Prooph\Common\Messaging\PayloadTrait; 19 | 20 | final class DoSomething extends Command implements PayloadConstructable 21 | { 22 | use PayloadTrait; 23 | } 24 | -------------------------------------------------------------------------------- /src/Messaging/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * This is the base class for commands used to trigger actions in a domain model. 18 | */ 19 | abstract class Command extends DomainMessage 20 | { 21 | public function messageType(): string 22 | { 23 | return self::TYPE_COMMAND; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Mock/SomethingWasDone.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | use Prooph\Common\Messaging\DomainEvent; 17 | use Prooph\Common\Messaging\PayloadConstructable; 18 | use Prooph\Common\Messaging\PayloadTrait; 19 | 20 | final class SomethingWasDone extends DomainEvent implements PayloadConstructable 21 | { 22 | use PayloadTrait; 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/ActionEventListenerAggregate.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | /** 17 | * An action event listener aggregate interface can itself attach to an ActionEventEmitter. 18 | */ 19 | interface ActionEventListenerAggregate 20 | { 21 | public function attach(ActionEventEmitter $dispatcher): void; 22 | 23 | public function detach(ActionEventEmitter $dispatcher): void; 24 | } 25 | -------------------------------------------------------------------------------- /tests/Mock/ActionEventListenerMock.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | use Prooph\Common\Event\ActionEvent; 17 | 18 | final class ActionEventListenerMock 19 | { 20 | /** 21 | * @var ActionEvent 22 | */ 23 | public $lastEvent; 24 | 25 | /** 26 | * @param ActionEvent $event 27 | */ 28 | public function __invoke(ActionEvent $event): void 29 | { 30 | $this->lastEvent = $event; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Event/DefaultListenerHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | final class DefaultListenerHandler implements ListenerHandler 17 | { 18 | /** 19 | * @var callable 20 | */ 21 | private $listener; 22 | 23 | /** 24 | * @throws \InvalidArgumentException 25 | */ 26 | public function __construct(callable $listener) 27 | { 28 | $this->listener = $listener; 29 | } 30 | 31 | public function getActionEventListener(): callable 32 | { 33 | return $this->listener; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/event.md: -------------------------------------------------------------------------------- 1 | # Event 2 | 3 | ## ActionEventEmitter 4 | 5 | To add event-driven capabilities to prooph components `prooph/common` provides a basic implementation of an event emitter and 6 | an default action event object. We call these events `action events` because we want to differentiate them from domain events. 7 | 8 | A `domain event` describes something happened in the past. 9 | 10 | An `action event` on the other side is an communication object 11 | used to pass parameters from one listener to the next listener during an event-driven process. Furthermore an `action event` 12 | can be stopped which in turn stops the running process. 13 | All prooph components type hint the interfaces `Prooph\Common\Event\ActionEventEmitter`, `Prooph\Common\Event\ActionEventListener/ListenerAggregate`, 14 | `Prooph\Common\Event\ActionEvent` and therefor allow the usage of custom implementations. 15 | If you don't inject your own implementations all components fall back to the default implementations provided by `prooph/common`. 16 | -------------------------------------------------------------------------------- /src/Messaging/NoOpMessageConverter.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | use Assert\Assertion; 17 | 18 | /** 19 | * The NoOpMessageConverter does not perform any conversion logic. 20 | * It simply returns DomainMessage::toArray. 21 | * The converter acts as a default implementation but allows replacement 22 | * with a custom converter using some special logic. 23 | */ 24 | final class NoOpMessageConverter implements MessageConverter 25 | { 26 | public function convertToArray(Message $domainMessage): array 27 | { 28 | Assertion::isInstanceOf($domainMessage, DomainMessage::class); 29 | 30 | return $domainMessage->toArray(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Event/DetachAggregateHandlers.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | /** 17 | * Trait to centralize logic of keeping track of registered ListenerHandlers of a ActionEventListenerAggregate and 18 | * to simplify detaching a ActionEventListenerAggregate. 19 | */ 20 | trait DetachAggregateHandlers 21 | { 22 | /** 23 | * @var ListenerHandler[] 24 | */ 25 | private $handlerCollection = []; 26 | 27 | protected function trackHandler(ListenerHandler $handler): void 28 | { 29 | $this->handlerCollection[] = $handler; 30 | } 31 | 32 | public function detach(ActionEventEmitter $dispatcher): void 33 | { 34 | foreach ($this->handlerCollection as $handler) { 35 | $dispatcher->detachListener($handler); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Messaging/QueryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use PHPUnit\Framework\Attributes\Test; 17 | use PHPUnit\Framework\TestCase; 18 | use Prooph\Common\Messaging\DomainMessage; 19 | use ProophTest\Common\Mock\AskSomething; 20 | use Ramsey\Uuid\Uuid; 21 | 22 | class QueryTest extends TestCase 23 | { 24 | #[Test] 25 | public function it_has_the_message_type_query(): void 26 | { 27 | $query = AskSomething::fromArray([ 28 | 'message_name' => 'TestQuery', 29 | 'uuid' => Uuid::uuid4()->toString(), 30 | 'created_at' => (new \DateTimeImmutable('now', new \DateTimeZone('UTC'))), 31 | 'payload' => ['query' => 'payload'], 32 | 'metadata' => ['query' => 'metadata'], 33 | ]); 34 | 35 | $this->assertEquals(DomainMessage::TYPE_QUERY, $query->messageType()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Mock/ActionListenerAggregateMock.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Mock; 15 | 16 | use Prooph\Common\Event\ActionEvent; 17 | use Prooph\Common\Event\ActionEventEmitter; 18 | use Prooph\Common\Event\ActionEventListenerAggregate; 19 | use Prooph\Common\Event\DetachAggregateHandlers; 20 | 21 | final class ActionListenerAggregateMock implements ActionEventListenerAggregate 22 | { 23 | use DetachAggregateHandlers; 24 | 25 | /** 26 | * @param ActionEventEmitter $dispatcher 27 | */ 28 | public function attach(ActionEventEmitter $dispatcher): void 29 | { 30 | $callable = \Closure::fromCallable([$this, 'onTest']); 31 | $this->trackHandler($dispatcher->attachListener('test', $callable, 100)); 32 | } 33 | 34 | /** 35 | * @param ActionEvent $event 36 | */ 37 | private function onTest(ActionEvent $event): void 38 | { 39 | $event->stopPropagation(true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Messaging/MessageConverter.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * A message converter is able to convert a Message into an array 18 | */ 19 | interface MessageConverter 20 | { 21 | /** 22 | * The result array MUST contain the following data structure: 23 | * 24 | * [ 25 | * 'message_name' => string, 26 | * 'uuid' => string, 27 | * 'payload' => array, //MUST only contain sub arrays and/or scalar types, objects, etc. are not allowed! 28 | * 'metadata' => array, //MUST only contain key/value pairs with values being only scalar types! 29 | * 'created_at' => \DateTimeInterface, 30 | * ] 31 | * 32 | * The correct structure and types are asserted by MessageDataAssertion::assert() 33 | * so make sure that the returned array of your custom conversion logic passes the assertion. 34 | */ 35 | public function convertToArray(Message $domainMessage): array; 36 | } 37 | -------------------------------------------------------------------------------- /src/Messaging/PayloadTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | /** 17 | * Use this trait together with the PayloadConstructable interface 18 | * to use simple message instantiation and default implementations 19 | * for DomainMessage::payload() and DomainMessage::setPayload() 20 | */ 21 | trait PayloadTrait 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected $payload; 27 | 28 | public function __construct(array $payload = []) 29 | { 30 | $this->init(); 31 | $this->setPayload($payload); 32 | } 33 | 34 | public function payload(): array 35 | { 36 | return $this->payload; 37 | } 38 | 39 | protected function setPayload(array $payload): void 40 | { 41 | $this->payload = $payload; 42 | } 43 | 44 | /** 45 | * Use this method to initialize message with defaults or extend your class from DomainMessage 46 | */ 47 | abstract protected function init(): void; 48 | } 49 | -------------------------------------------------------------------------------- /src/Messaging/Message.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | use DateTimeImmutable; 17 | use Ramsey\Uuid\UuidInterface; 18 | 19 | interface Message extends HasMessageName 20 | { 21 | public const TYPE_COMMAND = 'command'; 22 | 23 | public const TYPE_EVENT = 'event'; 24 | 25 | public const TYPE_QUERY = 'query'; 26 | 27 | /** 28 | * Should be one of Message::TYPE_COMMAND, Message::TYPE_EVENT or Message::TYPE_QUERY 29 | */ 30 | public function messageType(): string; 31 | 32 | public function uuid(): UuidInterface; 33 | 34 | public function createdAt(): DateTimeImmutable; 35 | 36 | public function payload(): array; 37 | 38 | public function metadata(): array; 39 | 40 | public function withMetadata(array $metadata): Message; 41 | 42 | /** 43 | * Returns new instance of message with $key => $value added to metadata 44 | * 45 | * Given value must have a scalar or array type. 46 | */ 47 | public function withAddedMetadata(string $key, $value): Message; 48 | } 49 | -------------------------------------------------------------------------------- /src/Messaging/MessageFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | interface MessageFactory 17 | { 18 | /** 19 | * Message data MUST contain at least a "payload" key 20 | * but may also contain "uuid", "message_name", "metadata", and "created_at". 21 | * 22 | * In general the message factory MUST support creating event objects from an array returned by 23 | * the corresponding Prooph\Common\Messaging\MessageConverter 24 | * 25 | * You can use the assertion helper Prooph\Common\Messaging\MessageDataAssertion to assert message data 26 | * before processing it. 27 | * 28 | * If one of the optional keys is not part of the message data the factory should use a default instead: 29 | * For example: 30 | * uuid = Uuid::uuid4() 31 | * message_name = $messageName //First parameter passed to the method 32 | * metadata = [] 33 | * created_at = new \DateTimeImmutable('now', new \DateTimeZone('UTC')) //We treat all dates as UTC 34 | */ 35 | public function createMessageFromArray(string $messageName, array $messageData): Message; 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prooph/common", 3 | "description": "Common classes used across prooph packages", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Alexander Miertsch", 8 | "email": "kontakt@codeliner.ws" 9 | }, 10 | { 11 | "name": "Sascha-Oliver Prolic", 12 | "email": "saschaprolic@googlemail.com" 13 | } 14 | ], 15 | "keywords": [ 16 | "prooph", 17 | "common" 18 | ], 19 | "homepage": "http://getprooph.org/", 20 | "license": "BSD-3-Clause", 21 | "minimum-stability": "dev", 22 | "prefer-stable": true, 23 | "require": { 24 | "php": "^8.1", 25 | "ramsey/uuid": "4.1.2 || ^4.3", 26 | "beberlei/assert": "^2.7.1 || ^3.0" 27 | }, 28 | "require-dev": { 29 | "php-coveralls/php-coveralls": "^2.2", 30 | "phpunit/phpunit": "^10.5.45", 31 | "prooph/bookdown-template": "^0.2.3", 32 | "prooph/php-cs-fixer-config": "^0.5" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Prooph\\Common\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "ProophTest\\Common\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "check": [ 46 | "@cs-check", 47 | "@test" 48 | ], 49 | "cs-check": "php-cs-fixer fix -v --diff --dry-run", 50 | "cs-fix": "php-cs-fixer fix -v --diff", 51 | "test": "phpunit --colors=always" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2025, Alexander Miertsch 2 | Copyright (c) 2015-2025, Sascha-Oliver Prolic 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of prooph nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/Messaging/FQCNMessageFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | use DateTimeImmutable; 17 | use DateTimeZone; 18 | use Ramsey\Uuid\Uuid; 19 | 20 | class FQCNMessageFactory implements MessageFactory 21 | { 22 | public function createMessageFromArray(string $messageName, array $messageData): Message 23 | { 24 | if (! \class_exists($messageName)) { 25 | throw new \UnexpectedValueException('Given message name is not a valid class: ' . (string) $messageName); 26 | } 27 | 28 | if (! \is_subclass_of($messageName, DomainMessage::class)) { 29 | throw new \UnexpectedValueException(\sprintf( 30 | 'Message class %s is not a sub class of %s', 31 | $messageName, 32 | DomainMessage::class 33 | )); 34 | } 35 | 36 | if (! isset($messageData['message_name'])) { 37 | $messageData['message_name'] = $messageName; 38 | } 39 | 40 | if (! isset($messageData['uuid'])) { 41 | $messageData['uuid'] = Uuid::uuid4(); 42 | } 43 | 44 | if (! isset($messageData['created_at'])) { 45 | $messageData['created_at'] = new DateTimeImmutable('now', new DateTimeZone('UTC')); 46 | } 47 | 48 | if (! isset($messageData['metadata'])) { 49 | $messageData['metadata'] = []; 50 | } 51 | 52 | return $messageName::fromArray($messageData); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Event/ActionEventEmitter.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | /** 17 | * An action event dispatcher dispatches ActionEvents which are mutable objects used as a communication mechanism 18 | * between ActionEventListeners listening on the same event and performing actions based on it. 19 | */ 20 | interface ActionEventEmitter 21 | { 22 | /** 23 | * @param null|string $name of the action event 24 | * @param string|object $target of the action event 25 | * @param null|array|\ArrayAccess $params with which the event is initialized 26 | * 27 | * @return ActionEvent that can be triggered by the ActionEventEmitter 28 | */ 29 | public function getNewActionEvent(?string $name = null, $target = null, $params = null): ActionEvent; 30 | 31 | public function dispatch(ActionEvent $event): void; 32 | 33 | /** 34 | * Trigger an event until the given callback returns a boolean true 35 | * 36 | * The callback is invoked after each listener and gets the action event as only argument 37 | */ 38 | public function dispatchUntil(ActionEvent $event, callable $callback): void; 39 | 40 | public function attachListener(string $event, callable $listener, int $priority = 1): ListenerHandler; 41 | 42 | public function detachListener(ListenerHandler $listenerHandler): bool; 43 | 44 | public function attachListenerAggregate(ActionEventListenerAggregate $aggregate): void; 45 | 46 | public function detachListenerAggregate(ActionEventListenerAggregate $aggregate): void; 47 | } 48 | -------------------------------------------------------------------------------- /tests/Messaging/NoOpMessageConverterTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use Assert\InvalidArgumentException; 17 | use PHPUnit\Framework\Attributes\Test; 18 | use PHPUnit\Framework\MockObject\MockObject; 19 | use PHPUnit\Framework\TestCase; 20 | use Prooph\Common\Messaging\DomainMessage; 21 | use Prooph\Common\Messaging\Message; 22 | use Prooph\Common\Messaging\NoOpMessageConverter; 23 | 24 | class NoOpMessageConverterTest extends TestCase 25 | { 26 | #[Test] 27 | public function it_converts_to_array(): void 28 | { 29 | /** @var DomainMessage|MockObject $messageMock */ 30 | $messageMock = $this->getMockBuilder(DomainMessage::class) 31 | ->disableOriginalConstructor() 32 | ->onlyMethods(['toArray', 'setPayload', 'messageType', 'payload']) 33 | ->getMock(); 34 | 35 | $messageMock->expects($this->once())->method('toArray'); 36 | 37 | $converter = new NoOpMessageConverter(); 38 | $converter->convertToArray($messageMock); 39 | } 40 | 41 | #[Test] 42 | public function it_throws_exception_when_message_is_not_a_domain_message(): void 43 | { 44 | $this->expectException(InvalidArgumentException::class); 45 | $this->expectExceptionMessage('was expected to be instanceof of "Prooph\Common\Messaging\DomainMessage" but is not'); 46 | 47 | /** @var Message|MockObject $messageMock */ 48 | $messageMock = $this->getMockBuilder(Message::class) 49 | ->getMock(); 50 | 51 | $converter = new NoOpMessageConverter(); 52 | $converter->convertToArray($messageMock); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Event/ActionEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | /** 17 | * An action event is mutable object used as a communication mechanism for ActionEventListeners listening on the same 18 | * event and performing actions based on the event and its current state. 19 | */ 20 | interface ActionEvent 21 | { 22 | public function getName(): string; 23 | 24 | /** 25 | * Get target/context from which event was triggered 26 | * 27 | * @return null|string|object 28 | */ 29 | public function getTarget(); 30 | 31 | /** 32 | * Get parameters passed to the event 33 | * 34 | * @return array|\ArrayAccess 35 | */ 36 | public function getParams(); 37 | 38 | /** 39 | * Get a single parameter by name 40 | * 41 | * @param string $name 42 | * @param mixed $default Default value to return if parameter does not exist 43 | * 44 | * @return mixed 45 | */ 46 | public function getParam(string $name, $default = null); 47 | 48 | public function setName(string $name): void; 49 | 50 | /** 51 | * Set the event target/context 52 | * 53 | * @param null|string|object $target 54 | * 55 | * @return void 56 | */ 57 | public function setTarget($target): void; 58 | 59 | /** 60 | * Set event parameters 61 | * 62 | * @param array|\ArrayAccess $params 63 | * 64 | * @return void 65 | */ 66 | public function setParams($params): void; 67 | 68 | /** 69 | * Set a single parameter by key 70 | * 71 | * @param string $name 72 | * @param mixed $value 73 | * 74 | * @return void 75 | */ 76 | public function setParam(string $name, $value): void; 77 | 78 | /** 79 | * Indicate whether or not the parent ActionEventEmitter should stop propagating events 80 | * 81 | * @param bool $flag 82 | * 83 | * @return void 84 | */ 85 | public function stopPropagation(bool $flag = true): void; 86 | 87 | /** 88 | * Has this event indicated event propagation should stop? 89 | */ 90 | public function propagationIsStopped(): bool; 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prooph/common 2 | 3 | [![Continuous Integration](https://github.com/prooph/common/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/prooph/common/actions/workflows/continuous-integration.yml) 4 | [![Coverage Status](https://coveralls.io/repos/prooph/common/badge.svg?branch=master)](https://coveralls.io/r/prooph/common?branch=master) 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prooph/improoph) 6 | 7 | Common classes shared between prooph components 8 | 9 | ## Important 10 | 11 | This library will receive support until December 31, 2019 and will then be deprecated. 12 | 13 | For further information see the official announcement here: [https://www.sasaprolic.com/2018/08/the-future-of-prooph-components.html](https://www.sasaprolic.com/2018/08/the-future-of-prooph-components.html) 14 | 15 | ## Note about versions 16 | 17 | The 4.0 release is only for the newer prooph-components (event-store v7, service-bus v6, and so on). If you are using 18 | an older version of prooph/event-store or prooph/service bus, stick to 3.x series. 19 | 20 | ## Shared Kernel 21 | 22 | Prooph components work with [php-fig](http://www.php-fig.org/) standards and other de facto standards like [Container-Interop](https://github.com/container-interop/container-interop) whenever possible. 23 | But they also share some prooph specific classes. These common classes are included in this repository. 24 | 25 | ## Documentation 26 | 27 | Documentation is in the doc tree, and can be compiled using bookdown. 28 | 29 | $ php ./vendor/bin/bookdown docs/bookdown.json 30 | $ php -S 0.0.0.0:8080 -t docs/html/ 31 | 32 | Then browse to http://localhost:8080/ 33 | 34 | ## Changes from 3.x series 35 | 36 | - Minimum requirement is now PHP 7.1 37 | - Add payload-method to Message interface 38 | - Removed version-method from Message interface 39 | - Removed ActionEventListener interface 40 | - Action Event Emitter can accept a list of available event names 41 | - Update to ramsey/uuid 3.5.1 42 | - Update to PHPUnit 6.0 43 | 44 | ## Support 45 | 46 | - Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). 47 | - File issues at [https://github.com/prooph/common/issues](https://github.com/prooph/common/issues). 48 | 49 | ## Contribute 50 | 51 | Please feel free to fork and extend existing or add new features and send a pull request with your changes! 52 | To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. 53 | -------------------------------------------------------------------------------- /tests/Messaging/DomainEventTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use DateTimeImmutable; 17 | use DateTimeZone; 18 | use PHPUnit\Framework\Attributes\Test; 19 | use PHPUnit\Framework\TestCase; 20 | use Prooph\Common\Messaging\DomainEvent; 21 | use Prooph\Common\Messaging\DomainMessage; 22 | use ProophTest\Common\Mock\SomethingWasDone; 23 | use Ramsey\Uuid\Uuid; 24 | use Ramsey\Uuid\UuidInterface; 25 | 26 | class DomainEventTest extends TestCase 27 | { 28 | private DomainEvent $domainEvent; 29 | 30 | private DateTimeImmutable $createdAt; 31 | 32 | private UuidInterface $uuid; 33 | 34 | protected function setUp(): void 35 | { 36 | $this->uuid = Uuid::uuid4(); 37 | $this->createdAt = new DateTimeImmutable('now', new DateTimeZone('UTC')); 38 | 39 | $this->domainEvent = SomethingWasDone::fromArray([ 40 | 'message_name' => 'TestDomainEvent', 41 | 'uuid' => $this->uuid->toString(), 42 | 'created_at' => $this->createdAt, 43 | 'payload' => ['event' => 'payload'], 44 | 'metadata' => ['event' => 'metadata'], 45 | ]); 46 | } 47 | 48 | #[Test] 49 | public function it_has_a_name(): void 50 | { 51 | $this->assertEquals('TestDomainEvent', $this->domainEvent->messageName()); 52 | } 53 | 54 | #[Test] 55 | public function it_has_a_uuid(): void 56 | { 57 | $this->assertTrue($this->uuid->equals($this->domainEvent->uuid())); 58 | } 59 | 60 | #[Test] 61 | public function it_has_created_at_information(): void 62 | { 63 | $this->assertEquals($this->createdAt->format(\DateTime::ISO8601), $this->domainEvent->createdAt()->format(\DateTime::ISO8601)); 64 | } 65 | 66 | #[Test] 67 | public function it_has_payload(): void 68 | { 69 | $this->assertEquals(['event' => 'payload'], $this->domainEvent->payload()); 70 | } 71 | 72 | #[Test] 73 | public function it_has_metadata(): void 74 | { 75 | $this->assertEquals(['event' => 'metadata'], $this->domainEvent->metadata()); 76 | } 77 | 78 | #[Test] 79 | public function it_can_be_converted_to_array_and_back(): void 80 | { 81 | $commandData = $this->domainEvent->toArray(); 82 | 83 | $commandCopy = SomethingWasDone::fromArray($commandData); 84 | 85 | $this->assertEquals($commandData, $commandCopy->toArray()); 86 | } 87 | 88 | #[Test] 89 | public function it_is_of_type_event(): void 90 | { 91 | $this->assertEquals(DomainMessage::TYPE_EVENT, $this->domainEvent->messageType()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Messaging/FQCNMessageFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use DateTimeImmutable; 17 | use PHPUnit\Framework\Attributes\Test; 18 | use PHPUnit\Framework\TestCase; 19 | use Prooph\Common\Messaging\FQCNMessageFactory; 20 | use ProophTest\Common\Mock\DoSomething; 21 | use ProophTest\Common\Mock\InvalidMessage; 22 | use Ramsey\Uuid\Uuid; 23 | 24 | class FQCNMessageFactoryTest extends TestCase 25 | { 26 | private FQCNMessageFactory $messageFactory; 27 | 28 | protected function setUp(): void 29 | { 30 | $this->messageFactory = new FQCNMessageFactory(); 31 | } 32 | 33 | #[Test] 34 | public function it_creates_a_new_message_from_array_and_fqcn(): void 35 | { 36 | $uuid = Uuid::uuid4(); 37 | $createdAt = new DateTimeImmutable(); 38 | 39 | $command = $this->messageFactory->createMessageFromArray(DoSomething::class, [ 40 | 'uuid' => $uuid->toString(), 41 | 'payload' => ['command' => 'payload'], 42 | 'metadata' => ['command' => 'metadata'], 43 | 'created_at' => $createdAt, 44 | ]); 45 | 46 | $this->assertEquals(DoSomething::class, $command->messageName()); 47 | $this->assertEquals($uuid->toString(), $command->uuid()->toString()); 48 | $this->assertEquals($createdAt, $command->createdAt()); 49 | $this->assertEquals(['command' => 'payload'], $command->payload()); 50 | $this->assertEquals(['command' => 'metadata'], $command->metadata()); 51 | } 52 | 53 | #[Test] 54 | public function it_creates_a_new_message_with_defaults_from_array_and_fqcn(): void 55 | { 56 | $command = $this->messageFactory->createMessageFromArray(DoSomething::class, [ 57 | 'payload' => ['command' => 'payload'], 58 | ]); 59 | 60 | $this->assertEquals(DoSomething::class, $command->messageName()); 61 | $this->assertEquals(['command' => 'payload'], $command->payload()); 62 | $this->assertEquals([], $command->metadata()); 63 | } 64 | 65 | #[Test] 66 | public function it_throws_exception_when_message_class_cannot_be_found(): void 67 | { 68 | $this->expectException(\UnexpectedValueException::class); 69 | 70 | $this->messageFactory->createMessageFromArray('NotExistingClass', []); 71 | } 72 | 73 | #[Test] 74 | public function it_throws_exception_when_message_class_is_not_a_sub_class_domain_message(): void 75 | { 76 | $this->expectException(\UnexpectedValueException::class); 77 | 78 | $this->messageFactory->createMessageFromArray(InvalidMessage::class, []); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Messaging/MessageDataAssertion.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | use Assert\Assertion; 17 | use DateTimeImmutable; 18 | 19 | final class MessageDataAssertion 20 | { 21 | /** 22 | * @param mixed $messageData 23 | * 24 | * @return void 25 | */ 26 | public static function assert($messageData): void 27 | { 28 | Assertion::isArray($messageData, 'MessageData must be an array'); 29 | Assertion::keyExists($messageData, 'message_name', 'MessageData must contain a key message_name'); 30 | Assertion::keyExists($messageData, 'uuid', 'MessageData must contain a key uuid'); 31 | Assertion::keyExists($messageData, 'payload', 'MessageData must contain a key payload'); 32 | Assertion::keyExists($messageData, 'metadata', 'MessageData must contain a key metadata'); 33 | Assertion::keyExists($messageData, 'created_at', 'MessageData must contain a key created_at'); 34 | 35 | self::assertMessageName($messageData['message_name']); 36 | self::assertUuid($messageData['uuid']); 37 | self::assertPayload($messageData['payload']); 38 | self::assertMetadata($messageData['metadata']); 39 | self::assertCreatedAt($messageData['created_at']); 40 | } 41 | 42 | public static function assertUuid($uuid): void 43 | { 44 | Assertion::uuid($uuid, 'uuid must be a valid UUID string'); 45 | } 46 | 47 | public static function assertMessageName($messageName): void 48 | { 49 | Assertion::minLength($messageName, 3, 'message_name must be string with at least 3 chars length'); 50 | } 51 | 52 | public static function assertPayload($payload): void 53 | { 54 | Assertion::isArray($payload, 'payload must be an array'); 55 | self::assertSubPayload($payload); 56 | } 57 | 58 | /** 59 | * @param mixed $payload 60 | */ 61 | private static function assertSubPayload($payload): void 62 | { 63 | if (\is_array($payload)) { 64 | foreach ($payload as $subPayload) { 65 | self::assertSubPayload($subPayload); 66 | } 67 | 68 | return; 69 | } 70 | 71 | Assertion::nullOrscalar($payload, 'payload must only contain arrays and scalar values'); 72 | } 73 | 74 | public static function assertMetadata($metadata): void 75 | { 76 | Assertion::isArray($metadata, 'metadata must be an array'); 77 | 78 | foreach ($metadata as $key => $value) { 79 | Assertion::minLength($key, 1, 'A metadata key must be non empty string'); 80 | Assertion::scalar($value, 'A metadata value must have a scalar type. Got ' . \gettype($value) . ' for ' . $key); 81 | } 82 | } 83 | 84 | public static function assertCreatedAt($createdAt): void 85 | { 86 | Assertion::isInstanceOf($createdAt, DateTimeImmutable::class, \sprintf( 87 | 'created_at must be of type %s. Got %s', 88 | DateTimeImmutable::class, 89 | \is_object($createdAt) ? \get_class($createdAt) : \gettype($createdAt) 90 | )); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Messaging/DomainMessage.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Messaging; 15 | 16 | use Assert\Assertion; 17 | use DateTimeImmutable; 18 | use DateTimeZone; 19 | use Ramsey\Uuid\Uuid; 20 | use Ramsey\Uuid\UuidInterface; 21 | use ReflectionClass; 22 | 23 | /** 24 | * Base class for commands, domain events and queries. All are messages but differ in their intention. 25 | */ 26 | abstract class DomainMessage implements Message 27 | { 28 | /** 29 | * @var string 30 | */ 31 | protected $messageName; 32 | 33 | /** 34 | * @var UuidInterface 35 | */ 36 | protected $uuid; 37 | 38 | /** 39 | * @var DateTimeImmutable 40 | */ 41 | protected $createdAt; 42 | 43 | /** 44 | * @var array 45 | */ 46 | protected $metadata = []; 47 | 48 | abstract protected function setPayload(array $payload): void; 49 | 50 | public static function fromArray(array $messageData): DomainMessage 51 | { 52 | MessageDataAssertion::assert($messageData); 53 | 54 | $messageRef = new ReflectionClass(\get_called_class()); 55 | 56 | /** @var $message DomainMessage */ 57 | $message = $messageRef->newInstanceWithoutConstructor(); 58 | 59 | $message->uuid = Uuid::fromString((string) $messageData['uuid']); 60 | $message->messageName = $messageData['message_name']; 61 | $message->metadata = $messageData['metadata']; 62 | $message->createdAt = $messageData['created_at']; 63 | $message->setPayload($messageData['payload']); 64 | 65 | return $message; 66 | } 67 | 68 | protected function init(): void 69 | { 70 | if ($this->uuid === null) { 71 | $this->uuid = Uuid::uuid4(); 72 | } 73 | 74 | if ($this->messageName === null) { 75 | $this->messageName = \get_class($this); 76 | } 77 | 78 | if ($this->createdAt === null) { 79 | $this->createdAt = new DateTimeImmutable('now', new DateTimeZone('UTC')); 80 | } 81 | } 82 | 83 | public function uuid(): UuidInterface 84 | { 85 | return $this->uuid; 86 | } 87 | 88 | public function createdAt(): DateTimeImmutable 89 | { 90 | return $this->createdAt; 91 | } 92 | 93 | public function metadata(): array 94 | { 95 | return $this->metadata; 96 | } 97 | 98 | public function toArray(): array 99 | { 100 | return [ 101 | 'message_name' => $this->messageName, 102 | 'uuid' => $this->uuid->toString(), 103 | 'payload' => $this->payload(), 104 | 'metadata' => $this->metadata, 105 | 'created_at' => $this->createdAt(), 106 | ]; 107 | } 108 | 109 | public function messageName(): string 110 | { 111 | return $this->messageName; 112 | } 113 | 114 | /** 115 | * @return static 116 | */ 117 | public function withMetadata(array $metadata): Message 118 | { 119 | $message = clone $this; 120 | 121 | $message->metadata = $metadata; 122 | 123 | return $message; 124 | } 125 | 126 | /** 127 | * Returns new instance of message with $key => $value added to metadata 128 | * 129 | * Given value must have a scalar type. 130 | * 131 | * @return static 132 | */ 133 | public function withAddedMetadata(string $key, $value): Message 134 | { 135 | Assertion::notEmpty($key, 'Invalid key'); 136 | 137 | $message = clone $this; 138 | 139 | $message->metadata[$key] = $value; 140 | 141 | return $message; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Event/DefaultActionEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | class DefaultActionEvent implements ActionEvent 17 | { 18 | /** 19 | * @var string 20 | */ 21 | protected $name; 22 | 23 | /** 24 | * @var mixed 25 | */ 26 | protected $target; 27 | 28 | /** 29 | * @var array|\ArrayAccess 30 | */ 31 | protected $params; 32 | 33 | /** 34 | * @var bool 35 | */ 36 | protected $stopPropagation = false; 37 | 38 | /** 39 | * @param string $name 40 | * @param null|string|object $target 41 | * @param array|\ArrayAccess|null $params 42 | */ 43 | public function __construct(string $name, $target = null, $params = null) 44 | { 45 | $this->setName($name); 46 | 47 | $this->setTarget($target); 48 | 49 | if ($params === null) { 50 | $params = []; 51 | } 52 | 53 | $this->setParams($params); 54 | } 55 | 56 | public function getName(): string 57 | { 58 | return $this->name; 59 | } 60 | 61 | /** 62 | * Get target/context from which event was triggered 63 | * 64 | * @return null|string|object 65 | */ 66 | public function getTarget() 67 | { 68 | return $this->target; 69 | } 70 | 71 | /** 72 | * Get parameters passed to the event 73 | * 74 | * @return array|\ArrayAccess 75 | */ 76 | public function getParams() 77 | { 78 | return $this->params; 79 | } 80 | 81 | /** 82 | * Get a single parameter by name 83 | * 84 | * @param string $name 85 | * @param mixed $default Default value to return if parameter does not exist 86 | * 87 | * @return mixed 88 | */ 89 | public function getParam(string $name, $default = null) 90 | { 91 | return $this->params[$name] ?? $default; 92 | } 93 | 94 | public function setName(string $name): void 95 | { 96 | $this->name = $name; 97 | } 98 | 99 | /** 100 | * Set the event target/context 101 | * 102 | * @param null|string|object $target 103 | * 104 | * @return void 105 | */ 106 | public function setTarget($target): void 107 | { 108 | $this->target = $target; 109 | } 110 | 111 | /** 112 | * Set event parameters 113 | * 114 | * @param array|\ArrayAccess $params 115 | * 116 | * @return void 117 | * 118 | * @throws \InvalidArgumentException 119 | */ 120 | public function setParams($params): void 121 | { 122 | if (! \is_array($params) && ! $params instanceof \ArrayAccess) { 123 | throw new \InvalidArgumentException('Event params are invalid. Expected type is array or \\ArrayAccess. Got ' . \gettype($params)); 124 | } 125 | 126 | $this->params = $params; 127 | } 128 | 129 | /** 130 | * Set a single parameter by key 131 | * 132 | * @param string $name 133 | * @param mixed $value 134 | * 135 | * @return void 136 | */ 137 | public function setParam(string $name, $value): void 138 | { 139 | $this->params[$name] = $value; 140 | } 141 | 142 | /** 143 | * Indicate whether or not the parent ActionEventEmitter should stop propagating events 144 | */ 145 | public function stopPropagation(bool $flag = true): void 146 | { 147 | $this->stopPropagation = $flag; 148 | } 149 | 150 | /** 151 | * Has this event indicated event propagation should stop? 152 | */ 153 | public function propagationIsStopped(): bool 154 | { 155 | return $this->stopPropagation; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/Event/DefaultActionEventTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Event; 15 | 16 | use PHPUnit\Framework\Attributes\Test; 17 | use PHPUnit\Framework\TestCase; 18 | use Prooph\Common\Event\DefaultActionEvent; 19 | 20 | class DefaultActionEventTest extends TestCase 21 | { 22 | /** 23 | * @return DefaultActionEvent 24 | */ 25 | private function getTestEvent() 26 | { 27 | return new DefaultActionEvent('test-event', 'target', ['param1' => 'foo']); 28 | } 29 | 30 | #[Test] 31 | public function it_can_be_initialized_with_a_name_a_target_and_params(): void 32 | { 33 | $event = $this->getTestEvent(); 34 | 35 | $this->assertEquals('test-event', $event->getName()); 36 | $this->assertEquals('target', $event->getTarget()); 37 | $this->assertEquals(['param1' => 'foo'], $event->getParams()); 38 | } 39 | 40 | #[Test] 41 | public function it_can_initialized_without_a_target_and_params(): void 42 | { 43 | $event = new DefaultActionEvent('test-event'); 44 | 45 | $this->assertNull($event->getTarget()); 46 | $this->assertEquals([], $event->getParams()); 47 | } 48 | 49 | #[Test] 50 | public function it_returns_param_if_set(): void 51 | { 52 | $event = $this->getTestEvent(); 53 | $this->assertEquals('foo', $event->getParam('param1')); 54 | $event->setParam('param1', 'bar'); 55 | $this->assertEquals('bar', $event->getParam('param1')); 56 | } 57 | 58 | #[Test] 59 | public function it_returns_null_if_param_is_not_set_and_no_other_default_is_given(): void 60 | { 61 | $this->assertNull($this->getTestEvent()->getParam('unknown')); 62 | } 63 | 64 | #[Test] 65 | public function it_returns_default_if_param_is_not_set(): void 66 | { 67 | $this->assertEquals('default', $this->getTestEvent()->getParam('unknown', 'default')); 68 | } 69 | 70 | #[Test] 71 | public function it_changes_name_when_new_one_is_set(): void 72 | { 73 | $event = $this->getTestEvent(); 74 | 75 | $event->setName('new name'); 76 | 77 | $this->assertEquals('new name', $event->getName()); 78 | } 79 | 80 | #[Test] 81 | public function it_overrides_params_array_if_new_one_is_set(): void 82 | { 83 | $event = $this->getTestEvent(); 84 | 85 | $event->setParams(['param_new' => 'bar']); 86 | 87 | $this->assertEquals(['param_new' => 'bar'], $event->getParams()); 88 | } 89 | 90 | #[Test] 91 | public function it_allows_object_implementing_array_access_as_params(): void 92 | { 93 | $arrayLikeObject = new \ArrayObject(['object_param' => 'baz']); 94 | 95 | $event = $this->getTestEvent(); 96 | 97 | $event->setParams($arrayLikeObject); 98 | 99 | $this->assertSame($arrayLikeObject, $event->getParams()); 100 | } 101 | 102 | #[Test] 103 | public function it_does_not_allow_params_object_that_is_not_of_type_array_access(): void 104 | { 105 | $this->expectException(\InvalidArgumentException::class); 106 | 107 | $stdObj = new \stdClass(); 108 | 109 | $stdObj->param1 = 'foo'; 110 | 111 | $this->getTestEvent()->setParams($stdObj); 112 | } 113 | 114 | #[Test] 115 | public function it_changes_target_if_new_is_set(): void 116 | { 117 | $event = $this->getTestEvent(); 118 | 119 | $target = new \stdClass(); 120 | 121 | $event->setTarget($target); 122 | 123 | $this->assertSame($target, $event->getTarget()); 124 | } 125 | 126 | #[Test] 127 | public function it_indicates_that_propagation_should_be_stopped(): void 128 | { 129 | $event = $this->getTestEvent(); 130 | 131 | $this->assertFalse($event->propagationIsStopped()); 132 | 133 | $event->stopPropagation(); 134 | 135 | $this->assertTrue($event->propagationIsStopped()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Messaging/CommandTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use DateTimeImmutable; 17 | use DateTimeZone; 18 | use PHPUnit\Framework\Attributes\Test; 19 | use PHPUnit\Framework\TestCase; 20 | use Prooph\Common\Messaging\Command; 21 | use Prooph\Common\Messaging\DomainMessage; 22 | use ProophTest\Common\Mock\DoSomething; 23 | use Ramsey\Uuid\Uuid; 24 | use Ramsey\Uuid\UuidInterface; 25 | 26 | class CommandTest extends TestCase 27 | { 28 | private Command $command; 29 | 30 | private DateTimeImmutable $createdAt; 31 | 32 | private UuidInterface $uuid; 33 | 34 | protected function setUp(): void 35 | { 36 | $this->uuid = Uuid::uuid4(); 37 | $this->createdAt = new DateTimeImmutable('now', new DateTimeZone('UTC')); 38 | 39 | $this->command = DoSomething::fromArray([ 40 | 'message_name' => 'TestCommand', 41 | 'uuid' => $this->uuid->toString(), 42 | 'created_at' => $this->createdAt, 43 | 'payload' => ['command' => 'payload'], 44 | 'metadata' => ['command' => 'metadata'], 45 | ]); 46 | } 47 | 48 | #[Test] 49 | public function it_has_a_name(): void 50 | { 51 | $this->assertEquals('TestCommand', $this->command->messageName()); 52 | } 53 | 54 | #[Test] 55 | public function it_has_a_uuid(): void 56 | { 57 | $this->assertTrue($this->uuid->equals($this->command->uuid())); 58 | } 59 | 60 | #[Test] 61 | public function it_has_created_at_information(): void 62 | { 63 | $this->assertEquals($this->createdAt->format(\DateTime::ISO8601), $this->command->createdAt()->format(\DateTime::ISO8601)); 64 | } 65 | 66 | #[Test] 67 | public function it_has_payload(): void 68 | { 69 | $this->assertEquals(['command' => 'payload'], $this->command->payload()); 70 | } 71 | 72 | #[Test] 73 | public function it_has_metadata(): void 74 | { 75 | $this->assertEquals(['command' => 'metadata'], $this->command->metadata()); 76 | } 77 | 78 | #[Test] 79 | public function it_can_be_converted_to_array_and_back(): void 80 | { 81 | $commandData = $this->command->toArray(); 82 | 83 | $commandCopy = DoSomething::fromArray($commandData); 84 | 85 | $this->assertEquals($commandData, $commandCopy->toArray()); 86 | } 87 | 88 | #[Test] 89 | public function it_returns_new_instance_with_replaced_metadata(): void 90 | { 91 | $newCommand = $this->command->withMetadata(['other' => 'metadata']); 92 | 93 | $this->assertNotSame($this->command, $newCommand); 94 | $this->assertEquals(['command' => 'metadata'], $this->command->metadata()); 95 | $this->assertEquals(['other' => 'metadata'], $newCommand->metadata()); 96 | } 97 | 98 | #[Test] 99 | public function it_returns_new_instance_with_added_metadata(): void 100 | { 101 | $newCommand = $this->command->withAddedMetadata('other', 'metadata'); 102 | 103 | $this->assertNotSame($this->command, $newCommand); 104 | $this->assertEquals(['command' => 'metadata'], $this->command->metadata()); 105 | $this->assertEquals(['command' => 'metadata', 'other' => 'metadata'], $newCommand->metadata()); 106 | } 107 | 108 | #[Test] 109 | public function it_is_initialized_with_defaults(): void 110 | { 111 | $command = new DoSomething(['command' => 'payload']); 112 | 113 | $this->assertEquals(DoSomething::class, $command->messageName()); 114 | $this->assertInstanceOf(UuidInterface::class, $command->uuid()); 115 | $this->assertEquals((new \DateTimeImmutable())->format('Y-m-d'), $command->createdAt()->format('Y-m-d')); 116 | $this->assertEquals(['command' => 'payload'], $command->payload()); 117 | $this->assertEquals([], $command->metadata()); 118 | } 119 | 120 | #[Test] 121 | public function it_is_of_type_command(): void 122 | { 123 | $this->assertEquals(DomainMessage::TYPE_COMMAND, $this->command->messageType()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Event/ProophActionEventEmitter.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Common\Event; 15 | 16 | use Assert\Assertion; 17 | 18 | class ProophActionEventEmitter implements ActionEventEmitter 19 | { 20 | /** 21 | * Map of event name to listeners array 22 | * 23 | * @var array 24 | */ 25 | protected $events = []; 26 | 27 | /** 28 | * @var string[] 29 | */ 30 | protected $availableEventNames = []; 31 | 32 | public function __construct(array $availableEventNames = []) 33 | { 34 | Assertion::allString($availableEventNames, 'Available event names must be an array of strings'); 35 | $this->availableEventNames = $availableEventNames; 36 | } 37 | 38 | /** 39 | * @param null|string $name of the action event 40 | * @param string|object $target of the action event 41 | * @param null|array|\ArrayAccess $params with which the event is initialized 42 | * 43 | * @return ActionEvent that can be triggered by the ActionEventEmitter 44 | */ 45 | public function getNewActionEvent(?string $name = null, $target = null, $params = null): ActionEvent 46 | { 47 | if ($name === null) { 48 | $name = 'action_event'; 49 | } 50 | 51 | return new DefaultActionEvent($name, $target, $params); 52 | } 53 | 54 | public function dispatch(ActionEvent $event): void 55 | { 56 | foreach ($this->getListeners($event) as $listenerHandler) { 57 | $listener = $listenerHandler->getActionEventListener(); 58 | $listener($event); 59 | if ($event->propagationIsStopped()) { 60 | return; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Trigger an event until the given callback returns a boolean true 67 | * 68 | * The callback is invoked after each listener and gets the action event as only argument 69 | */ 70 | public function dispatchUntil(ActionEvent $event, callable $callback): void 71 | { 72 | foreach ($this->getListeners($event) as $listenerHandler) { 73 | $listener = $listenerHandler->getActionEventListener(); 74 | $listener($event); 75 | 76 | if ($event->propagationIsStopped()) { 77 | return; 78 | } 79 | if ($callback($event) === true) { 80 | return; 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Attach a listener to an event 87 | * 88 | * @throws \InvalidArgumentException 89 | */ 90 | public function attachListener(string $event, callable $listener, int $priority = 1): ListenerHandler 91 | { 92 | if (! empty($this->availableEventNames) && ! \in_array($event, $this->availableEventNames, true)) { 93 | throw new \InvalidArgumentException("Unknown event name given: $event"); 94 | } 95 | 96 | $handler = new DefaultListenerHandler($listener); 97 | 98 | $this->events[$event][((int) $priority) . '.0'][] = $handler; 99 | 100 | return $handler; 101 | } 102 | 103 | public function detachListener(ListenerHandler $listenerHandler): bool 104 | { 105 | foreach ($this->events as &$prioritizedListeners) { 106 | foreach ($prioritizedListeners as &$listenerHandlers) { 107 | foreach ($listenerHandlers as $index => $listedListenerHandler) { 108 | if ($listedListenerHandler === $listenerHandler) { 109 | unset($listenerHandlers[$index]); 110 | 111 | return true; 112 | } 113 | } 114 | } 115 | } 116 | 117 | return false; 118 | } 119 | 120 | public function attachListenerAggregate(ActionEventListenerAggregate $aggregate): void 121 | { 122 | $aggregate->attach($this); 123 | } 124 | 125 | public function detachListenerAggregate(ActionEventListenerAggregate $aggregate): void 126 | { 127 | $aggregate->detach($this); 128 | } 129 | 130 | /** 131 | * @param ActionEvent $event 132 | * 133 | * @return ListenerHandler[] 134 | */ 135 | private function getListeners(ActionEvent $event): iterable 136 | { 137 | $prioritizedListeners = $this->events[$event->getName()] ?? []; 138 | 139 | \krsort($prioritizedListeners, SORT_NUMERIC); 140 | 141 | foreach ($prioritizedListeners as $listenersByPriority) { 142 | foreach ($listenersByPriority as $listenerHandler) { 143 | yield $listenerHandler; 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /docs/messaging.md: -------------------------------------------------------------------------------- 1 | # Messaging 2 | 3 | `Prooph\Common\Messaging` provides basic message implementations. These message classes are dispatched by 4 | [prooph/service-bus](https://github.com/prooph/service-bus), persisted by [prooph/event-store](https://github.com/prooph/event-store) and applied to aggregate roots by [prooph/event-sourcing](https://github.com/prooph/event-sourcing). 5 | 6 | ## DomainMessage 7 | 8 | `Prooph\Common\Messaging\DomainMessage` is the basic class for all prooph messages. It is declared abstract and 9 | requires implementers to provide a `messageType` and implementations for `public function payload(): array` and `protected function setPayload(array $payload)`. 10 | 11 | A class constructor is not defined for `Prooph\Common\Messaging\DomainMessage`. It is up to you how you want to instantiate your messages. 12 | Just call `DomainMessage::init()` within your message constructors to initialize a message object with defaults. 13 | 14 | Each `Prooph\Common\Messaging\DomainMessage` 15 | - has a `Ramsey\Uuid uuid` to identify the message in logs or in event streams, allows for duplicate checks and so on, 16 | - a `string messageName` which defaults to the FQCN of the message, used to reconstitute a message 17 | - a `int version` defaults to 0, mainly required for domain events to track version of corresponding aggregate roots 18 | - `array metadata` can contain scalar types or sub arrays, it is recommended to only use it as a hash table for scalar values 19 | - `DateTimeImmutable createdAt` is set when `DomainMessage::init` is invoked, implementers can override `protected $dateTimeFormat = \DateTime::ISO8601` to use another format when message is serialized/deserialized. 20 | 21 | ### Payload 22 | 23 | Since prooph/common 3.x `payload` is no longer part of the message object but instead methods are required to get/set the `payload`. 24 | Payload is the data transported by the message. It should only consist of scalar types and sub arrays so that it can easily be `json_encode`d and `json_decode`d. 25 | Implementers don't need to manage a payload property but `public function payload()` should return the message data. 26 | The `protected function setPayload(array $payload)` method instead should reconstitute message data from given payload array. 27 | 28 | Here is a simple example of a command: 29 | ```php 30 | userId = $userId; 47 | $this->username = $username; 48 | //Initialize message properties: uuid, messageName, createdAt 49 | $this->init(); 50 | } 51 | 52 | public function payload(): array 53 | { 54 | return [ 55 | 'user_id' => $this->userId->toString(), 56 | 'username' => $this->username 57 | ]; 58 | } 59 | 60 | 61 | protected function setPayload(array $payload): void 62 | { 63 | //we skip assertions here for the sake of simplicity 64 | $this->userId = UserId::fromString((string)$payload['user_id']); 65 | $this->username = (string)$payload['username']; 66 | } 67 | } 68 | ``` 69 | 70 | ### PayloadConstructable 71 | 72 | A very simple way to handle payload data is to provide it as an array at construction time. 73 | prooph/common ships with a `Prooph\Common\Messaging\PayloadConstructable` interface and a `Prooph\Common\Messaging\PayloadTrait`. 74 | Use them if you don't want to worry about payload handling. 75 | 76 | ### Metadata 77 | 78 | Message metadata is an array of key => value pairs, where the key is a string and the value has to be a scalar type. 79 | Note that you should NOT use metadata keys beginning with an underscore "_", because this is used internally. 80 | 81 | ## Custom Messages 82 | 83 | If you prefer to work with your own message implementations and want to use third party serializers everything you need to 84 | do is to implement the interface `Prooph\Common\Messaging\Message` and inject custom implementations of 85 | `Prooph\Common\Messaging\MessageFactory` and `Prooph\Common\Messaging\MessageConverter` into prooph components which deal 86 | with message conversion from/to plain PHP arrays. Please refer the docs of the components for details. 87 | 88 | ## Migration from Prooph\Common v3 89 | 90 | The method `Prooph\Common\Messaging\Message::version(): int` has been removed in v4. If you rely on the version-method 91 | you have to implement it yourself and store the version in the metadata, f.e. 92 | 93 | ``` 94 | public function version(): int 95 | { 96 | return $this->metadata['version']; 97 | } 98 | 99 | public function withVersion(int $version): Message 100 | { 101 | $self = clone $this; 102 | $self->setVersion($version); 103 | return $self; 104 | } 105 | 106 | protected function setVersion(int $version): void 107 | { 108 | $this->metadata['version'] = $version; 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /tests/Messaging/MessageDataAssertionTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Messaging; 15 | 16 | use InvalidArgumentException; 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\Attributes\Test; 19 | use PHPUnit\Framework\TestCase; 20 | use Prooph\Common\Messaging\MessageDataAssertion; 21 | use Prooph\Common\Messaging\NoOpMessageConverter; 22 | use ProophTest\Common\Mock\DoSomething; 23 | use Ramsey\Uuid\Uuid; 24 | 25 | class MessageDataAssertionTest extends TestCase 26 | { 27 | #[Test] 28 | public function it_asserts_message_data_returned_by_the_no_op_message_converter(): void 29 | { 30 | $testAssertions = new DoSomething(['test' => 'assertions', ['null' => null]]); 31 | 32 | $messageConverter = new NoOpMessageConverter(); 33 | 34 | MessageDataAssertion::assert($messageConverter->convertToArray($testAssertions)); 35 | 36 | //No exception thrown means test green 37 | $this->assertTrue(true); 38 | } 39 | 40 | #[Test] 41 | #[DataProvider('provideMessageDataWithMissingKey')] 42 | public function it_throws_exception_if_message_data_is_invalid($messageData, $errorMessage) 43 | { 44 | $this->expectException(InvalidArgumentException::class); 45 | $this->expectExceptionMessage($errorMessage); 46 | 47 | MessageDataAssertion::assert($messageData); 48 | } 49 | 50 | public static function provideMessageDataWithMissingKey() 51 | { 52 | $uuid = Uuid::uuid4()->toString(); 53 | $payload = ['foo' => ['bar' => ['baz' => 100]]]; 54 | $metadata = ['key' => 'value', 'string' => 'scalar']; 55 | $createdAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); 56 | 57 | return [ 58 | [ 59 | 'message-data', 60 | 'MessageData must be an array', 61 | ], 62 | [ 63 | //#1 uuid is missing 64 | ['message_name' => 'test-message', 'payload' => $payload, 'metadata' => $metadata, 'created_at' => $createdAt], 65 | 'MessageData must contain a key uuid', 66 | ], 67 | [ 68 | //#2 message_name missing 69 | ['uuid' => $uuid, 'payload' => $payload, 'metadata' => $metadata, 'created_at' => $createdAt], 70 | 'MessageData must contain a key message_name', 71 | ], 72 | [ 73 | //#3 payload missing 74 | ['uuid' => $uuid, 'message_name' => 'test-message', 'metadata' => $metadata, 'created_at' => $createdAt], 75 | 'MessageData must contain a key payload', 76 | ], 77 | [ 78 | //#4 metadata missing 79 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => $payload, 'created_at' => $createdAt], 80 | 'MessageData must contain a key metadata', 81 | ], 82 | [ 83 | //#5 created at missing 84 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => $payload, 'metadata' => $metadata], 85 | 'MessageData must contain a key created_at', 86 | ], 87 | [ 88 | //#6 invalid uuid string 89 | ['uuid' => 'invalid', 'message_name' => 'test-message', 'payload' => $payload, 'metadata' => $metadata, 'created_at' => $createdAt], 90 | 'uuid must be a valid UUID string', 91 | ], 92 | [ 93 | //#7 message name to short 94 | ['uuid' => $uuid, 'message_name' => 'te', 'payload' => $payload, 'metadata' => $metadata, 'created_at' => $createdAt], 95 | 'message_name must be string with at least 3 chars length', 96 | ], 97 | [ 98 | //#8 payload must be an array 99 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => 'string', 'metadata' => $metadata, 'created_at' => $createdAt], 100 | 'payload must be an array', 101 | ], 102 | [ 103 | //#9 payload must not contain objects 104 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => ['sub' => ['key' => new \stdClass()]], 'metadata' => $metadata, 'created_at' => $createdAt], 105 | 'payload must only contain arrays and scalar values', 106 | ], 107 | [ 108 | //#10 metadata must be an array 109 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => $payload, 'metadata' => 'string', 'created_at' => $createdAt], 110 | 'metadata must be an array', 111 | ], 112 | [ 113 | //#11 metadata must not contain non scalar values 114 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => $payload, 'metadata' => ['sub_array' => []], 'created_at' => $createdAt], 115 | 'A metadata value must have a scalar type. Got array for sub_array', 116 | ], 117 | [ 118 | //#12 created_at must be of type \DateTimeImmutable 119 | ['uuid' => $uuid, 'message_name' => 'test-message', 'payload' => $payload, 'metadata' => $metadata, 'created_at' => '2015-08-25 16:30:10'], 120 | 'created_at must be of type DateTimeImmutable. Got string', 121 | ], 122 | ]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/Event/ProophActionEventEmitterTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2015-2025 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Common\Event; 15 | 16 | use PHPUnit\Framework\Attributes\Test; 17 | use PHPUnit\Framework\TestCase; 18 | use Prooph\Common\Event\ActionEvent; 19 | use Prooph\Common\Event\ListenerHandler; 20 | use Prooph\Common\Event\ProophActionEventEmitter; 21 | use ProophTest\Common\Mock\ActionEventListenerMock; 22 | use ProophTest\Common\Mock\ActionListenerAggregateMock; 23 | 24 | class ProophActionEventEmitterTest extends TestCase 25 | { 26 | private ProophActionEventEmitter $proophActionEventEmitter; 27 | 28 | protected function setUp(): void 29 | { 30 | $this->proophActionEventEmitter = new ProophActionEventEmitter(); 31 | } 32 | 33 | #[Test] 34 | public function it_attaches_action_event_listeners_and_dispatch_event_to_them(): void 35 | { 36 | $lastEvent = null; 37 | $listener1 = new ActionEventListenerMock(); 38 | $listener2 = function (ActionEvent $event) use (&$lastEvent): void { 39 | if ($event->getParam('payload', false)) { 40 | $lastEvent = $event; 41 | } 42 | }; 43 | 44 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 45 | 46 | $this->proophActionEventEmitter->attachListener('test', $listener1); 47 | $this->proophActionEventEmitter->attachListener('test', $listener2); 48 | 49 | $this->proophActionEventEmitter->dispatch($actionEvent); 50 | 51 | $this->assertSame($lastEvent, $listener1->lastEvent); 52 | } 53 | 54 | #[Test] 55 | public function it_detaches_a_listener(): void 56 | { 57 | $lastEvent = null; 58 | $listener1 = new ActionEventListenerMock(); 59 | $listener2 = function (ActionEvent $event) use (&$lastEvent): void { 60 | if ($event->getParam('payload', false)) { 61 | $lastEvent = $event; 62 | } 63 | }; 64 | 65 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 66 | 67 | $handler = $this->proophActionEventEmitter->attachListener('test', $listener1); 68 | $this->proophActionEventEmitter->attachListener('test', $listener2); 69 | 70 | $this->proophActionEventEmitter->detachListener($handler); 71 | 72 | $this->proophActionEventEmitter->dispatch($actionEvent); 73 | 74 | $this->assertNull($listener1->lastEvent); 75 | $this->assertSame($actionEvent, $lastEvent); 76 | } 77 | 78 | #[Test] 79 | public function it_triggers_listeners_until_callback_returns_true(): void 80 | { 81 | $lastEvent = null; 82 | $listener1 = new ActionEventListenerMock(); 83 | $listener2 = function (ActionEvent $event) use (&$lastEvent): void { 84 | if ($event->getParam('payload', false)) { 85 | $lastEvent = $event; 86 | } 87 | }; 88 | 89 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 90 | 91 | $this->proophActionEventEmitter->attachListener('test', $listener1); 92 | $this->proophActionEventEmitter->attachListener('test', $listener2); 93 | 94 | $this->proophActionEventEmitter->dispatchUntil($actionEvent, fn (ActionEvent $e) => //We return true directly after first listener was triggered 95 | true); 96 | 97 | $this->assertNull($lastEvent); 98 | $this->assertSame($actionEvent, $listener1->lastEvent); 99 | } 100 | 101 | #[Test] 102 | public function it_stops_dispatching_when_event_propagation_is_stopped(): void 103 | { 104 | $lastEvent = null; 105 | $listener1 = new ActionEventListenerMock(); 106 | $listener2 = function (ActionEvent $event) { 107 | $event->stopPropagation(true); 108 | }; 109 | $listener3 = function (ActionEvent $event) use (&$lastEvent): void { 110 | if ($event->getParam('payload', false)) { 111 | $lastEvent = $event; 112 | } 113 | }; 114 | 115 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 116 | 117 | $this->proophActionEventEmitter->attachListener('test', $listener1); 118 | $this->proophActionEventEmitter->attachListener('test', $listener2); 119 | $this->proophActionEventEmitter->attachListener('test', $listener3); 120 | 121 | $this->proophActionEventEmitter->dispatch($actionEvent); 122 | 123 | $this->assertNull($lastEvent); 124 | $this->assertSame($actionEvent, $listener1->lastEvent); 125 | } 126 | 127 | #[Test] 128 | public function it_stops_dispatching_when_event_propagation_is_stopped_2(): void 129 | { 130 | $lastEvent = null; 131 | $listener1 = new ActionEventListenerMock(); 132 | $listener2 = function (ActionEvent $event) { 133 | }; 134 | $listener3 = function (ActionEvent $event) use (&$lastEvent): void { 135 | if ($event->getParam('payload', false)) { 136 | $lastEvent = $event; 137 | } 138 | }; 139 | 140 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 141 | 142 | $this->proophActionEventEmitter->attachListener('test', $listener1); 143 | $this->proophActionEventEmitter->attachListener('test', $listener2); 144 | $this->proophActionEventEmitter->attachListener('test', $listener3); 145 | 146 | $this->proophActionEventEmitter->dispatchUntil($actionEvent, function (ActionEvent $e) { 147 | $e->stopPropagation(true); 148 | }); 149 | 150 | $this->assertNull($lastEvent); 151 | $this->assertSame($actionEvent, $listener1->lastEvent); 152 | } 153 | 154 | #[Test] 155 | public function it_triggers_listeners_with_high_priority_first(): void 156 | { 157 | $lastEvent = null; 158 | $listener1 = new ActionEventListenerMock(); 159 | $listener2 = function (ActionEvent $event) { 160 | $event->stopPropagation(true); 161 | }; 162 | $listener3 = function (ActionEvent $event) use (&$lastEvent): void { 163 | if ($event->getParam('payload', false)) { 164 | $lastEvent = $event; 165 | } 166 | }; 167 | 168 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 169 | 170 | $this->proophActionEventEmitter->attachListener('test', $listener1, -100); 171 | $this->proophActionEventEmitter->attachListener('test', $listener3); 172 | $this->proophActionEventEmitter->attachListener('test', $listener2, 100); 173 | 174 | $this->proophActionEventEmitter->dispatch($actionEvent); 175 | 176 | $this->assertNull($lastEvent); 177 | $this->assertNull($listener1->lastEvent); 178 | } 179 | 180 | #[Test] 181 | public function it_attaches_a_listener_aggregate(): void 182 | { 183 | $listener1 = new ActionEventListenerMock(); 184 | $listenerAggregate = new ActionListenerAggregateMock(); 185 | 186 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 187 | 188 | $this->proophActionEventEmitter->attachListener('test', $listener1); 189 | $this->proophActionEventEmitter->attachListenerAggregate($listenerAggregate); 190 | 191 | $this->proophActionEventEmitter->dispatch($actionEvent); 192 | 193 | //The listener aggregate attaches itself with a high priority and stops event propagation so $listener1 should not be triggered 194 | $this->assertNull($listener1->lastEvent); 195 | } 196 | 197 | #[Test] 198 | public function it_detaches_listener_aggregate(): void 199 | { 200 | $listener1 = new ActionEventListenerMock(); 201 | $listenerAggregate = new ActionListenerAggregateMock(); 202 | 203 | $actionEvent = $this->proophActionEventEmitter->getNewActionEvent('test', $this, ['payload' => true]); 204 | 205 | $this->proophActionEventEmitter->attachListener('test', $listener1); 206 | $this->proophActionEventEmitter->attachListenerAggregate($listenerAggregate); 207 | $this->proophActionEventEmitter->detachListenerAggregate($listenerAggregate); 208 | 209 | $this->proophActionEventEmitter->dispatch($actionEvent); 210 | 211 | //If aggregate is not detached it would stop the event propagation and $listener1 would not be triggered 212 | $this->assertSame($actionEvent, $listener1->lastEvent); 213 | } 214 | 215 | #[Test] 216 | public function it_uses_default_event_name_if_none_given(): void 217 | { 218 | $event = $this->proophActionEventEmitter->getNewActionEvent(); 219 | $this->assertEquals('action_event', $event->getName()); 220 | } 221 | 222 | #[Test] 223 | public function it_returns_false_when_unattached_listener_handler_gets_detached(): void 224 | { 225 | $listener = $this->getMockBuilder(ListenerHandler::class) 226 | ->onlyMethods(['getActionEventListener']) 227 | ->getMock(); 228 | 229 | $this->assertFalse($this->proophActionEventEmitter->detachListener($listener)); 230 | } 231 | 232 | #[Test] 233 | public function it_dispatches_until_with_no_listeners_attached(): void 234 | { 235 | $this->expectNotToPerformAssertions(); 236 | 237 | $actionEventMock = $this->createMock(ActionEvent::class); 238 | 239 | $this->proophActionEventEmitter->dispatchUntil($actionEventMock, fn () => true); 240 | } 241 | 242 | #[Test] 243 | public function it_attaches_to_known_event_names(): void 244 | { 245 | $this->expectNotToPerformAssertions(); 246 | 247 | $proophActionEventEmitter = new ProophActionEventEmitter(['foo']); 248 | $proophActionEventEmitter->attachListener('foo', function (): void { 249 | }); 250 | } 251 | 252 | #[Test] 253 | public function it_does_not_attach_to_unknown_event_names(): void 254 | { 255 | $this->expectException(\InvalidArgumentException::class); 256 | $this->expectExceptionMessage('Unknown event name given: invalid'); 257 | 258 | $proophActionEventEmitter = new ProophActionEventEmitter(['foo']); 259 | $proophActionEventEmitter->attachListener('invalid', function (): void { 260 | }); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v4.5.2](https://github.com/prooph/common/tree/v4.5.2) 4 | 5 | [Full Changelog](https://github.com/prooph/common/compare/v4.5.1...v4.5.2) 6 | 7 | **Fixed bugs:** 8 | 9 | - wrong uuid version [\#83](https://github.com/prooph/common/pull/83) ([basz](https://github.com/basz)) 10 | 11 | ## [v4.5.1](https://github.com/prooph/common/tree/v4.5.1) (2022-10-31) 12 | 13 | [Full Changelog](https://github.com/prooph/common/compare/v4.5.0...v4.5.1) 14 | 15 | **Implemented enhancements:** 16 | 17 | - add gh ci badge, remove travis file, update readme [\#82](https://github.com/prooph/common/pull/82) ([basz](https://github.com/basz)) 18 | - php74+, github actions, cs [\#81](https://github.com/prooph/common/pull/81) ([basz](https://github.com/basz)) 19 | 20 | ## [v4.5.0](https://github.com/prooph/common/tree/v4.5.0) (2021-01-04) 21 | 22 | [Full Changelog](https://github.com/prooph/common/compare/v4.4.0...v4.5.0) 23 | 24 | **Merged pull requests:** 25 | 26 | - Pr/php8 [\#80](https://github.com/prooph/common/pull/80) ([fritz-gerneth](https://github.com/fritz-gerneth)) 27 | 28 | ## [v4.4.0](https://github.com/prooph/common/tree/v4.4.0) (2020-03-27) 29 | 30 | [Full Changelog](https://github.com/prooph/common/compare/v4.3.0...v4.4.0) 31 | 32 | **Implemented enhancements:** 33 | 34 | - Allow use of ramsey/uuid ^4.0 [\#79](https://github.com/prooph/common/pull/79) ([kochen](https://github.com/kochen)) 35 | 36 | **Closed issues:** 37 | 38 | - Ramsey/uuid v4 is out [\#78](https://github.com/prooph/common/issues/78) 39 | - Only array and scalar in event objects [\#77](https://github.com/prooph/common/issues/77) 40 | - Allow objects in message payload / enforce scalar values only for infrastructure reasons [\#68](https://github.com/prooph/common/issues/68) 41 | 42 | **Merged pull requests:** 43 | 44 | - Change copyright [\#76](https://github.com/prooph/common/pull/76) ([codeliner](https://github.com/codeliner)) 45 | - Update cs headers [\#75](https://github.com/prooph/common/pull/75) ([basz](https://github.com/basz)) 46 | - Add return annotations [\#74](https://github.com/prooph/common/pull/74) ([enumag](https://github.com/enumag)) 47 | 48 | ## [v4.3.0](https://github.com/prooph/common/tree/v4.3.0) (2018-08-22) 49 | 50 | [Full Changelog](https://github.com/prooph/common/compare/v4.2.2...v4.3.0) 51 | 52 | **Closed issues:** 53 | 54 | - RFC: messageName\(\) method is abstract by default [\#72](https://github.com/prooph/common/issues/72) 55 | - RFC: Change createdAt format on MessageConverter [\#71](https://github.com/prooph/common/issues/71) 56 | - RFC: Remove payload from Message [\#70](https://github.com/prooph/common/issues/70) 57 | 58 | ## [v4.2.2](https://github.com/prooph/common/tree/v4.2.2) (2018-03-22) 59 | 60 | [Full Changelog](https://github.com/prooph/common/compare/v4.2.1...v4.2.2) 61 | 62 | **Implemented enhancements:** 63 | 64 | - Fix DomainMessage metadata bottleneck [\#69](https://github.com/prooph/common/pull/69) ([enumag](https://github.com/enumag)) 65 | 66 | ## [v4.2.1](https://github.com/prooph/common/tree/v4.2.1) (2017-12-04) 67 | 68 | [Full Changelog](https://github.com/prooph/common/compare/v4.2.0...v4.2.1) 69 | 70 | **Implemented enhancements:** 71 | 72 | - test php 7.2 [\#67](https://github.com/prooph/common/pull/67) ([prolic](https://github.com/prolic)) 73 | 74 | **Closed issues:** 75 | 76 | - PayloadTrait requires payload to present [\#65](https://github.com/prooph/common/issues/65) 77 | 78 | ## [v4.2.0](https://github.com/prooph/common/tree/v4.2.0) (2017-12-02) 79 | 80 | [Full Changelog](https://github.com/prooph/common/compare/v4.1.0...v4.2.0) 81 | 82 | **Implemented enhancements:** 83 | 84 | - make payload optional [\#66](https://github.com/prooph/common/pull/66) ([prolic](https://github.com/prolic)) 85 | 86 | ## [v4.1.0](https://github.com/prooph/common/tree/v4.1.0) (2017-03-30) 87 | 88 | [Full Changelog](https://github.com/prooph/common/compare/v4.0.0...v4.1.0) 89 | 90 | **Merged pull requests:** 91 | 92 | - Prepare for 4.0 Release [\#62](https://github.com/prooph/common/pull/62) ([prolic](https://github.com/prolic)) 93 | 94 | ## [v4.0.0](https://github.com/prooph/common/tree/v4.0.0) (2017-02-10) 95 | 96 | [Full Changelog](https://github.com/prooph/common/compare/v4.0.0-beta2...v4.0.0) 97 | 98 | **Implemented enhancements:** 99 | 100 | - use UuidInterface instead of Uuid [\#60](https://github.com/prooph/common/pull/60) ([prolic](https://github.com/prolic)) 101 | 102 | **Closed issues:** 103 | 104 | - Update Uuid to latest version [\#61](https://github.com/prooph/common/issues/61) 105 | 106 | **Merged pull requests:** 107 | 108 | - Docs [\#59](https://github.com/prooph/common/pull/59) ([prolic](https://github.com/prolic)) 109 | 110 | ## [v4.0.0-beta2](https://github.com/prooph/common/tree/v4.0.0-beta2) (2017-01-07) 111 | 112 | [Full Changelog](https://github.com/prooph/common/compare/v4.0.0-beta1...v4.0.0-beta2) 113 | 114 | ## [v4.0.0-beta1](https://github.com/prooph/common/tree/v4.0.0-beta1) (2016-12-12) 115 | 116 | [Full Changelog](https://github.com/prooph/common/compare/v3.7.1...v4.0.0-beta1) 117 | 118 | **Implemented enhancements:** 119 | 120 | - Remove DateTimeImmutable hack [\#58](https://github.com/prooph/common/pull/58) ([prolic](https://github.com/prolic)) 121 | - add action event emitter aware interface [\#57](https://github.com/prooph/common/pull/57) ([prolic](https://github.com/prolic)) 122 | - Support for PHP 7.1 [\#56](https://github.com/prooph/common/pull/56) ([prolic](https://github.com/prolic)) 123 | 124 | **Closed issues:** 125 | 126 | - Remove ActionEventListener interface [\#55](https://github.com/prooph/common/issues/55) 127 | - Allow stdClass objects in payload [\#54](https://github.com/prooph/common/issues/54) 128 | - Upgrade to ramsey/uuid 3.x [\#48](https://github.com/prooph/common/issues/48) 129 | 130 | ## [v3.7.1](https://github.com/prooph/common/tree/v3.7.1) (2016-05-14) 131 | 132 | [Full Changelog](https://github.com/prooph/common/compare/v3.7...v3.7.1) 133 | 134 | **Implemented enhancements:** 135 | 136 | - Update to coveralls ^1.0 [\#53](https://github.com/prooph/common/issues/53) 137 | 138 | ## [v3.7](https://github.com/prooph/common/tree/v3.7) (2016-01-30) 139 | 140 | [Full Changelog](https://github.com/prooph/common/compare/v3.6...v3.7) 141 | 142 | **Closed issues:** 143 | 144 | - Wrong email address in php doc [\#44](https://github.com/prooph/common/issues/44) 145 | 146 | **Merged pull requests:** 147 | 148 | - Prepare v3.7 [\#51](https://github.com/prooph/common/pull/51) ([codeliner](https://github.com/codeliner)) 149 | - move setPayload call to allow the method to use information from metadata array \(develop\) [\#50](https://github.com/prooph/common/pull/50) ([pvgnd](https://github.com/pvgnd)) 150 | 151 | ## [v3.6](https://github.com/prooph/common/tree/v3.6) (2015-11-08) 152 | 153 | [Full Changelog](https://github.com/prooph/common/compare/v3.5.5...v3.6) 154 | 155 | **Implemented enhancements:** 156 | 157 | - update composer json [\#45](https://github.com/prooph/common/pull/45) ([prolic](https://github.com/prolic)) 158 | 159 | **Merged pull requests:** 160 | 161 | - added abstract method init\(\) to ensure it's available [\#46](https://github.com/prooph/common/pull/46) ([sandrokeil](https://github.com/sandrokeil)) 162 | 163 | ## [v3.5.5](https://github.com/prooph/common/tree/v3.5.5) (2015-10-02) 164 | 165 | [Full Changelog](https://github.com/prooph/common/compare/v3.5.4...v3.5.5) 166 | 167 | **Fixed bugs:** 168 | 169 | - allow null in payload [\#43](https://github.com/prooph/common/pull/43) ([prolic](https://github.com/prolic)) 170 | 171 | ## [v3.5.4](https://github.com/prooph/common/tree/v3.5.4) (2015-09-15) 172 | 173 | [Full Changelog](https://github.com/prooph/common/compare/v3.5.3...v3.5.4) 174 | 175 | **Merged pull requests:** 176 | 177 | - faster fqcn message factory [\#42](https://github.com/prooph/common/pull/42) ([prolic](https://github.com/prolic)) 178 | 179 | ## [v3.5.3](https://github.com/prooph/common/tree/v3.5.3) (2015-09-01) 180 | 181 | [Full Changelog](https://github.com/prooph/common/compare/v3.5.2...v3.5.3) 182 | 183 | **Merged pull requests:** 184 | 185 | - Bugfix for microtime usage to create date time immutable [\#41](https://github.com/prooph/common/pull/41) ([prolic](https://github.com/prolic)) 186 | - test php7 on travis [\#40](https://github.com/prooph/common/pull/40) ([prolic](https://github.com/prolic)) 187 | 188 | ## [v3.5.2](https://github.com/prooph/common/tree/v3.5.2) (2015-08-26) 189 | 190 | [Full Changelog](https://github.com/prooph/common/compare/v3.5.1...v3.5.2) 191 | 192 | **Implemented enhancements:** 193 | 194 | - Add tests [\#39](https://github.com/prooph/common/pull/39) ([prolic](https://github.com/prolic)) 195 | 196 | ## [v3.5.1](https://github.com/prooph/common/tree/v3.5.1) (2015-08-25) 197 | 198 | [Full Changelog](https://github.com/prooph/common/compare/v3.5...v3.5.1) 199 | 200 | **Closed issues:** 201 | 202 | - Force datetime format to include microseconds [\#36](https://github.com/prooph/common/issues/36) 203 | 204 | **Merged pull requests:** 205 | 206 | - Create DateTime from microseconds [\#38](https://github.com/prooph/common/pull/38) ([codeliner](https://github.com/codeliner)) 207 | 208 | ## [v3.5](https://github.com/prooph/common/tree/v3.5) (2015-08-25) 209 | 210 | [Full Changelog](https://github.com/prooph/common/compare/v3.4...v3.5) 211 | 212 | **Merged pull requests:** 213 | 214 | - Force \DateTimeInterface [\#37](https://github.com/prooph/common/pull/37) ([codeliner](https://github.com/codeliner)) 215 | 216 | ## [v3.4](https://github.com/prooph/common/tree/v3.4) (2015-08-23) 217 | 218 | [Full Changelog](https://github.com/prooph/common/compare/v3.3.1...v3.4) 219 | 220 | **Implemented enhancements:** 221 | 222 | - Add tests [\#34](https://github.com/prooph/common/pull/34) ([prolic](https://github.com/prolic)) 223 | - Add php cs fixer [\#33](https://github.com/prooph/common/pull/33) ([prolic](https://github.com/prolic)) 224 | 225 | **Merged pull requests:** 226 | 227 | - Improve interface descriptions [\#35](https://github.com/prooph/common/pull/35) ([codeliner](https://github.com/codeliner)) 228 | 229 | ## [v3.3.1](https://github.com/prooph/common/tree/v3.3.1) (2015-08-13) 230 | 231 | [Full Changelog](https://github.com/prooph/common/compare/v3.3...v3.3.1) 232 | 233 | ## [v3.3](https://github.com/prooph/common/tree/v3.3) (2015-08-12) 234 | 235 | [Full Changelog](https://github.com/prooph/common/compare/v3.2.1...v3.3) 236 | 237 | **Merged pull requests:** 238 | 239 | - Add a message interface to reduce coupling [\#32](https://github.com/prooph/common/pull/32) ([codeliner](https://github.com/codeliner)) 240 | 241 | ## [v3.2.1](https://github.com/prooph/common/tree/v3.2.1) (2015-08-11) 242 | 243 | [Full Changelog](https://github.com/prooph/common/compare/v3.2...v3.2.1) 244 | 245 | **Closed issues:** 246 | 247 | - DomainMessage treat all Dates as UTC [\#30](https://github.com/prooph/common/issues/30) 248 | 249 | **Merged pull requests:** 250 | 251 | - Treat all dates as UTC [\#31](https://github.com/prooph/common/pull/31) ([codeliner](https://github.com/codeliner)) 252 | 253 | ## [v3.2](https://github.com/prooph/common/tree/v3.2) (2015-07-30) 254 | 255 | [Full Changelog](https://github.com/prooph/common/compare/v3.1...v3.2) 256 | 257 | **Merged pull requests:** 258 | 259 | - Handle defaults [\#29](https://github.com/prooph/common/pull/29) ([codeliner](https://github.com/codeliner)) 260 | - Messaging docs [\#28](https://github.com/prooph/common/pull/28) ([codeliner](https://github.com/codeliner)) 261 | 262 | ## [v3.1](https://github.com/prooph/common/tree/v3.1) (2015-07-26) 263 | 264 | [Full Changelog](https://github.com/prooph/common/compare/v3.0.1...v3.1) 265 | 266 | **Merged pull requests:** 267 | 268 | - Add message converter [\#27](https://github.com/prooph/common/pull/27) ([codeliner](https://github.com/codeliner)) 269 | 270 | ## [v3.0.1](https://github.com/prooph/common/tree/v3.0.1) (2015-07-26) 271 | 272 | [Full Changelog](https://github.com/prooph/common/compare/v3.0...v3.0.1) 273 | 274 | **Merged pull requests:** 275 | 276 | - Rename name property of domain message to message name [\#26](https://github.com/prooph/common/pull/26) ([codeliner](https://github.com/codeliner)) 277 | 278 | ## [v3.0](https://github.com/prooph/common/tree/v3.0) (2015-07-25) 279 | 280 | [Full Changelog](https://github.com/prooph/common/compare/v2.2...v3.0) 281 | 282 | **Closed issues:** 283 | 284 | - Rename ActionEventDispatcher to ActionEventEmitter [\#22](https://github.com/prooph/common/issues/22) 285 | - Replace phpunit with phpspec [\#15](https://github.com/prooph/common/issues/15) 286 | - Require ramsey/uuid [\#14](https://github.com/prooph/common/issues/14) 287 | - Move ZF2ActionEventDispatcher to proophessor [\#13](https://github.com/prooph/common/issues/13) 288 | - Remove PsrZF2Logger [\#12](https://github.com/prooph/common/issues/12) 289 | - Remove ServiceLocator [\#11](https://github.com/prooph/common/issues/11) 290 | - Ad a MessageFactory interface + FQCNMessageFactory [\#10](https://github.com/prooph/common/issues/10) 291 | - Remove RemoteMessage [\#9](https://github.com/prooph/common/issues/9) 292 | - Turn DomainMessage into abstract class [\#8](https://github.com/prooph/common/issues/8) 293 | - Turn ZF2 dependencies into suggestions [\#7](https://github.com/prooph/common/issues/7) 294 | 295 | **Merged pull requests:** 296 | 297 | - Remove no longer required dependencies [\#25](https://github.com/prooph/common/pull/25) ([codeliner](https://github.com/codeliner)) 298 | - Add tests for factory exception cases [\#24](https://github.com/prooph/common/pull/24) ([codeliner](https://github.com/codeliner)) 299 | - Rename ActionEventDispatcher to ActionEventEmitter [\#23](https://github.com/prooph/common/pull/23) ([codeliner](https://github.com/codeliner)) 300 | - Remove ZF2ActionEventDispatcher [\#21](https://github.com/prooph/common/pull/21) ([codeliner](https://github.com/codeliner)) 301 | - Remove PsrLogger [\#20](https://github.com/prooph/common/pull/20) ([codeliner](https://github.com/codeliner)) 302 | - Feature/\#11 [\#19](https://github.com/prooph/common/pull/19) ([codeliner](https://github.com/codeliner)) 303 | - Add MessageFactory interface + FQCNMessageFactory [\#18](https://github.com/prooph/common/pull/18) ([codeliner](https://github.com/codeliner)) 304 | - Remove RemoteMessage [\#17](https://github.com/prooph/common/pull/17) ([codeliner](https://github.com/codeliner)) 305 | - Turn DomainMessage into abstract class [\#16](https://github.com/prooph/common/pull/16) ([codeliner](https://github.com/codeliner)) 306 | 307 | ## [v2.2](https://github.com/prooph/common/tree/v2.2) (2015-05-22) 308 | 309 | [Full Changelog](https://github.com/prooph/common/compare/v2.1...v2.2) 310 | 311 | **Closed issues:** 312 | 313 | - Support new message type Query [\#5](https://github.com/prooph/common/issues/5) 314 | 315 | **Merged pull requests:** 316 | 317 | - Patch-5: Add message type Query [\#6](https://github.com/prooph/common/pull/6) ([codeliner](https://github.com/codeliner)) 318 | 319 | ## [v2.1](https://github.com/prooph/common/tree/v2.1) (2015-05-22) 320 | 321 | [Full Changelog](https://github.com/prooph/common/compare/v2.0...v2.1) 322 | 323 | **Implemented enhancements:** 324 | 325 | - Add own event dispatcher [\#3](https://github.com/prooph/common/issues/3) 326 | 327 | **Merged pull requests:** 328 | 329 | - Patch-3: Add ProophActionEventDispatcher [\#4](https://github.com/prooph/common/pull/4) ([codeliner](https://github.com/codeliner)) 330 | 331 | ## [v2.0](https://github.com/prooph/common/tree/v2.0) (2015-05-09) 332 | 333 | [Full Changelog](https://github.com/prooph/common/compare/v1.5...v2.0) 334 | 335 | ## [v1.5](https://github.com/prooph/common/tree/v1.5) (2015-05-01) 336 | 337 | [Full Changelog](https://github.com/prooph/common/compare/v1.4...v1.5) 338 | 339 | ## [v1.4](https://github.com/prooph/common/tree/v1.4) (2015-04-30) 340 | 341 | [Full Changelog](https://github.com/prooph/common/compare/v1.3...v1.4) 342 | 343 | ## [v1.3](https://github.com/prooph/common/tree/v1.3) (2015-04-30) 344 | 345 | [Full Changelog](https://github.com/prooph/common/compare/v1.2...v1.3) 346 | 347 | ## [v1.2](https://github.com/prooph/common/tree/v1.2) (2015-04-30) 348 | 349 | [Full Changelog](https://github.com/prooph/common/compare/v1.1...v1.2) 350 | 351 | ## [v1.1](https://github.com/prooph/common/tree/v1.1) (2015-03-06) 352 | 353 | [Full Changelog](https://github.com/prooph/common/compare/v1.0...v1.1) 354 | 355 | ## [v1.0](https://github.com/prooph/common/tree/v1.0) (2015-03-05) 356 | 357 | [Full Changelog](https://github.com/prooph/common/compare/6aff163e7c312dde09a7cdf29875459be869e7e3...v1.0) 358 | 359 | **Closed issues:** 360 | 361 | - Wrap ZF2 Service Manager [\#2](https://github.com/prooph/common/issues/2) 362 | - Wrap ZF2 Event Manager [\#1](https://github.com/prooph/common/issues/1) 363 | 364 | 365 | 366 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 367 | --------------------------------------------------------------------------------