Skip to content

Commit 9166c23

Browse files
committed
Merge branch '2.2'
2 parents 984ed7f + 9900196 commit 9166c23

File tree

9 files changed

+228
-35
lines changed

9 files changed

+228
-35
lines changed

core/data-persisters.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Data Persisters
2+
3+
To mutate the application states during `POST`, `PUT`, `PATCH` or `DELETE` [operations](operations.md), API Platform uses
4+
classes called **data persisters**. Data persisters receive an instance of the class marked as an API resource (usually using
5+
the `@ApiResource` annotation). This instance contains data submitted by the client during [the deserialization
6+
process](serialization.md).
7+
8+
A data persister using [Doctrine ORM](http://www.doctrine-project.org/projects/orm.html) is included with the library and
9+
is enabled by default. It is able to persist and delete objects that are also mapped as [Doctrine entities](https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/basic-mapping.html).
10+
11+
However, you may want to:
12+
13+
* store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
14+
* not publicly expose the internal model mapped with the database through the API
15+
* 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)
16+
17+
Custom data persisters can be used to do so. A project can include as many data persisters as it needs. The first able to
18+
persist data for a given resource will be used.
19+
20+
## Creating a Custom Data Persister
21+
22+
To create a data persister, you have to implement the [`DataPersisterInterface`](https://github.com/api-platform/core/blob/master/src/DataPersister/DataPersisterInterface.php).
23+
This interface defines only 3 methods:
24+
25+
* `persist`: to create or update the given data
26+
* `delete`: to delete the given data
27+
* `support`: checks whether the given data is supported by this data persister
28+
29+
Here is an implementation example:
30+
31+
```php
32+
namespace App\DataPersister;
33+
34+
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
35+
use App\Entity\BlogPost;
36+
37+
final class BlogPostDataPersister implements DataPersisterInterface
38+
{
39+
public function supports($data): bool
40+
{
41+
return $data instanceof BlogPost;
42+
}
43+
44+
public function persist($data)
45+
{
46+
// call your persistence layer to save $data
47+
return $data;
48+
}
49+
50+
public function delete($data): void
51+
{
52+
// call your persistence layer to delete $data
53+
}
54+
}
55+
```
56+
57+
If service autowiring and autoconfiguration are enabled (they are by default), you are done!
58+
59+
Otherwise, if you use a custom dependency injection configuration, you need to register the corresponding service and add the
60+
`api_platform.data_persister` tag. The `priority` attribute can be used to order persisters.
61+
62+
```yaml
63+
# api/config/services.yaml
64+
services:
65+
# ...
66+
'App\DataPersister\BlogPostDataPersister': ~
67+
# Uncomment only if autoconfiguration is disabled
68+
#tags: [ 'api_platform.data_persister' ]
69+
```

core/data-providers.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ final class BlogPostCollectionDataProvider implements CollectionDataProviderInte
5858
}
5959
```
6060

61-
Then declare a Symfony service, for example:
61+
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).
62+
To declare the service explicitly, or to set a custom priority, you can use the following snippet:
6263

6364
```yaml
6465
# api/config/services.yaml

core/design.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# General Design Considerations
2+
3+
Since you only need to describe the structure of the data to expose, API Platform is both [a "design-first" and "code-first"](https://swagger.io/blog/api-design/design-first-or-code-first-api-development/)
4+
API framework. However, the "design-first" methodology is strongly recommended: first you design the **public shape** of
5+
API endpoints.
6+
7+
To do so, you have to write a plain old PHP object representing the input and output of your endpoint. This is the class
8+
that is [marked with the `@ApiResource` annotation](../distribution/index.md).
9+
This class **doesn't have** to be mapped with Doctrine ORM, or any other persistence system. It must be simple (it's usually
10+
just a data structure with no or minimal behaviors) and will be automatically converted to [Hydra](extending-jsonld-context.md),
11+
[OpenAPI / Swagger](swagger.md) and [GraphQL](graphql.md) documentations or schemas by API Platform (there is a 1-1 mapping
12+
between this class and those docs).
13+
14+
Then, it's up to the developer to feed API Platform with an hydrated instance of this API resource object by implementing
15+
the [`DataProviderInterface`](data-providers.md). Basically, the data provider will query the persistence system (RDBMS,
16+
document or graph DB, external API...), and must hydrate and return the POPO that has been designed as mentioned above.
17+
18+
When updating a state (`POST`, `PUT`, `PATCH`, `DELETE` HTTP methods), it's up to the developer to properly persist the
19+
data provided by API Platform's resource object [hydrated by the serializer](serialization.md).
20+
To do so, there is another interface to implement: [`DataPersisterInterface`](data-persisters.md).
21+
22+
This class will read the API resource object (the one marked with `@ApiResource`) and:
23+
24+
* persist it directly in the database;
25+
* or hydrate a DTO then trigger a command;
26+
* or populate an event store;
27+
* or persist the data in any other useful way.
28+
29+
The logic of data persisters is the responsibility of application developers, and is **out of the API Platform's scope**.
30+
31+
For [Rapid Application Development](https://en.wikipedia.org/wiki/Rapid_application_development), convenience and prototyping,
32+
**if and only if the class marked with `@ApiResource` is also a Doctrine entity**, the developer can use the Doctrine
33+
ORM's data provider and persister implementations shipped with API Platform.
34+
35+
In this case, the public (`@ApiResource`) and internal (Doctrine entity) data model are shared. Then, API Platform will
36+
be able to query, filter, paginate and persist automatically data.
37+
This is 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)
38+
and/or large systems.
39+
Again, it's up to the developers to use, or to not use those built-in data providers/persisters depending of the business
40+
they are dealing with. API Platform makes it easy to create custom data providers and persisters, and to implement appropriate
41+
patterns such as [CQS](https://www.martinfowler.com/bliki/CommandQuerySeparation.html) or [CQRS](https://martinfowler.com/bliki/CQRS.html).
42+
43+
Last but not least, to create [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)-based systems, a convenient
44+
approach is:
45+
46+
* to persist data in an event store using a custom [data persister](data-persisters.md)
47+
* to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
48+
* to map those projections with read-only Doctrine entity classes **and** to mark those classes with `@ApiResource`
49+
50+
You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.

core/events.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ Built-in event listeners are:
7676
Name | Event | Pre & Post hooks | Priority | Description
7777
------------------------------|--------------------|--------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------
7878
`AddFormatListener` | `kernel.request` | None | 7 | guess the best response format ([content negotiation](content-negotiation.md))
79-
`ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | retrieve data from the persistence system using the [data providers](data-providers.md)
79+
`ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | retrieve data from the persistence system using the [data providers](data-providers.md) (`GET`, `PUT`, `DELETE`)
8080
`DeserializeListener` | `kernel.request` | `PRE_DESERIALIZE`, `POST_DESERIALIZE`| 2 | deserialize data into a PHP entity (`GET`, `POST`, `DELETE`); update the entity retrieved using the data provider (`PUT`)
8181
`ValidateListener` | `kernel.view` | `PRE_VALIDATE`, `POST_VALIDATE` | 64 | [validate data](validation.md) (`POST`, `PUT`)
82-
`WriteListener` | `kernel.view` | `PRE_WRITE`, `POST_WRITE` | 32 | if using the Doctrine ORM, persist data (`POST`, `PUT`, `DELETE`)
82+
`WriteListener` | `kernel.view` | `PRE_WRITE`, `POST_WRITE` | 32 | persist changes in the persistence system using the [data persisters](data-persisters.md) (`POST`, `PUT`, `DELETE`)
8383
`SerializeListener` | `kernel.view` | `PRE_SERIALIZE`, `POST_SERIALIZE` | 16 | serialize the PHP entity in string [according to the request format](content-negotiation.md)
8484
`RespondListener` | `kernel.view` | `PRE_RESPOND`, `POST_RESPOND` | 8 | transform serialized to a `Symfony\Component\HttpFoundation\Response` instance
8585
`AddLinkHeaderListener` | `kernel.response` | None | 0 | add a `Link` HTTP header pointing to the Hydra documentation

core/operations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ use ApiPlatform\Core\Annotation\ApiResource;
6767
* @ApiResource(
6868
* collectionOperations={"get"},
6969
* itemOperations={"get"}
70-
* )
70+
* )
7171
*/
7272
class Book
7373
{
@@ -88,7 +88,7 @@ use ApiPlatform\Core\Annotation\ApiResource;
8888
* @ApiResource(
8989
* collectionOperations={"get"={"method"="GET"}},
9090
* itemOperations={"get"={"method"="GET"}}
91-
* )
91+
* )
9292
*/
9393
class Book
9494
{

core/serialization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ The Normalizer class is a bit harder to understand because it has to make sure t
405405
406406
namespace App\Serializer;
407407
408-
class BookAttributeNormalizer implements NormalizerInterface, SerializerAwareInterface
408+
class BookAttributeNormalizer implements ContextAwareNormalizerInterface, SerializerAwareInterface
409409
{
410410
use SerializerAwareTrait;
411411

core/validation.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,70 @@ Of course, you can use XML or YAML configuration format instead of annotations i
167167
You may also pass in a [group sequence](http://symfony.com/doc/current/validation/sequence_provider.html) in place of
168168
the array of group names.
169169

170+
## Using Validation Groups on Operations
171+
172+
You can have different validation for each [operation](operations.md) related to your resource.
173+
174+
```php
175+
<?php
176+
// api/src/Entity/Book.php
177+
178+
use ApiPlatform\Core\Annotation\ApiResource;
179+
use Symfony\Component\Validator\Constraints as Assert;
180+
181+
/**
182+
* @ApiResource(
183+
* collectionOperations={
184+
* "get",
185+
* "post"={"validation_groups"={"Default", "postValidation"}}
186+
* },
187+
* itemOperations={
188+
* "delete",
189+
* "get",
190+
* "put"={"validation_groups"={"Default", "putValidation"}}
191+
* }
192+
* )
193+
* ...
194+
*/
195+
class Book
196+
{
197+
/**
198+
* @Assert\Uuid
199+
*/
200+
private $id;
201+
202+
/**
203+
* @Assert\NotBlank(groups={"postValidation"})
204+
*/
205+
private $name;
206+
207+
/**
208+
* @Assert\NotNull
209+
* @Assert\Length(
210+
* min = 2,
211+
* max = 50,
212+
* groups={"postValidation"}
213+
* )
214+
* @Assert\Length(
215+
* min = 2,
216+
* max = 70,
217+
* groups={"putValidation"}
218+
* )
219+
*/
220+
private $author;
221+
222+
// ...
223+
}
224+
```
225+
226+
With this configuration, there are three validation groups:
227+
228+
`Default` contains the constraints that belong to no other group.
229+
230+
`postValidation` contains the constraints on the name and author (length from 2 to 50) fields only.
231+
232+
`putValidation` contains the constraints on the author (length from 2 to 70) field only.
233+
170234
## Dynamic Validation Groups
171235

172236
If you need to dynamically determine which validation groups to use for an entity in different scenarios, just pass in a

deployment/kubernetes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ package manager) chart to deploy in a wink on any of these platforms.
3030

3131
## Deploying
3232

33+
Firstly you need to update helm dependencies by running:
34+
35+
helm dependency update ./api/helm/api
36+
37+
You are now ready to deploy the API!
38+
3339
Deploy your API to the container:
3440

3541
helm install ./api/helm/api --namespace=baz --name baz \

0 commit comments

Comments
 (0)