Skip to content

Commit

Permalink
Finished initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
mnavarrocarter committed Jul 13, 2021
1 parent c2630f4 commit 331d32c
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 38 deletions.
29 changes: 6 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,14 @@ Castor Message Bus

A well-designed and flexible message bus.

```
composer require castor/message-bus
```

## Basic Usage
## Installation

You must create a message bus and pass some middleware to it. By default, we
provide middleware that finds handlers from a service container using a naming
convention.
You can install the latest stable version with:

```php
<?php

use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(new SomeMiddleware());
$bus->add(new MessageBus\HandleMessage(
new MessageBus\Handler\ClassSuffixInflector(),
new MessageBus\Handler\ContainerLocator($aContainer)
));

$bus->handle(new SomeCommand());
```bash
composer require castor/message-bus
```

By default, the middleware is run in the order of addition.
## Documentation

The booting defaults don't even need some registrations.
Documentation is available in Markdown format [here](docs/README.md).
165 changes: 165 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Introduction

`castor/message-bus` provides a command bus object capable of processing and
handle messages. Messages are simply data classes (DTO).

This message bus is designed to be extensible by leveraging the middleware pattern.

# Basic Usage

Simply, instantiate the bus and provide some middleware.

You must create a message bus and pass some middleware to it. By default, we
provide middleware that finds handlers from a service container using a naming
convention.

```php
<?php

use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(new SomeMiddleware());
$bus->add(new SomeOtherMiddleware);

$bus->add(new MessageBus\HandleMessage(
new MessageBus\Handler\ClassSuffixInflector(),
new MessageBus\Handler\ContainerLocator($aContainer)
));

$bus->handle(new SomeCommand());
```

By default, the middleware is run in the order of addition.

# Included Middleware

## `Castor\MessageBus\HandleMessage` middleware.

This middleware takes a command, finds a handler for it and executes it. In order
to do this it depends on two abstractions: `Castor\MessageBus\Handler\Inflector` and
`Castor\MessageBus\Handler\Locator`. The first returns the handler name for the
given message object. The second returns the actual handler instance using the handler
name.

There is only one implementation provided for each abstraction.

The `Castor\MessageBus\Handler\ClassSuffixInflector` derives the handler name from
the message FQNC by adding a suffix to it. By default, the suffix is `Handler`. So,
if you pass a message named `Foo` then the derived handler name for that message
will be `FooHandler`. This is a convention you can easily bypass by implementing
a custom `Castor\MessageBus\Handler\Inflector` that resolves from, for example, an
associative PHP array.

The `Castor\MessageBus\Handler\ContainerLocator` obtains the handler from a
`Psr\Container\ContainerInterface` instance. The objects returned from the container
must be either an invokable class that takes the raw message as the first argument,
or an instance of `Castor\MessageBus\Handler`.

Instantiating this middleware with the defaults its very easy:

```php
use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(MessageBus\HandleMessage::usingContainer($container));
```

If you pass custom implementations, you can do it using the constructor:

```php
<?php

use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(new MessageBus\HandleMessage(
new MyCustomArrayInflector(),
new MyCustomLocator()
));
```

> NOTE: We highly recommend the use of the Container Locator as it makes fetching
> handlers very easily and efficient, specially if your container makes use of
> reflection. Also, some sort of convention between a message and its handler
> would be beneficial to avoid maintaining maps of messages to handlers, reducing
> application maintenance costs.
## `Castor\MessageBus\HandleMultiple` middleware.

It's common practice that message buses execute messages inside a database transaction, which
is usually handled in a transactional middleware. In some cases though, you might
want to execute multiple messages in a single transaction.

This message bus provides this middleware for that reason. You can wrap your messages
in the `Castor\MessgeBus\Multi` class and pass that class as the message. Then,
inject the `Castor\MessageBus\ExecuteMultiple` middleware whenever you want the
commands to be processed individually. They will be passed one by one to the same
stack point.

So, in this code example:

```php
<?php

use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(new TransactionalMiddleware());
$bus->add(new Castor\MessageBus\HandleMultiple());
$bus->add(new HandleMessage());

// All these three messages will be executed in the same database transaction,
// since they are past the TransactionalMiddleware.
$bus->handle(MessageBus\Multi::wrap([new MessageOne(), new MessageTwo(), new MessageThree()]));

// HandleMessage will never receive the Multi class, but each of the wrapped messages
// in order of insertion.
```

Keep in mind that any middleware you have in between the handling of a message and
the `HandleMultiple` middleware will be executed one time for every wrapped message
in the `Castor\MessgeBus\Multi` class.

## `Castor\MessageBus\ClosureMiddleware` middleware.

This middleware is designed to avoid instantiating a full object graph in a
middleware chain by deferring the instantiation of it until it has been reached
in the execution stack. This is what is known as lazy-loading.

It is best used with a `Psr\Container\ContainerInterface`, but you can also pass
a custom factory as a `Closure` in the constructor.

```php
<?php

use Castor\MessageBus;

$bus = new MessageBus();
$bus->add(MessageBus\ClosureMiddleware::lazy('service-name', $container));
$bus->add(new MessageBus\ClosureMiddleware(function () {
return new CustomMiddleware();
}));
```

> NOTE: Bear in mind that the closure is executed every time the middleware is
> used, potentially returning a new instance every time. It does not save any
> kind of internal state or performs any memoization for you; you must do that
> in userland.
>
> If you use a container, make sure you cache resolutions if you want to get the
> same instance every time.
# On Middleware and Envelopes

Envelopes are designed to wrap original messages into other classes that can be
used by third party middleware to handle that message in a particular way.
For instance, the `castor/async-message-bus` package implements the
`Castor\MessageBus\Async` envelope. This envelope wraps a message and keeps some
state in it, like the queue where is going to be executed and the number of times it
has been put on the queue. The middleware uses the `Castor\MessageBus\Envelope` public
api to find the envelope and obtain information about it.

Every envelope must extend the base `Castor\MessageBus\Envelope` class because it
contains the base methods other middleware can rely upon to extract the original
message or find the envelope class corresponding to their middleware.
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<psalm
errorLevel="5"
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
Expand Down
20 changes: 16 additions & 4 deletions src/MessageBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,41 @@

use Castor\MessageBus as Bus;
use InvalidArgumentException;
use Traversable;

/**
* Class MessageBus.
*/
final class MessageBus implements Bus\Handler
{
/**
* @var array<int,Bus\Middleware>
* @var Bus\Middleware[]
*/
private array $middleware;

/**
* Bus constructor.
*
* @psalm-param array<int,Bus\Middleware> $messages
*/
public function __construct(Bus\Middleware ...$middleware)
{
$this->middleware = $middleware;
}

public static function fromIterator(Traversable $traversable): MessageBus
/**
* @psalm-param iterable<int,Bus\Middleware> $iterable
*/
public static function fromIterable(iterable $iterable): MessageBus
{
return new self(...iterator_to_array($traversable));
if (is_array($iterable)) {
return new self(...$iterable);
}
$bus = new self();
foreach ($iterable as $middleware) {
$bus->add($middleware);
}

return $bus;
}

public function add(Bus\Middleware $middleware): void
Expand Down
1 change: 1 addition & 0 deletions src/MessageBus/ClosureMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function __construct(Closure $closure)
public static function lazy(string $service, ContainerInterface $container): ClosureMiddleware
{
return new self(static function (object $command, Stack $stack) use ($service, $container) {
/** @psalm-var object $middleware */
$middleware = $container->get($service);
if (is_callable($middleware)) {
$middleware = self::make($middleware);
Expand Down
12 changes: 12 additions & 0 deletions src/MessageBus/HandleMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

namespace Castor\MessageBus;

use Castor\MessageBus\Handler\ClassSuffixInflector;
use Castor\MessageBus\Handler\ContainerLocator;
use Psr\Container\ContainerInterface;

/**
* Class HandleMessage.
*/
Expand All @@ -33,6 +37,14 @@ public function __construct(Handler\Inflector $inflector, Handler\Locator $locat
$this->locator = $locator;
}

public static function usingContainer(ContainerInterface $container, string $suffix = 'Handler'): HandleMessage
{
return new self(
new ClassSuffixInflector($suffix),
new ContainerLocator($container)
);
}

/**
* @throws Handler\HandlerNotFound
* @throws Handler\InflectionError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
namespace Castor\MessageBus;

/**
* Class ExecuteMultiple allows you to execute multiple commands at the same
* Class HandleMultiple allows you to execute multiple commands at the same
* stack frame.
*
* This is useful, for instance, if you want to execute multiple commands in
* the same database transaction. You just need to place this middleware after
* the transactional one.
*/
final class ExecuteMultiple implements Middleware
final class HandleMultiple implements Middleware
{
public function process(object $message, Stack $stack): void
{
Expand Down
1 change: 1 addition & 0 deletions src/MessageBus/Handler/ContainerLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function __construct(ContainerInterface $container)
public function locate(string $handlerName): Handler
{
try {
/** @psalm-var object $handler */
$handler = $this->container->get($handlerName);
} catch (NotFoundExceptionInterface $e) {
throw new HandlerNotFound(sprintf(
Expand Down
8 changes: 8 additions & 0 deletions src/MessageBus/Multi.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class Multi implements Countable, IteratorAggregate

/**
* Multi constructor.
*
* @psalm-param object[] $messages
*/
public function __construct(array $messages = [])
{
Expand All @@ -43,6 +45,9 @@ public function push(object $message): void
$this->messages[] = $message;
}

/**
* @psalm-param iterable<object> $commands
*/
public static function wrap(iterable $commands): Multi
{
if (is_array($commands)) {
Expand All @@ -66,6 +71,9 @@ public function getMessages(): array
return $this->messages;
}

/**
* @return Generator<array-key,object>
*/
public function getIterator(): Generator
{
yield from $this->messages;
Expand Down
2 changes: 1 addition & 1 deletion tests/MessageBus/EnvelopeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* Class EnvelopeTest.
*
* @internal
* @coversNothing
* @covers \Castor\MessageBus\Envelope
*/
class EnvelopeTest extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@

/**
* @internal
* @coversNothing
* @covers \Castor\MessageBus\HandleMultiple
*/
class ExecuteMultipleTest extends TestCase
class HandleMultipleTest extends TestCase
{
public function testItDoesNotExecuteMultiple(): void
{
$message = new \stdClass();
$stack = $this->createMock(Stack::class);
$handler = $this->createMock(Handler::class);
$middleware = new ExecuteMultiple();
$middleware = new HandleMultiple();

$stack->expects($this->once())
->method('next')
Expand All @@ -49,7 +49,7 @@ public function testItExecutesMultiple(): void
$messageTwo = new \stdClass();
$stack = $this->createMock(Stack::class);
$handler = $this->createMock(Handler::class);
$middleware = new ExecuteMultiple();
$middleware = new HandleMultiple();

$stack->expects($this->exactly(2))
->method('next')
Expand All @@ -73,7 +73,7 @@ public function testItExecutesMultipleFromEnvelope(): void
$messageTwo = new \stdClass();
$stack = $this->createMock(Stack::class);
$handler = $this->createMock(Handler::class);
$middleware = new ExecuteMultiple();
$middleware = new HandleMultiple();

$stack->expects($this->exactly(2))
->method('next')
Expand Down
Loading

0 comments on commit 331d32c

Please sign in to comment.