{"id":16673560,"url":"https://github.com/sasa-b/command-bus","last_synced_at":"2025-08-18T09:33:53.479Z","repository":{"id":62540390,"uuid":"322605241","full_name":"sasa-b/command-bus","owner":"sasa-b","description":"Command Bus pattern implementation with an event and middleware system. PSR-11 and PSR-3 compliant","archived":false,"fork":false,"pushed_at":"2024-04-24T10:02:45.000Z","size":229,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-04-24T22:38:45.470Z","etag":null,"topics":["command-bus","commandbus","message-bus","messagebus","php","php-library","psr-11","psr-3"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sasa-b.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-18T13:37:48.000Z","updated_at":"2024-04-24T10:02:37.000Z","dependencies_parsed_at":"2023-02-18T05:15:25.870Z","dependency_job_id":"eb1946cd-dc1d-49a2-b39d-a3989aa67cb2","html_url":"https://github.com/sasa-b/command-bus","commit_stats":{"total_commits":66,"total_committers":2,"mean_commits":33.0,"dds":0.0757575757575758,"last_synced_commit":"b12591307d1bbb3625ba6e7b815b0e6b9da446b0"},"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa-b%2Fcommand-bus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa-b%2Fcommand-bus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa-b%2Fcommand-bus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa-b%2Fcommand-bus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sasa-b","download_url":"https://codeload.github.com/sasa-b/command-bus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221817438,"owners_count":16885537,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command-bus","commandbus","message-bus","messagebus","php","php-library","psr-11","psr-3"],"created_at":"2024-10-12T12:27:12.828Z","updated_at":"2024-10-28T10:33:37.019Z","avatar_url":"https://github.com/sasa-b.png","language":"PHP","readme":"# Message Bus Pattern \n\nColloquially more known as **Command Bus** pattern, but the library makes a distinction between **Commands** and **Queries** and allows you to \nenforce no return values in Command Handlers to keep you in line with [CQRS pattern](https://martinfowler.com/bliki/CQRS.html).\n\nThis is a **stand-alone library**, the only two dependencies being the [PSR-11 Container](https://www.php-fig.org/psr/psr-11/) and [PSR-3 Log](https://www.php-fig.org/psr/psr-3/) interfaces to allow for better \ninteroperability.\n\n**Table of Contents:**\n* [Getting Started](#getting-started)\n    * [Stand-alone usage](#stand-alone-usage)\n    * [Using with Symfony Framework](#using-with-symfony-framework)\n    * [Using with Laravel Framework](#using-with-laravel-framework)\n* [Core Concepts](#core-concepts)\n  * [Identity](#identity)\n  * [Handler Mapping Strategy](#handler-mapping-strategy)\n  * [Middleware](#middleware)\n  * [Event](#event)\n  * [Transaction](#transaction)\n  * ~~[Result Types](#result-types)~~ (Removed in version 3.0)\n\n## Getting Started\n\nInstall the library using composer:\n```bash\ncomposer require sco/message-bus\n```\n\n### Stand-alone usage\n\nYou will need to follow the [PSR-4 autoloading standard](https://www.php-fig.org/psr/psr-4/) and either create your own Service Container class, which is a matter of implementing the `Psr\\Container\\ContainerInterface` and can be as simple as what\nthe library is using for its test suite `Sco\\MessageBus\\Tests\\Stub\\Container\\InMemoryContainer`, or you can composer require a Service Container library which\nadheres to the [PSR-11 Standard](https://www.php-fig.org/psr/psr-11/) like [PHP-DI](https://php-di.org/).\n\n```php\nrequire 'vendor/autoload.php'\n\n$container = new InMemoryContainer($services)\n\n$bus = new \\Sco\\MessageBus\\Bus($container);\n\n$bus-\u003edispatch(new FindPostByIdQuery(1))\n```\n\n### Using with Symfony Framework\n\nWe can use two approaches here, decorating the Bus class provided by the library, or injecting the Service Locator. For more\ninfo you can read [Symfony Docs](https://symfony.com/doc/current/service_container/service_subscribers_locators.html)\n\n#### Decorating the Bus\nWe can create a new Decorator class which will implement Symfony's `Symfony\\Contracts\\Service\\ServiceSubscriberInterface` interface:\n\n```php\nuse Sco\\MessageBus\\Bus;\nuse Sco\\MessageBus\\Message;\nuse Sco\\MessageBus\\Result;\nuse Psr\\Container\\ContainerInterface;\nuse Symfony\\Contracts\\Service\\ServiceSubscriberInterface;\n\nclass MessageBus implements ServiceSubscriberInterface\n{\n    private Bus $bus;\n\n    public function __construct(ContainerInterface $locator)\n    {\n        $this-\u003ebus = new Bus($locator, [], null, new UuidV4Identity());\n    }\n\n    public function dispatch(\\Sco\\MessageBus\\Message $message): Result\n    {\n        return $this-\u003ebus-\u003edispatch($message);\n    }\n\n    public static function getSubscribedServices(): array\n    {\n        return [\n            FindPostByIdHandler::class,\n            SavePostHandler::class\n        ];\n    }\n}\n```\nWith this approach all handlers in you application will have to be added to the array returned by `getSubscribedServices`, since services in Symfony are not\npublic by default, and they really shouldn't be, so unless you add your handlers to this array when the mapper is done mapping\nit won't be able to find the handler and a service not found container exception will be thrown.\n\n#### Injecting the ServiceLocator\n\nA different approach would be to inject a Service Locator with all the handlers into the library's Bus. This would be done in the\nservice registration yaml files.\n\nAnonymous service locator:\n```yaml\nservices:\n    _defaults:\n      autowire: true      \n      autoconfigure: true \n\n    # Anonymous Service Locator\n    Sco\\MessageBus\\Bus:\n      arguments:\n        $container: !service_locator\n                        '@FindPostByIdHandler': 'handler_one'\n                        '@SavePostHandler': 'handler_two'\n```\n\nExplicit service locator definition:\n```yaml\nservices:\n    _defaults:\n      autowire: true      \n      autoconfigure: true \n\n    # Explicit Service Locator\n    message_handler_service_locator:\n      class: Symfony\\Component\\DependencyInjection\\ServiceLocator\n      arguments:\n          - '@FindPostByIdHandler'\n          - '@SavePostHandler' \n\n    Sco\\MessageBus\\Bus:\n      arguments:\n        $container: '@message_handler_service_locator'\n```\n\nLet's expand these configurations and use the tags feature of Symfony's service container to automatically add handlers to the Bus:\n\nUsing `!tagged_locator`:\n```yaml\nservices:\n  _defaults:\n    autowire: true\n    autoconfigure: true\n    \n  _instanceof: \n    Sco\\MessageBus\\Handler:\n      tags: ['message_handler']\n\n  # Anonymous Service Locator\n  Sco\\MessageBus\\Bus:\n    arguments:\n      $container: !tagged_locator message_handler\n```\n\nExplicit service locator definition:\n```yaml\nservices:\n  _defaults:\n    autowire: true\n    autoconfigure: true\n\n  _instanceof:\n    Sco\\MessageBus\\Handler:\n      tags: ['message_handler']\n      \n  # Explicit Service Locator\n  message_handler_service_locator:\n    class: Symfony\\Component\\DependencyInjection\\ServiceLocator\n    arguments:\n      - !tagged_iterator message_handler\n\n  Sco\\MessageBus\\Bus:\n    arguments:\n      $container: '@message_handler_service_locator'\n```\n\n### Using with Laravel Framework\nTo use it effectively with Laravel framework all you have to do is register the Bus in [Laravel's Service Container](https://laravel.com/docs/9.x/container) and provide the container as an argument to the library's Bus class:\n```php\n$this-\u003eapp-\u003ebind(\\Sco\\MessageBus\\Bus::class, function ($app) {\n    return new \\Sco\\MessageBus\\Bus($app);\n});\n```\n\n## Core Concepts\n\n### Identity\nEach _Command_ or _Query_ and their respective _Result_ object combo will be assigned a unique Identity, e.g. a _Command,_ and its respective _Result_ object will have and identity of `00000001`. \nThis can be useful for logging, auditing or debugging purposes. \n\nThe default Identity generation strategy is a simple `Sco\\MessageBus\\Identity\\RandomString` generator to keep the external dependencies to a minimum. To use something else you could require a library like https://github.com/ramsey/uuid and implement the `\\Sco\\MessageBus\\Identity`.\n\n```php\nuse Sco\\MessageBus\\Identity;\n\nclass UuidIdentity implements Identity\n{\n    public function generate() : string\n    {\n        return Uuid::uuid7()-\u003etoString();\n    }\n}\n```\n\n### Handler Mapping Strategy\n1. **MapByName** - this strategy takes into account the [FQN](https://www.php.net/manual/en/language.namespaces.rules.php) and requires a _Command_ or _Query_ suffix in the class name. \nFor example an `FindPostByIdQuery` will get mapped to `FindPostByIdHandler` or a `SavePostCommand` will get mapped to `SavePostHandler`.\n2. **MapByAttribute** - this strategy uses PHP attributes, add either `#[IsCommand(handler: SavePostHandler::class)]` or `#[IsQuery(handler: FindPostByIdHandler::class)]` to your Command/Query class. The `handler` parameter name can be omitted, it's up to your personal preference.\n3. **Custom** - if you want to create your own custom mapping strategy you can do so by implementing the `Sco\\MessageBus\\Mapper` interface.\n\n### Middleware\nEach command will be passed through a chain of Middlewares. By default the chain is empty, but the library does offer \nsome Middleware out of the box:\n* **EventMiddleware** - raises events before and after handling a command or query, and on failure\n* **TransactionMiddleware** - runs individual _Commands_ or _Queries_ in a Transaction, `begin`, `commit` and `rollback` steps are plain `\\Closure` objects, so you can use whichever ORM or Persistence approach you prefer. \n* **EmptyResultMiddleware** - throws an Exception if anything aside from null is returned in _Command_ Results to enforce the _Command-Query Segregation_\n* **ImmutableResultMiddleware** - throws an Exception if you have properties without _readonly_ modifier defined on your Result objects\n\nTo create your own custom middleware you need to implement the `Sco\\MessageBus\\Middleware` interface and provide it\nto the bus:\n\n```php\nuse Sco\\MessageBus\\Bus;\nuse Sco\\MessageBus\\Message;\nuse Sco\\MessageBus\\Middleware;\n\nclass CustomMiddleware implements Middleware\n{\n    public function __invoke(Message $message,\\Closure $next) : mixed\n    {\n        // Do something before message handling\n        \n        $result = $next($message);\n        \n        // Do something after message handling\n        \n        return $result;\n    }\n}\n\n$bus = new Bus(middlewares: [new CustomMiddleware()]);\n```\n\n### Event\nIf you add the `Sco\\MessageBus\\Middleware\\EventMiddleware` you will be able to subscribe to the following events:\n\n**MessageReceivedEvent** - raised when the message is received but before being handled.\n```php\nuse Sco\\MessageBus\\Event\\Subscriber;\nuse Sco\\MessageBus\\Event\\MessageReceivedEvent;\n\n$subscriber = new Subscriber();\n\n$subscriber-\u003eaddListener(MessageReceivedEvent::class, function (MessageReceivedEvent $event) {\n  $event-\u003egetName(); // Name of the Event\n  $event-\u003egetMessage();; // Command or Query that has been received\n});\n```\n\n**MessageHandledEvent** - raised after the message has been handled successfully.\n\n```php\nuse Sco\\MessageBus\\Event\\Subscriber;\nuse Sco\\MessageBus\\Event\\MessageHandledEvent;\n\n$subscriber = new Subscriber();\n\n$subscriber-\u003eaddListener(MessageHandledEvent::class, function (MessageHandledEvent $event) {\n    $event-\u003egetName(); // Name of the Event\n    $event-\u003egetMessage(); // Command or Query being handled\n    $event-\u003egetResult(); // Result for the handled message\n});\n```\n\n**MessageFailedEvent** - raised when the message handling fails and an exception gets thrown.\n```php\nuse Sco\\MessageBus\\Event\\Subscriber;\nuse Sco\\MessageBus\\Event\\MessageFailedEvent;\n\n$subscriber = new Subscriber();\n\n$subscriber-\u003eaddListener(MessageFailedEvent::class, function (MessageFailedEvent $event) {\n    $event-\u003egetName(); // Name of the Event\n    $event-\u003egetMessage(); // Command or Query being handled\n    $event-\u003egetError(); // Captured Exception\n});\n```\n\n### Transaction\n\nTransaction Middleware accepts three function arguments, each for every stage of the transaction: begin, commit, and rollback. \nGoing with this approach allows you to use any ORM you prefer or even using the native \\PDO object to interact with your persistence layer.\n```php\n$pdo = new \\PDO('{connection_dsn}')\n\n$transaction = new \\Sco\\MessageBus\\Middleware\\TransactionMiddleware(\n    fn(): bool =\u003e $pdo-\u003ebeginTransaction(),\n    fn(): bool =\u003e $pdo-\u003ecommit(),\n    fn(\\Throwable $error): bool =\u003e $pdo-\u003erollBack(),\n);\n```\n\n### Result Types\n\nLibrary wraps the Handler return values into __Result value objects__ to provide a consistent API and so that you can\ndepend on the return values always being of the same type.\n\nAll Result value objects extend the `Sco\\MessageBus\\Result` abstract class and can be divided into 3 groups:\n1. The ones which wrap primitive values:\n   * `Sco\\MessageBus\\Result\\Boolean`\n   * `Sco\\MessageBus\\Result\\Integer`\n   * `Sco\\MessageBus\\Result\\Numeric`\n   * `Sco\\MessageBus\\Result\\Text`\n   * `Sco\\MessageBus\\Result\\None` (wraps null values)\n2. `Sco\\MessageBus\\Result\\Delegated` which wraps objects and delegates calls to properties and methods to the underlying object\n3. `Sco\\MessageBus\\Result\\Collection` and `Sco\\MessageBus\\Result\\Map` which wrap number indexed arrays (lists) and string indexed arrays (maps) and implement `\\Countable`, `\\ArrayAccess` and `\\IteratorAggregate` interfaces\n\nYou can also add your own custom Result value objects by extending the abstract class `Sco\\MessageBus\\Result` and returning them in the appropriate handler.\n\n## Contribute\n\n### Style Guide\nLibrary follows the [PSR-12 standard](https://www.php-fig.org/psr/psr-12/).\n\n### TO DO:\n1. Add PSR Cache interface and implementation for caching Results\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa-b%2Fcommand-bus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsasa-b%2Fcommand-bus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa-b%2Fcommand-bus/lists"}