Skip to content

fix: state interfaces #1533

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 1 commit into from
Apr 20, 2022
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
20 changes: 7 additions & 13 deletions core/dto.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ final class UserResetPasswordDto
namespace App\Model;

use App\Dto\UserResetPasswordDto;
use App\State\UserResetPasswordProcessor;

#[ApiResource(input: UserResetPasswordDto::class)]
#[ApiResource(input: UserResetPasswordDto::class, processor: UserResetPasswordProcessor::class)]
final class User {}
```

Expand All @@ -46,19 +47,14 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class UserResetPasswordProcessor implements ProcessorInterface
{
public function process($data, array $identifiers = [], ?string $operationName = null, array $context = [])
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if ('[email protected]' === $data->email) {
return $data;
}

throw new NotFoundHttpException();
}

public function supports($data, array $identifiers = [], ?string $operationName = null, array $context = []): bool
{
return $data instanceof UserResetPasswordDto;
}
}
```

Expand All @@ -78,15 +74,13 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class BookRepresentationProvider implements ProviderInterface
{
public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = [])
/**
* {@inheritDoc}
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
return new AnotherRepresentation();
}

public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool
{
return Book::class === $resourceClass;
}
}
```

Expand Down
23 changes: 11 additions & 12 deletions core/identifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\State\PersonProvider;
use App\Uuid;

#[ApiResource]
#[ApiResource(provider: PersonProvider::class)]
final class Person
{
/**
Expand All @@ -39,12 +40,18 @@ properties:
App\Entity\Person:
code:
identifier: true
resource:
App\Entity\Person:
provider: App\State\PersonProvider
```

```xml
<properties xmlns="https://api-platform.com/schema/metadata/properties">
<property resource="App\EntityPerson" name="code" identifier="true"/>
<property resource="App\Entity\Person" name="code" identifier="true"/>
</properties>
<resources xmlns="https://api-platform.com/schema/metadata/resources">
<resource class="App\Entity\Person" provider="App\State\PersonProvider" />
</resources>
```

[/codeSelector]
Expand All @@ -65,23 +72,15 @@ use App\Uuid;
final class PersonProvider implements ProviderInterface
{
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function provide(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = [])
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
// Our identifier is:
// $uriVariables['code']
// although it's a string, it's not an instance of Uuid and we wanted to retrieve the timestamp of our time-based uuid:
// $uriVariable['code']->getTimestamp()
}

/**
* {@inheritdoc}
*/
public function supports(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = []): bool
{
return $resourceClass === Person::class;
}
}
```

Expand Down
64 changes: 35 additions & 29 deletions core/state-processors.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ However, you may want to:

* store data to other persistence layers (Elasticsearch, external web services...)
* not publicly expose the internal model mapped with the database through the API
* use a separate model for [read operations](data-providers.md) and for updates by implementing patterns such as [CQRS](https://martinfowler.com/bliki/CQRS.html)
* use a separate model for [read operations](state-providers.md) and for updates by implementing patterns such as [CQRS](https://martinfowler.com/bliki/CQRS.html)

Custom state processors can be used to do so. A project can include as many state processors as needed. The first able to
process the data for a given resource will be used.
Expand All @@ -27,10 +27,7 @@ bin/console make:state-processor
```

To create a state processor, you have to implement the [`ProcessorInterface`](https://github.com/api-platform/core/blob/main/src/State/ProcessorInterface.php).
This interface defines only two methods:

* `process`: to create, delete, update, or process the given data in any ways
* `supports`: to check whether the given data is supported by this state processor
This interface defines a method `process`: to create, delete, update, or alter the given data in any ways.

Here is an implementation example:

Expand All @@ -47,36 +44,40 @@ class BlogPostProcessor implements ProcessorInterface
/**
* {@inheritDoc}
*/
public function process($data, array $identifiers = [], ?string $operationName = null, array $context = [])
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
// call your persistence layer to save $data
return $data;
}

/**
* {@inheritDoc}
*/
public function supports($data, array $identifiers = [], ?string $operationName = null, array $context = []): bool
{
return $data instanceof BlogPost && '_api_/blog_posts_post' === $operationName;
}
}
```

You can find the operation name information either with the `debug:router` command (the route name and the operation name are
the same), or by using the `debug:api` command.
We then configure our operation to use this processor:

```php
<?php

namespace App\Entity;

use ApiPlatform\Metadata\Post;
use App\State\BlogPostProcessor;

#[Post(processor: BlogPostProcessor::class)]
class BlogPost {}
```

If service autowiring and autoconfiguration are enabled (they are by default), you are done!

Otherwise, if you use a custom dependency injection configuration, you need to register the corresponding service and add the
`api_platform.state_processor` tag. The `priority` attribute can be used to order processors.
`api_platform.state_processor` tag.

```yaml
# api/config/services.yaml
services:
# ...
App\State\BlogPostProcessor: ~
# Uncomment only if autoconfiguration is disabled
#tags: [ 'api_platform.state_processor', priority: 2 ]
#tags: [ 'api_platform.state_processor' ]
```

## Decorating the Built-In State Processors
Expand Down Expand Up @@ -107,22 +108,13 @@ final class UserProcessor implements ProcessorInterface
$this->mailer = $mailer;
}

public function process($data, array $uriVariables = [], ?string $operationName = null, array $context = [])
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
$result = $this->decorated->process($data, $uriVariables, $operationName, $context);

if ($data instanceof User && '_api_/blog_posts_post' === $operationName) {
$this->sendWelcomeEmail($data);
}

$this->sendWelcomeEmail($data);
return $result;
}

public function supports($data, array $uriVariables = [], ?string $operationName = null, array $context = []): bool
{
return $this->decorated->supports($data, $uriVariables, $operationName, $context);
}

private function sendWelcomeEmail(User $user)
{
// Your welcome email logic...
Expand All @@ -144,3 +136,17 @@ services:
#arguments: ['@App\State\UserProcessor.inner']
#tags: [ 'api_platform.state_processor' ]
```

And configure that you want to use this processor on the User resource:

```php
<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\State\UserProcessor;

#[ApiResource(processor: UserProcessor::class)]
class User {}
```
89 changes: 46 additions & 43 deletions core/state-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ you can use the following command to generate a custom state provider easily:
bin/console make:state-provider
```

Let's start with a State Provider for the URI: `/blog_posts/{id}`, which operation name is `_api_/blog_posts/{id}_get`.
You can find this information either with the `debug:router` command (the route name and the operation name are the same),
or by using the `debug:api` command.
Let's start with a State Provider for the URI: `/blog_posts/{id}`.

First, your `BlogPostProvider` has to implement the
[`StateProviderInterface`](https://github.com/api-platform/core/blob/main/src/State/StateProviderInterface.php):
Expand All @@ -46,27 +44,45 @@ final class BlogPostProvider implements ProviderInterface
/**
* {@inheritDoc}
*/
public function provide(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = [])
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
return new BlogPost($uriVariables['id']);
}

/**
* {@inheritDoc}
*/
public function supports(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = []): bool
{
return BlogPost::class === $resourceClass && '_api_/blog_posts/{id}_get' === $operationName;
}
}
```

In the `supports` method, we declare that this State Provider only works for the given operation. As this operation expects a
BlogPost we return an instance of the BlogPost in the `provide` method.
As this operation expects a BlogPost we return an instance of the BlogPost in the `provide` method.
The `uriVariables` parameter is an array with the values of the URI variables.

To use this provider we need to configure the provider on the operation:

```php
<?php

namespace App\Entity;

use ApiPlatform\Metadata\Get;
use App\State\BlogPostProvider;

#[Get(provider: BlogPostProvider::class)]
class BlogPost {}
```

If you use the default configuration, the corresponding service will be automatically registered thanks to
[autowiring](https://symfony.com/doc/current/service_container/autowiring.html).
To declare the service explicitly, you can use the following snippet:

```yaml
# api/config/services.yaml
services:
# ...
App\State\BlogPostProvider: ~
# Uncomment only if autoconfiguration is disabled
#tags: [ 'api_platform.state_provider' ]
```

Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can change the Provider into
supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `GetCollection`:
supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `CollectionOperationInterface`:

```php
<?php
Expand All @@ -75,47 +91,34 @@ namespace App\State;

use App\Entity\BlogPost;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\CollectionOperationInterface;

final class BlogPostProvider implements ProviderInterface
{
/**
* Provides data.
*
* @return object|array|null
* {@inheritDoc}
*/
public function provide(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = [])
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
if ($context['operation'] instanceof GetCollection) {
if ($operation instanceof CollectionOperationInterface) {
return [new BlogPost(), new BlogPost()];
}

return new BlogPost($uriVariables['id']);
}

/**
* Whether this state provider supports the class/identifier tuple.
*/
public function supports(string $resourceClass, array $uriVariables = [], ?string $operationName = null, array $context = []): bool
{
return $data instanceof BlogPost;
}
}
```

If you use the default configuration, the corresponding service will be automatically registered thanks to
[autowiring](https://symfony.com/doc/current/service_container/autowiring.html).
To declare the service explicitly, or to set a custom priority, you can use the following snippet:
We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operations via the `ApiResource` attribute:

```yaml
# api/config/services.yaml
services:
# ...
App\State\BlogPostProvider: ~
# Uncomment only if autoconfiguration is disabled
#tags: [ 'api_platform.state_provider', priority: 2 ]
```
```php
<?php

namespace App\Entity;

Tagging the service with the tag `api_platform.state_provider` will enable API Platform Core to automatically
register and use this state provider. The optional attribute `priority` allows you to define the order in which the
data providers are called.
use ApiPlatform\Metadata\ApiResource;
use App\State\BlogPostProvider;

#[ApiResource(provider: BlogPostProvider::class)]
class BlogPost {}
```