Skip to content

Docs for the Messenger integration #708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Thanks to the `sunset` attribute, API Platform makes it easy to set this header
<?php
// api/src/Entity/Parchment.php

namespace App\Entity;

/**
* @ApiResource(
* deprecationReason="Create a Book instead",
Expand All @@ -128,6 +130,8 @@ It's also possible to set the `Sunset` header only for a specific [operation](op
<?php
// api/src/Entity/Parchment.php

namespace App\Entity;

/**
* @ApiResource(itemOperations={
* "get"={
Expand Down
10 changes: 6 additions & 4 deletions core/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ To do so, you have to write a plain old PHP object representing the input and ou
that is [marked with the `@ApiResource` annotation](../distribution/index.md).
This class **doesn't have** to be mapped with Doctrine ORM, or any other persistence system. It must be simple (it's usually
just a data structure with no or minimal behaviors) and will be automatically converted to [Hydra](extending-jsonld-context.md),
[OpenAPI / Swagger](swagger.md) and [GraphQL](graphql.md) documentations or schemas by API Platform (there is a 1-1 mapping
[OpenAPI](swagger.md) and [GraphQL](graphql.md) documentations or schemas by API Platform (there is a 1-1 mapping
between this class and those docs).

Then, it's up to the developer to feed API Platform with an hydrated instance of this API resource object by implementing
Expand Down Expand Up @@ -37,13 +37,15 @@ be able to query, filter, paginate and persist data automatically.
This approach is super-convenient and efficient, but is probably **not a good idea** for non-[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)
and/or large systems.
Again, it's up to the developers to use, or to not use these built-in data providers/persisters depending on the business logic
they are dealing with. API Platform makes it easy to create custom data providers and persisters, and to implement appropriate
patterns such as [CQS](https://www.martinfowler.com/bliki/CommandQuerySeparation.html) or [CQRS](https://martinfowler.com/bliki/CQRS.html).
they are dealing with.
API Platform makes it easy to create custom data providers and persisters.
It also makes it easy to implement patterns such as [CQS](https://www.martinfowler.com/bliki/CommandQuerySeparation.html)
or [CQRS](https://martinfowler.com/bliki/CQRS.html) thanks to [the Messenger Component integration](messenger.md) and the [DTO support](dto.md).

Last but not least, to create [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)-based systems, a convenient
approach is:

* to persist data in an event store using a custom [data persister](data-persisters.md)
* to persist data in an event store using a Messenger handler or a custom [data persister](data-persisters.md)
* to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
* to map those projections with read-only Doctrine entity classes **and** to mark those classes with `@ApiResource`

Expand Down
30 changes: 15 additions & 15 deletions core/dto.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ So let's create a basic DTO for this request:

```php
<?php
// api/src/Api/Dto/ForgotPasswordRequest.php
// api/src/Entity/ResetPasswordRequest.php

namespace App\Entity\Dto;
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;
Expand All @@ -62,44 +62,44 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ApiResource(
* collectionOperations={
* "post"={
* "path"="/users/forgot-password-request",
* "status"=202
* "path"="/users/forgot-password-request"
* },
* },
* itemOperations={},
* outputClass=false
* )
*/
final class ForgotPasswordRequest
final class ResetPasswordRequest
{
/**
* @var string
*
* @Assert\NotBlank
* @Assert\Email
*/
public $email;
public $username;
}
```

In this case, we disable all operations except `POST`. We also set the `output_class` attribute to `false` to hint API Platform that no data will be returned by this endpoint.
Finally, we use the `status` attribute to configure API Platform to return a [202 Accepted HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202).
It indicates that the request has been received and will be treated without giving an immediate return to the client.

Then, thanks to [a custom data persister](data-persisters.md), it's possible to trigger some custom logic when the request is received.

Create the data persister:

```php
// api/src/DataPersister/ForgotPasswordRequestDataPersister.php
<?php
// api/src/DataPersister/ResetPasswordRequestDataPersister.php
****
namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Entity\ForgotPasswordRequest;
use App\Entity\ResetPasswordRequest;

final class ForgotPasswordRequestDataPersister implements DataPersisterInterface
final class ResetPasswordRequestDataPersister implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof ForgotPasswordRequest;
return $data instanceof ResetPasswordRequest;
}

public function persist($data)
Expand All @@ -121,9 +121,9 @@ And register it:
# api/config/services.yaml
services:
# ...
'App\DataPersister\ForgotPasswordRequestDataPersister': ~
'App\DataPersister\ResetPasswordRequestDataPersister': ~
# Uncomment only if autoconfiguration is disabled
#tags: [ 'api_platform.data_persister' ]
```

Instead of a custom data persister, you'll probably want to leverage the Symfony Messenger Component integration.
Instead of a custom data persister, you'll probably want to leverage [the Symfony Messenger Component integration](messenger.md).
94 changes: 94 additions & 0 deletions core/messenger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Symfony Messenger Integration: CQRS and Async Message Processing

API Platform provides an integration with the [Symfony Messenger Component](https://symfony.com/doc/current/messenger.html).

This feature allows to implement the [Command Query Responsibility Segregation (CQRS)](https://martinfowler.com/bliki/CQRS.html) pattern in a convenient way.
It also makes it easy to send messages through the web API that will be consumed asynchronously.

Many transports are supported to dispatch messages to async consumers, including RabbitMQ, Apache Kafka, Amazon SQS and Google Pub/Sub.

## Installing Symfony Messenger

To enable the support of Messenger, install the library:

$ docker-compose exec php composer require messenger

## Dispatching a Resource through the Message Bus

Set the `messenger` attribute to `true`, and API Platform will automatically dispatch the API Resource instance as a message using the message bus provided by the Messenger Component:

```php
<?php

// api/src/Entity/ResetPasswordRequest.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ApiResource(
* messenger=true,
* collectionOperations={
* "post"={"status"=202}
* },
* itemOperations={},
* outputClass=false
* )
*/
final class ResetPasswordRequest
{
/**
* @var string
*
* @Assert\NotBlank
*/
public $username;
}
```

Because the `messenger` attribute is `true`, when a `POST` will be handled by API Platform, the corresponding instance of the `ResetPasswordRequest` will be dispatched.

For this example, only the `POST` operation is enabled.
We use the `status` attribute to configure API Platform to return a [202 Accepted HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202).
It indicates that the request has been received and will be treated later, without giving an immediate return to the client.
Finally, the `output_class` attribute is set to `false`, so the HTTP response that will be generated by API Platform will be empty, and the [serialization process](serialization.md) will be skipped.

## Registering a Message Handler

To process the message that will be dispatched, [a handler](https://symfony.com/doc/current/messenger.html#registering-handlers) must be created:

```php
<?php

// api/src/Handler/ResetPasswordRequestHandler.php

namespace App\Handler;

use App\Entity\ResetPasswordRequest;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

final class ResetPasswordRequestHandler implements MessageHandlerInterface
{
public function __invoke(ResetPasswordRequest $forgotPassword)
{
// do something with the resource
}
}
```

That's all!

By default, the handler will process your message synchronously.
If you want it to be consumed asynchronously (e.g. by a worker machine), [configure a transport and the consumer](https://symfony.com/doc/current/messenger.html#transports).

## Accessing to the Data Returned by the Handler

API Platform automatically uses the `Symfony\Component\Messenger\Stamp\HandledStamp` when set.
It means that if you use a synchronous handler, the data returned by the `__invoke` method replaces the original data.

## Detecting Removals

When a `DELETE` operation occurs, API Platform automatically adds a `ApiPlatform\Core\Bridge\Symfony\Messenger\RemoveStamp` ["stamp"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes) instance to the "envelope".
To differentiate typical persists calls (create and update) and removal calls, check for the presence of this stamp using [a custom "middleware"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes).