Skip to content

Commit 960e2d5

Browse files
committed
fix #504
1 parent 63cef67 commit 960e2d5

File tree

2 files changed

+118
-19
lines changed

2 files changed

+118
-19
lines changed

core/serialization.md

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ App\Entity\Book:
139139
```
140140

141141
In the previous example, the `name` property will be visible when reading (`GET`) the object, and it will also be available
142-
to write (`PUT/POST`). The `author` property will be write-only; it will not be visible when serialized responses are
142+
to write (`PUT/POST`). The `author` property will be write-only; it will not be visible when serialized responses are
143143
returned by the API.
144144

145145
Internally, API Platform passes the value of the `normalization_context` as the 3rd argument of [the `Serializer::serialize()` method](https://api.symfony.com/master/Symfony/Component/Serializer/SerializerInterface.html#method_serialize) during the normalization
@@ -222,9 +222,9 @@ In the following JSON document, the relation from a book to an author is represe
222222
}
223223
```
224224

225-
However, for performance reasons, it is sometimes preferable to avoid forcing the client to issue extra HTTP requests.
226-
It is possible to embed related objects (in their entirety, or only some of their properties) directly in the parent
227-
response through the use of serialization groups. By using the following serialization groups annotations (`@Groups`),
225+
However, for performance reasons, it is sometimes preferable to avoid forcing the client to issue extra HTTP requests.
226+
It is possible to embed related objects (in their entirety, or only some of their properties) directly in the parent
227+
response through the use of serialization groups. By using the following serialization groups annotations (`@Groups`),
228228
a JSON representation of the author is embedded in the book response:
229229

230230
```php
@@ -419,7 +419,7 @@ final class BookContextBuilder implements SerializerContextBuilderInterface
419419
{
420420
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
421421
$resourceClass = $context['resource_class'] ?? null;
422-
422+
423423
if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
424424
$context['groups'][] = 'admin:input';
425425
}
@@ -429,17 +429,17 @@ final class BookContextBuilder implements SerializerContextBuilderInterface
429429
}
430430
```
431431

432-
If the user has the `ROLE_ADMIN` permission and the subject is an instance of Book, `admin_input` group will be dynamically added to the
433-
denormalization context. The `$normalization` variable lets you check whether the context is for normalization (if `TRUE`) or denormalization
432+
If the user has the `ROLE_ADMIN` permission and the subject is an instance of Book, `admin_input` group will be dynamically added to the
433+
denormalization context. The `$normalization` variable lets you check whether the context is for normalization (if `TRUE`) or denormalization
434434
(`FALSE`).
435435

436436
## Changing the Serialization Context on a Per-item Basis
437437

438-
The example above demonstrates how you can modify the normalization/denormalization context based on the current user
438+
The example above demonstrates how you can modify the normalization/denormalization context based on the current user
439439
permissions for all books. Sometimes, however, the permissions vary depending on what book is being processed.
440440

441-
Think of ACL's: User "A" may retrieve Book "A" but not Book "B". In this case, we need to leverage the power of the
442-
Symfony Serializer and register our own normalizer that adds the group on every single item (note: priority `64` is
441+
Think of ACL's: User "A" may retrieve Book "A" but not Book "B". In this case, we need to leverage the power of the
442+
Symfony Serializer and register our own normalizer that adds the group on every single item (note: priority `64` is
443443
an example; it is always important to make sure your normalizer gets loaded first, so set the priority to whatever value
444444
is appropriate for your application; higher values are loaded earlier):
445445

@@ -452,7 +452,7 @@ services:
452452
- { name: 'serializer.normalizer', priority: 64 }
453453
```
454454

455-
The Normalizer class is a bit harder to understand, because it must ensure that it is only called once and that there is no recursion.
455+
The Normalizer class is a bit harder to understand, because it must ensure that it is only called once and that there is no recursion.
456456
To accomplish this, it needs to be aware of the parent Normalizer instance itself.
457457

458458
Here is an example:
@@ -492,7 +492,7 @@ class BookAttributeNormalizer implements ContextAwareNormalizerInterface, Normal
492492
493493
return $this->normalizer->normalize($object, $format, $context);
494494
}
495-
495+
496496
public function supportsNormalization($data, $format = null, array $context = [])
497497
{
498498
// Make sure we're not called twice
@@ -502,7 +502,7 @@ class BookAttributeNormalizer implements ContextAwareNormalizerInterface, Normal
502502
503503
return $data instanceof Book;
504504
}
505-
505+
506506
private function userHasPermissionsForBook($object): bool
507507
{
508508
// Get permissions from user in $this->tokenStorage
@@ -512,10 +512,10 @@ class BookAttributeNormalizer implements ContextAwareNormalizerInterface, Normal
512512
}
513513
```
514514

515-
This will add the serialization group `can_retrieve_book` only if the currently logged-in user has access to the given book
515+
This will add the serialization group `can_retrieve_book` only if the currently logged-in user has access to the given book
516516
instance.
517517

518-
Note: In this example, we use the `TokenStorageInterface` to verify access to the book instance. However, Symfony
518+
Note: In this example, we use the `TokenStorageInterface` to verify access to the book instance. However, Symfony
519519
provides many useful other services that might be better suited to your use case. For example, the [`AuthorizationChecker`](https://symfony.com/doc/current/components/security/authorization.html#authorization-checker).
520520

521521
## Name Conversion
@@ -539,7 +539,7 @@ api_platform:
539539

540540
## Decorating a Serializer and Adding Extra Data
541541

542-
In the following example, we will see how we add extra informations to the serialized output. Here is how we add the
542+
In the following example, we will see how we add extra informations to the serialized output. Here is how we add the
543543
date on each request in `GET`:
544544

545545
```yaml
@@ -598,7 +598,7 @@ final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface,
598598
{
599599
return $this->decorated->denormalize($data, $class, $format, $context);
600600
}
601-
601+
602602
public function setSerializer(SerializerInterface $serializer)
603603
{
604604
if($this->decorated instanceof SerializerAwareInterface) {
@@ -624,7 +624,7 @@ the `ApiPlatform\Core\Annotation\ApiProperty` annotation. For example:
624624
class Book
625625
{
626626
// ...
627-
627+
628628
/**
629629
* @ApiProperty(identifier=true)
630630
*/
@@ -659,7 +659,7 @@ App\Entity\Book:
659659
```
660660

661661
In some cases, you will want to set the identifier of a resource from the client (e.g. a client-side generated UUID, or a slug).
662-
In such cases, you must make the identifier property a writable class property. Specifically, to use client-generated IDs, you
662+
In such cases, you must make the identifier property a writable class property. Specifically, to use client-generated IDs, you
663663
must do the following:
664664

665665
1. create a setter for the identifier of the entity (e.g. `public function setId(string $id)`) or make it a `public` property ,
@@ -718,3 +718,69 @@ The JSON output will now include the embedded context:
718718
"author": "/people/59"
719719
}
720720
```
721+
722+
## Collection relation
723+
724+
This is a special case where, in an entity, you have a `toMany` relation. By default, Doctrine will use an `ArrayCollection` to store your values. This is fine when you have a *read* operation, but when you try to *write* you can observe some an issue where the response is not reflecting the changes correctly. It can lead to client errors even though the update was correct.
725+
Indeed, after an update on this relation, the collection looks wrong because `ArrayCollection`'s indexes are not sequential. To change this, we recommend to use a getter that returns `$collectionRelation->getValues()`. Thanks to this, the relation is now a real array which is sequentially indexed.
726+
727+
```php
728+
<?php
729+
730+
namespace App\Entity;
731+
732+
use ApiPlatform\Core\Annotation\ApiResource;
733+
use Doctrine\Common\Collections\ArrayCollection;
734+
use Doctrine\ORM\Mapping as ORM;
735+
736+
/**
737+
* @ApiResource
738+
* @ORM\Entity
739+
*/
740+
final class Brand
741+
{
742+
/**
743+
* @ORM\Id
744+
* @ORM\Column(type="integer")
745+
* @ORM\GeneratedValue(strategy="AUTO")
746+
*/
747+
private $id;
748+
749+
/**
750+
* @ORM\ManyToMany(targetEntity="App\Entity\Car", inversedBy="brands")
751+
* @ORM\JoinTable(
752+
* name="CarToBrand",
753+
* joinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id", nullable=false)},
754+
* inverseJoinColumns={@ORM\JoinColumn(name="car_id", referencedColumnName="id", nullable=false)}
755+
* )
756+
*/
757+
private $cars;
758+
759+
public function __construct()
760+
{
761+
$this->cars = new ArrayCollection();
762+
}
763+
764+
public function addCar(DummyCar $car)
765+
{
766+
$this->cars[] = $car;
767+
}
768+
769+
public function removeCar(DummyCar $car)
770+
{
771+
$this->cars->removeElement($car);
772+
}
773+
774+
public function getCars()
775+
{
776+
return $this->cars->getValues();
777+
}
778+
779+
public function getId()
780+
{
781+
return $this->id;
782+
}
783+
}
784+
```
785+
786+
For reference please check [#1534](https://github.com/api-platform/core/pull/1534).

core/validation.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,36 @@ api_platform:
367367
```
368368
369369
In this example, only `severity` and `anotherPayloadField` will be serialized.
370+
371+
## Validation on collection relations
372+
373+
Note: this is related to the [collection relation denormalization](./serialization.md#collection-relation).
374+
You may have an issue when trying to validate a relation representing a collection (`toMany`). After fixing the denormalization by using a getter that returns `$collectionRelation->getValues()`, you should define your validation on the getter instead of the property.
375+
376+
For example:
377+
378+
```xml
379+
<getter property="cars">
380+
<constraint name="Valid"/>
381+
</getter>
382+
```
383+
384+
```php
385+
final class Brand
386+
{
387+
// ...
388+
389+
public function __construct()
390+
{
391+
$this->cars = new ArrayCollection();
392+
}
393+
394+
/**
395+
* @Assert\Valid
396+
*/
397+
public function getCars()
398+
{
399+
return $this->cars->getValues();
400+
}
401+
}
402+
```

0 commit comments

Comments
 (0)