Skip to content

Commit

Permalink
Merge pull request #14 from camuthig/query-request-array
Browse files Browse the repository at this point in the history
Add multiple query support to QueryMiddleware
  • Loading branch information
prolic authored Apr 10, 2018
2 parents 607707b + 2af459d commit 122cf17
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 70 deletions.
84 changes: 61 additions & 23 deletions docs/book/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,79 @@ for your message data, you have to convert this data to an array before you can
> Note: The middleware uses an array for the message data
## CommandMiddleware
The `CommandMiddleware` dispatches the message data to the command bus system. This middleware needs an request attribute
(`$request->getAttribute(\Prooph\HttpMiddleware\CommandMiddleware::NAME_ATTRIBUTE)`) called `prooph_command_name`.
This name is used for the `\Prooph\Common\Messaging\MessageFactory` to create the `\Prooph\Common\Messaging\Message`
object. The data for the command is extracted from the body of the request (`$request->getParsedBody()`) and must be an
The `CommandMiddleware` dispatches the message data to the command bus system. This middleware needs an request attribute
(`$request->getAttribute(\Prooph\HttpMiddleware\CommandMiddleware::NAME_ATTRIBUTE)`) called `prooph_command_name`.
This name is used for the `\Prooph\Common\Messaging\MessageFactory` to create the `\Prooph\Common\Messaging\Message`
object. The data for the command is extracted from the body of the request (`$request->getParsedBody()`) and must be an
array.

## QueryMiddleware
The `QueryMiddleware` dispatches the message data to the query bus system. This middleware needs an request attribute
(`$request->getAttribute(\Prooph\HttpMiddleware\QueryMiddleware::NAME_ATTRIBUTE)`) called `prooph_query_name`.
This name is used for the `\Prooph\Common\Messaging\MessageFactory` to create the `\Prooph\Common\Messaging\Message`
object. The data for the query is extracted from query params of the request (`$request->getQueryParams()`) and must be an
array.
The `QueryMiddleware` is used to dispatch messages to the query bus system. Unlike the other middleware, the `QueryMiddleware`
supports sending multiple messages. Each query
is an object inside of the root `queries` object on the request payload and is indexed using a unique key. The response will include the query results
for each query, indexed using the same key as the request. An example request/response would look like:

**Request**

`POST /query`

```json
{
"queries": {
"getUsers": {
"prooph_query_name": "query:get-users",
"filter": [
"12"
]
},
"getTodos": {
"prooph_query_name": "query:get-todos",
"status": [
"OPEN"
]
}
}
}
```

**Response**

There is a special behaviour implemented. If you send a *POST* HTTP request, then the parsed body data (`$request->getParsedBody()`)
will be added to the payload under the key `data`. `data` is a reserved key if you use a *POST* HTTP request. However, it's
not recommended to use a *POST* HTTP request here. Use it only if you know what you do.
```json
{
"getUsers": [
{
"username": "John"
}
],
"getTodos": [
{
"task": "Write some docs"
},
{
"task": "Build cool things"
}
]
}
```

## EventMiddleware
The `EventMiddleware` dispatches the message data to the event bus system. This middleware needs an request attribute
(`$request->getAttribute(\Prooph\HttpMiddleware\EventMiddleware::NAME_ATTRIBUTE)`) called `prooph_event_name`.
This name is used for the `\Prooph\Common\Messaging\MessageFactory` to create the `\Prooph\Common\Messaging\Message`
object. The data for the event is extracted from the body of the request (`$request->getParsedBody()`) and must be an
The `EventMiddleware` dispatches the message data to the event bus system. This middleware needs an request attribute
(`$request->getAttribute(\Prooph\HttpMiddleware\EventMiddleware::NAME_ATTRIBUTE)`) called `prooph_event_name`.
This name is used for the `\Prooph\Common\Messaging\MessageFactory` to create the `\Prooph\Common\Messaging\Message`
object. The data for the event is extracted from the body of the request (`$request->getParsedBody()`) and must be an
array.

*Note:*

The `EventMiddleware` is commonly used for external event messages. An event comes from your domain, which was caused
by a command. It makes no sense to use this middleware in your project, if you only use a command bus with event sourcing.
by a command. It makes no sense to use this middleware in your project, if you only use a command bus with event sourcing.
In this case you will use the [event store bus bridge)](https://github.com/prooph/event-store-bus-bridge "Marry CQRS with Event Sourcing").

## MessageMiddleware
The `MessageMiddleware` dispatches the message data to the suitable bus system depending on message type. The data
for the message is extracted from the body of the request (`$request->getParsedBody()`) and must be an array. The
`message_name` is extracted from the parsed body data. This name is used for the `\Prooph\Common\Messaging\MessageFactory`
to create the `\Prooph\Common\Messaging\Message` object. Your specific message data must be located under the `payload`
The `MessageMiddleware` dispatches the message data to the suitable bus system depending on message type. The data
for the message is extracted from the body of the request (`$request->getParsedBody()`) and must be an array. The
`message_name` is extracted from the parsed body data. This name is used for the `\Prooph\Common\Messaging\MessageFactory`
to create the `\Prooph\Common\Messaging\Message` object. Your specific message data must be located under the `payload`
key. The value of `$request->getParsedBody()` is an array like this:

```
Expand All @@ -53,6 +91,6 @@ key. The value of `$request->getParsedBody()` is an array like this:
]
```

**Important:** The provided message factory must handle all 3 types (command, query, event) of messages depending on
**Important:** The provided message factory must handle all 3 types (command, query, event) of messages depending on
provided message name. It's recommended to use an prefix or something else in the message name to determine the correct
message type.
message type.
68 changes: 49 additions & 19 deletions src/QueryMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

namespace Prooph\HttpMiddleware;

use Fig\Http\Message\RequestMethodInterface;
use Fig\Http\Message\StatusCodeInterface;
use Prooph\Common\Messaging\MessageFactory;
use Prooph\HttpMiddleware\Exception\RuntimeException;
Expand All @@ -22,6 +21,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function React\Promise\all;

/**
* Query messages describe available information that can be fetched from your (read) model.
Expand All @@ -34,12 +34,19 @@
final class QueryMiddleware implements MiddlewareInterface
{
/**
* Identifier to execute specific query
* The query message identifier.
*
* @var string
*/
public const NAME_ATTRIBUTE = 'prooph_query_name';

/**
* The property identifier for the collection of queries.
*
* @var string
*/
public const QUERIES_ATTRIBUTE = 'queries';

/**
* Dispatches query
*
Expand Down Expand Up @@ -82,35 +89,58 @@ public function __construct(

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$queryName = $request->getAttribute(self::NAME_ATTRIBUTE);
$body = $request->getParsedBody();

if (null === $queryName) {
throw new RuntimeException(
sprintf('Query name attribute ("%s") was not found in request.', self::NAME_ATTRIBUTE),
StatusCodeInterface::STATUS_BAD_REQUEST
$this->validateRequestBody($body);

$promises = [];

foreach ($body[self::QUERIES_ATTRIBUTE] as $id => $message) {
$message['metadata'] = $this->metadataGatherer->getFromRequest($request);

$query = $this->queryFactory->createMessageFromArray(
$message[self::NAME_ATTRIBUTE],
$message
);
}
$payload = $request->getQueryParams();

if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
$payload['data'] = $request->getParsedBody();
try {
$promises[$id] = $this->queryBus->dispatch($query);
} catch (\Throwable $e) {
throw new RuntimeException(
sprintf('An error occurred during dispatching of query "%s"', $message[self::NAME_ATTRIBUTE]),
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
$e
);
}
}

try {
$query = $this->queryFactory->createMessageFromArray($queryName, [
'payload' => $payload,
'metadata' => $this->metadataGatherer->getFromRequest($request),
]);
$all = all($promises);

return $this->responseStrategy->fromPromise(
$this->queryBus->dispatch($query)
);
return $this->responseStrategy->fromPromise($all);
} catch (\Throwable $e) {
throw new RuntimeException(
sprintf('An error occurred during dispatching of query "%s"', $queryName),
'An error occurred dispatching queries',
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
$e
);
}
}

private function validateRequestBody(array $body): void
{
if (! isset($body[self::QUERIES_ATTRIBUTE])) {
throw new RuntimeException(
sprintf('The root query value ("%s") must be provided.', QueryMiddleware::QUERIES_ATTRIBUTE)
);
}

foreach ($body[self::QUERIES_ATTRIBUTE] as $message) {
if (! is_array($message) || ! array_key_exists(self::NAME_ATTRIBUTE, $message)) {
throw new RuntimeException(
sprintf('Each query must contain the query name attribute (%s).', self::NAME_ATTRIBUTE)
);
}
}
}
}
Loading

0 comments on commit 122cf17

Please sign in to comment.