├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── couscous.yml └── src ├── Bus ├── MessageBus.php └── Middleware │ ├── FinishesHandlingMessageBeforeHandlingNext.php │ ├── MessageBusMiddleware.php │ └── MessageBusSupportingMiddleware.php ├── CallableResolver ├── CallableCollection.php ├── CallableMap.php ├── CallableResolver.php ├── Exception │ ├── CouldNotResolveCallable.php │ └── UndefinedCallable.php └── ServiceLocatorAwareCallableResolver.php ├── Handler ├── DelegatesToMessageHandlerMiddleware.php └── Resolver │ ├── MessageHandlerResolver.php │ └── NameBasedMessageHandlerResolver.php ├── Logging └── LoggingMiddleware.php ├── Name ├── ClassBasedNameResolver.php ├── Exception │ └── CouldNotResolveMessageName.php ├── MessageNameResolver.php ├── NamedMessage.php └── NamedMessageNameResolver.php ├── Recorder ├── AggregatesRecordedMessages.php ├── ContainsRecordedMessages.php ├── HandlesRecordedMessagesMiddleware.php ├── PrivateMessageRecorderCapabilities.php ├── PublicMessageRecorder.php └── RecordsMessages.php └── Subscriber ├── NotifiesMessageSubscribersMiddleware.php └── Resolver ├── MessageSubscribersResolver.php └── NameBasedMessageSubscriberResolver.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [Unreleased] 4 | 5 | _None yet._ 6 | 7 | ## [1.1.0] - 27-02-2015 8 | 9 | ### Added 10 | 11 | - Added a message bus middleware for logging messages. 12 | - Added a change log. 13 | 14 | ## [1.0.2] - 20-01-2015 15 | 16 | - When an exception occurs in a message handler/subscriber, recorded messages (e.g. events) will now be erased. 17 | 18 | ## [1.0.1] - 20-01-2015 19 | 20 | ### Changed 21 | 22 | - When an exception occurs in a message handler/subscriber, the command bus will no longer be locked. 23 | 24 | ## 1.0.0 - 19-01-2015 25 | 26 | ### Added 27 | 28 | - Import of classes and interfaces from SimpleBus/CommandBus and SimpleBus/EventBus 1.0 29 | - This library applies generically to all kinds of messages. 30 | 31 | [Unreleased]: https://github.com/SimpleBus/MessageBus/compare/v1.1.0...HEAD 32 | [1.1.0]: https://github.com/SimpleBus/MessageBus/compare/v1.0.2...v1.1.0 33 | [1.0.2]: https://github.com/SimpleBus/MessageBus/compare/v1.0.1...v1.0.2 34 | [1.0.1]: https://github.com/SimpleBus/MessageBus/compare/v1.0.0...v1.0.1 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2018 Matthias Noback, Cliff Odijk, Ruud Kamphuis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleBus/MessageBus 2 | 3 | [![Tests Actions Status](https://github.com/SimpleBus/SimpleBus/workflows/Tests/badge.svg)](https://github.com/SimpleBus/SimpleBus/actions) 4 | 5 | By [Matthias Noback](http://php-and-symfony.matthiasnoback.nl/), Cliff Odijk, Ruud Kamphuis 6 | 7 | This package contains generic classes and interfaces which can be used to create message buses, like a [command 8 | bus](https://simplebus.github.io/MessageBus/doc/command_bus.html) or an [event 9 | bus](https://simplebus.github.io/MessageBus/doc/event_bus.html). 10 | 11 | Resources 12 | --------- 13 | 14 | * [Report issues](https://github.com/SimpleBus/SimpleBus/issues) and 15 | [send Pull Requests](https://github.com/SimpleBus/SimpleBus/pulls) 16 | in the [main SimpleBus repository](https://github.com/SimpleBus/SimpleBus) 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-bus/message-bus", 3 | "type": "library", 4 | "description": "Generic classes and interfaces for messages and message buses", 5 | "keywords": [ 6 | "message", 7 | "message bus", 8 | "event bus", 9 | "command bus" 10 | ], 11 | "homepage": "http://github.com/SimpleBus/MessageBus", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Cliff Odijk", 16 | "email": "cliff@jcid.nl" 17 | }, 18 | { 19 | "name": "Ruud Kamphuis", 20 | "homepage": "https://github.com/ruudk" 21 | }, 22 | { 23 | "name": "Matthias Noback", 24 | "email": "matthiasnoback@gmail.com", 25 | "homepage": "http://php-and-symfony.matthiasnoback.nl" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.0", 30 | "psr/log": "^1.1.4 || ^2.0 || ^3.0" 31 | }, 32 | "require-dev": { 33 | "ergebnis/composer-normalize": "^2.11", 34 | "phpunit/phpunit": "^9.5.5", 35 | "symfony/phpunit-bridge": "^6.0" 36 | }, 37 | "config": { 38 | "sort-packages": true 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "SimpleBus\\Message\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "SimpleBus\\Message\\Tests\\": "tests" 48 | } 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /couscous.yml: -------------------------------------------------------------------------------- 1 | title: SimpleBus/MessageBus 2 | subTitle: Generic classes and interfaces for command and event buses 3 | baseUrl: //simplebus.github.io/MessageBus 4 | menu: 5 | items: 6 | home: 7 | itemId: home 8 | text: Home 9 | relativeUrl: "" 10 | getting_started: 11 | itemId: getting_started 12 | text: Getting started 13 | relativeUrl: doc/getting_started.html 14 | command_bus: 15 | itemId: command_bus 16 | text: Command bus 17 | relativeUrl: doc/command_bus.html 18 | event_bus: 19 | itemId: event_bus 20 | text: Event bus 21 | relativeUrl: doc/event_bus.html 22 | message_recorder: 23 | itemId: message_recorder 24 | text: Recording events 25 | relativeUrl: doc/message_recorder.html 26 | upgrade_guide: 27 | itemId: upgrade_guide 28 | text: Upgrade guide 29 | relativeUrl: doc/upgrade_guide.html 30 | doctrine_orm_integration: 31 | text: Doctrine ORM integration 32 | absoluteUrl: http://simplebus.github.io/DoctrineORMBridge/ 33 | symfony_integration: 34 | text: Symfony integration 35 | absoluteUrl: http://simplebus.github.io/SymfonyBridge/ 36 | -------------------------------------------------------------------------------- /src/Bus/MessageBus.php: -------------------------------------------------------------------------------- 1 | queue[] = $message; 22 | 23 | if (!$this->isHandling) { 24 | $this->isHandling = true; 25 | 26 | while ($message = array_shift($this->queue)) { 27 | try { 28 | $next($message); 29 | } catch (Throwable $exception) { 30 | $this->isHandling = false; 31 | 32 | throw $exception; 33 | } 34 | } 35 | 36 | $this->isHandling = false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Bus/Middleware/MessageBusMiddleware.php: -------------------------------------------------------------------------------- 1 | appendMiddleware($middleware); 21 | } 22 | } 23 | 24 | /** 25 | * Appends new middleware for this message bus. Should only be used at configuration time. 26 | * 27 | * @private 28 | */ 29 | public function appendMiddleware(MessageBusMiddleware $middleware): void 30 | { 31 | $this->middlewares[] = $middleware; 32 | } 33 | 34 | /** 35 | * Returns a list of middlewares. Should only be used for introspection. 36 | * 37 | * @private 38 | * 39 | * @return MessageBusMiddleware[] 40 | */ 41 | public function getMiddlewares(): array 42 | { 43 | return $this->middlewares; 44 | } 45 | 46 | /** 47 | * Prepends new middleware for this message bus. Should only be used at configuration time. 48 | * 49 | * @private 50 | */ 51 | public function prependMiddleware(MessageBusMiddleware $middleware): void 52 | { 53 | array_unshift($this->middlewares, $middleware); 54 | } 55 | 56 | public function handle(object $message): void 57 | { 58 | call_user_func($this->callableForNextMiddleware(0), $message); 59 | } 60 | 61 | private function callableForNextMiddleware(int $index): callable 62 | { 63 | if (!isset($this->middlewares[$index])) { 64 | return function () { 65 | }; 66 | } 67 | 68 | $middleware = $this->middlewares[$index]; 69 | 70 | return function ($message) use ($middleware, $index) { 71 | $middleware->handle($message, $this->callableForNextMiddleware($index + 1)); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/CallableResolver/CallableCollection.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | private array $callablesByName; 11 | 12 | private CallableResolver $callableResolver; 13 | 14 | /** 15 | * @param array $callablesByName 16 | */ 17 | public function __construct( 18 | array $callablesByName, 19 | CallableResolver $callableResolver 20 | ) { 21 | foreach ($callablesByName as $callable) { 22 | if (!is_array($callable)) { 23 | throw new \InvalidArgumentException('You need to provide arrays of callables, indexed by name'); 24 | } 25 | } 26 | 27 | $this->callablesByName = $callablesByName; 28 | $this->callableResolver = $callableResolver; 29 | } 30 | 31 | /** 32 | * @return callable[] 33 | */ 34 | public function filter(string $name): array 35 | { 36 | if (!array_key_exists($name, $this->callablesByName)) { 37 | return []; 38 | } 39 | 40 | $callables = $this->callablesByName[$name]; 41 | 42 | return array_map([$this->callableResolver, 'resolve'], $callables); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/CallableResolver/CallableMap.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $callablesByName; 13 | 14 | private CallableResolver $callableResolver; 15 | 16 | /** 17 | * @param array $callablesByName 18 | */ 19 | public function __construct( 20 | array $callablesByName, 21 | CallableResolver $callableResolver 22 | ) { 23 | $this->callablesByName = $callablesByName; 24 | $this->callableResolver = $callableResolver; 25 | } 26 | 27 | public function get(string $name): callable 28 | { 29 | if (!array_key_exists($name, $this->callablesByName)) { 30 | throw new UndefinedCallable(sprintf('Could not find a callable for name "%s"', $name)); 31 | } 32 | 33 | $callable = $this->callablesByName[$name]; 34 | 35 | return $this->callableResolver->resolve($callable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CallableResolver/CallableResolver.php: -------------------------------------------------------------------------------- 1 | serviceLocator = $serviceLocator; 17 | } 18 | 19 | /** 20 | * @param callable|mixed|object|string $maybeCallable 21 | */ 22 | public function resolve($maybeCallable): callable 23 | { 24 | if (is_callable($maybeCallable)) { 25 | return $maybeCallable; 26 | } 27 | 28 | if (is_string($maybeCallable)) { 29 | // a string can be converted to an object, which may then be a callable 30 | return $this->resolve($this->loadService($maybeCallable)); 31 | } 32 | 33 | // to make the upgrade process easier: auto-select the "handle" method 34 | if (is_object($maybeCallable) && method_exists($maybeCallable, 'handle')) { 35 | return [$maybeCallable, 'handle']; 36 | } 37 | 38 | // to make the upgrade process easier: auto-select the "notify" method 39 | if (is_object($maybeCallable) && method_exists($maybeCallable, 'notify')) { 40 | return [$maybeCallable, 'notify']; 41 | } 42 | 43 | if (is_array($maybeCallable) && 2 === count($maybeCallable)) { 44 | // Symfony 3.3 supports services by classname. This interferes with `is_callable` above 45 | // so the SymfonyBridge will now use an array with `serviceId`, `method` keys. 46 | if (array_key_exists('serviceId', $maybeCallable)) { 47 | $serviceId = $maybeCallable['serviceId']; 48 | $method = $maybeCallable['method']; 49 | } else { 50 | [$serviceId, $method] = $maybeCallable; 51 | } 52 | 53 | if (is_string($serviceId)) { 54 | return $this->resolve([$this->loadService($serviceId), $method]); 55 | } 56 | } 57 | 58 | throw CouldNotResolveCallable::createFor($maybeCallable); 59 | } 60 | 61 | private function loadService(string $serviceId): object 62 | { 63 | return call_user_func($this->serviceLocator, $serviceId); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Handler/DelegatesToMessageHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | messageHandlerResolver = $messageHandlerResolver; 15 | } 16 | 17 | /** 18 | * Handles the message by resolving the correct message handler and calling it. 19 | */ 20 | public function handle(object $message, callable $next): void 21 | { 22 | $handler = $this->messageHandlerResolver->resolve($message); 23 | call_user_func($handler, $message); 24 | 25 | $next($message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Handler/Resolver/MessageHandlerResolver.php: -------------------------------------------------------------------------------- 1 | messageNameResolver = $messageNameResolver; 17 | $this->messageHandlers = $messageHandlers; 18 | } 19 | 20 | public function resolve(object $message): callable 21 | { 22 | $name = $this->messageNameResolver->resolve($message); 23 | 24 | return $this->messageHandlers->get($name); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Logging/LoggingMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 17 | $this->level = $level; 18 | } 19 | 20 | public function handle(object $message, callable $next): void 21 | { 22 | $this->logger->log($this->level, 'Started handling a message', ['message' => $message]); 23 | 24 | $next($message); 25 | 26 | $this->logger->log($this->level, 'Finished handling a message', ['message' => $message]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Name/ClassBasedNameResolver.php: -------------------------------------------------------------------------------- 1 | addMessageRecorder($messageRecorder); 19 | } 20 | } 21 | 22 | /** 23 | * Get messages recorded by all known message recorders. 24 | * 25 | * @return object[] 26 | */ 27 | public function recordedMessages(): array 28 | { 29 | $allRecordedMessages = []; 30 | 31 | foreach ($this->messageRecorders as $messageRecorder) { 32 | $allRecordedMessages = array_merge($allRecordedMessages, $messageRecorder->recordedMessages()); 33 | } 34 | 35 | return $allRecordedMessages; 36 | } 37 | 38 | /** 39 | * Erase messages recorded by all known message recorders. 40 | */ 41 | public function eraseMessages(): void 42 | { 43 | foreach ($this->messageRecorders as $messageRecorder) { 44 | $messageRecorder->eraseMessages(); 45 | } 46 | } 47 | 48 | private function addMessageRecorder(ContainsRecordedMessages $messageRecorder): void 49 | { 50 | $this->messageRecorders[] = $messageRecorder; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Recorder/ContainsRecordedMessages.php: -------------------------------------------------------------------------------- 1 | messageRecorder = $messageRecorder; 18 | $this->messageBus = $messageBus; 19 | } 20 | 21 | public function handle(object $message, callable $next): void 22 | { 23 | try { 24 | $next($message); 25 | } catch (Exception $exception) { 26 | $this->messageRecorder->eraseMessages(); 27 | 28 | throw $exception; 29 | } 30 | 31 | $recordedMessages = $this->messageRecorder->recordedMessages(); 32 | 33 | $this->messageRecorder->eraseMessages(); 34 | 35 | foreach ($recordedMessages as $recordedMessage) { 36 | $this->messageBus->handle($recordedMessage); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Recorder/PrivateMessageRecorderCapabilities.php: -------------------------------------------------------------------------------- 1 | messages; 22 | } 23 | 24 | public function eraseMessages(): void 25 | { 26 | $this->messages = []; 27 | } 28 | 29 | /** 30 | * Record a message. 31 | */ 32 | protected function record(object $message): void 33 | { 34 | $this->messages[] = $message; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Recorder/PublicMessageRecorder.php: -------------------------------------------------------------------------------- 1 | messageSubscribersResolver = $messageSubscribersResolver; 25 | $this->logger = $logger ?? new NullLogger(); 26 | $this->level = $level ?? LogLevel::DEBUG; 27 | } 28 | 29 | public function handle(object $message, callable $next): void 30 | { 31 | $messageSubscribers = $this->messageSubscribersResolver->resolve($message); 32 | 33 | foreach ($messageSubscribers as $messageSubscriber) { 34 | $this->logger->log($this->level, 'Started notifying a subscriber', ['subscriber' => $messageSubscriber]); 35 | 36 | call_user_func($messageSubscriber, $message); 37 | 38 | $this->logger->log($this->level, 'Finished notifying a subscriber', ['subscriber' => $messageSubscriber]); 39 | } 40 | 41 | $next($message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Subscriber/Resolver/MessageSubscribersResolver.php: -------------------------------------------------------------------------------- 1 | messageNameResolver = $messageNameResolver; 17 | $this->messageSubscribers = $messageSubscribers; 18 | } 19 | 20 | public function resolve(object $message): array 21 | { 22 | $name = $this->messageNameResolver->resolve($message); 23 | 24 | return $this->messageSubscribers->filter($name); 25 | } 26 | } 27 | --------------------------------------------------------------------------------