If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNuestra misión es clara: configurar nuestro normalizador para que decore el servicio normalizador del núcleo de Symfony, de modo que podamos añadir el grupo owner:read
cuando sea necesario y, a continuación, llamar al normalizador decorado.
¡Y ya conocemos la decoración! Añade public function __construct()
conprivate NormalizerInterface $normalizer
:
... lines 1 - 4 | |
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | |
class AddOwnerGroupsNormalizer implements NormalizerInterface | |
{ | |
public function __construct(private NormalizerInterface $normalizer) | |
{ | |
} | |
... lines 12 - 23 | |
} |
Abajo en normalize()
, añade un dump()
luego return $this->normalizer->normalize()
pasando $object
$format
, y $context
. Para supportsNormalization()
, haz lo mismo: llama a supportsNormalization()
en la clase decorada y pasa los args:
... lines 1 - 6 | |
class AddOwnerGroupsNormalizer implements NormalizerInterface | |
{ | |
... lines 9 - 12 | |
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null | |
{ | |
dump('IT WORKS!'); | |
return $this->normalizer->normalize($object, $format, $context); | |
} | |
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool | |
{ | |
return $this->normalizer->supportsNormalization($data, $format); | |
} | |
} |
Para completar la decoración, dirígete a la parte superior de la clase. Quitaré unas cuantas declaraciones antiguas use
... y luego diré #[AsDecorator]
pasando serializer
, que ya he mencionado que es el id de servicio para el normalizador principal de nivel superior:
... lines 1 - 4 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
... lines 6 - 7 | |
'serializer') ( | |
class AddOwnerGroupsNormalizer implements NormalizerInterface | |
{ | |
... lines 11 - 25 | |
} |
¡Vale! Aún no hemos hecho ningún cambio... así que deberíamos seguir viendo la única prueba que falla. Pruébalo:
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
¡Woh! ¡Una explosión! Guau.
ValidationExceptionListener::__construct()
: El argumento nº 1 ($serializer
) debe ser de tipoSerializerInterface
,AddOwnerGroupsNormalizer
dado.
¿De acuerdo? Cuando añadimos #[AsDecorator('serializer')]
, significa que nuestro servicio sustituye al servicio conocido como serializer
. Así, todos los que dependen del servicio serializer
pasarán ahora a nosotros... y luego elserializer
original se pasa a nuestro constructor.
Entonces, ¿cuál es el problema? La decoración ya ha funcionado varias veces. El problema es que el servicio serializer
de Symfony es... un poco grande. ImplementaNormalizerInterface
, ¡pero también DenormalizerInterface
, EncoderInterface
,DecoderInterface
y SerializerInterface
! Pero nuestro objeto sólo implementa uno de ellos. Y así, cuando nuestra clase se pasa a algo que espera un objeto con una de esas otras 4 interfaces, explota.
Si de verdad quisiéramos decorar el servicio serializer
, tendríamos que implementar las cinco interfaces... lo cual es feo y demasiado. ¡Y no pasa nada!
En lugar de decorar el nivel superior normalizer
, vamos a decorar un normalizador concreto: el que se encarga de normalizar los objetos ApiResource
enJSON-LD
. Éste es otro punto en el que puedes confiar en la documentación para que te dé el ID de servicio exacto que necesitas. Es api_platform.jsonld.normalizer.item
:
... lines 1 - 4 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
... lines 6 - 7 | |
'api_platform.jsonld.normalizer.item') ( | |
class AddOwnerGroupsNormalizer implements NormalizerInterface | |
{ | |
... lines 11 - 25 | |
} |
Vuelve a hacer la prueba: testOwnerCanSeeIsPublishedField
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
¡Sí! ¡Vemos nuestro volcado! Y... ¿un error 400? Déjame abrir la respuesta para que podamos verla. Extraño:
El serializador inyectado debe ser una instancia de
NormalizerInterface
.
Y procede de lo más profundo del código del serializador de API Platform. Así que... decorar normalizadores no es un proceso muy amigable. Está bien documentado, pero es raro. Cuando decoras este normalizador específico, también tienes que implementarSerializerAwareInterface
. Y eso va a requerir que tengas un método setSerializer()
. Oh, déjame importar esa declaración use
: No sé por qué no ha aparecido automáticamente:
... lines 1 - 6 | |
use Symfony\Component\Serializer\SerializerAwareInterface; | |
... lines 8 - 10 | |
class AddOwnerGroupsNormalizer implements NormalizerInterface, SerializerAwareInterface | |
{ | |
... lines 13 - 28 | |
public function setSerializer(SerializerInterface $serializer) | |
{ | |
... lines 31 - 33 | |
} | |
} |
Ya está.
Dentro, digamos, si $this->normalizer
es un instanceof SerializerAwareInterface
, entonces llama a $this->normalizer->setSerializer($serializer)
:
... lines 1 - 10 | |
class AddOwnerGroupsNormalizer implements NormalizerInterface, SerializerAwareInterface | |
{ | |
... lines 13 - 28 | |
public function setSerializer(SerializerInterface $serializer) | |
{ | |
if ($this->normalizer instanceof SerializerAwareInterface) { | |
$this->normalizer->setSerializer($serializer); | |
} | |
} | |
} |
Ni siquiera quiero entrar en los detalles de esto: lo que ocurre es que el normalizador que estamos decorando implementa otra interfaz... así que también tenemos que implementarla.
Intentémoslo de nuevo.
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
Por último, tenemos el volcado y falla la aserción que esperamos... puesto que aún no hemos añadido el grupo. ¡Hagámoslo!
Recuerda el objetivo: si poseemos este DragonTreasure
, queremos añadir el grupo owner:read
. En el constructor, autocablea el servicio Security
como una propiedad:
... lines 1 - 5 | |
use Symfony\Bundle\SecurityBundle\Security; | |
... lines 7 - 12 | |
class AddOwnerGroupsNormalizer implements NormalizerInterface, SerializerAwareInterface | |
{ | |
public function __construct(private NormalizerInterface $normalizer, private Security $security) | |
{ | |
} | |
... lines 18 - 38 | |
} |
Entonces, aquí abajo, si $object
es un instanceof DragonTreasure
-porque este método se llamará para todas nuestras clases de recursos API- y $this->security->getUser()
es igual a $object->getOwner()
, entonces llama a $context['groups'][]
para añadirowner:read
:
... lines 1 - 4 | |
use App\Entity\DragonTreasure; | |
... lines 6 - 12 | |
class AddOwnerGroupsNormalizer implements NormalizerInterface, SerializerAwareInterface | |
{ | |
... lines 15 - 18 | |
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null | |
{ | |
if ($object instanceof DragonTreasure && $this->security->getUser() === $object->getOwner()) { | |
$context['groups'][] = 'owner:read'; | |
} | |
... lines 24 - 25 | |
} | |
... lines 27 - 38 | |
} |
¡Uf! Intenta esa prueba una vez más:
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
¡Lo hemos conseguido! Ahora podemos devolver diferentes campos objeto por objeto.
Si quieres añadir también owner:write
durante la desnormalización, tendrías que implementar una segunda interfaz. No voy a hacerlo entero... pero implementarías DenormalizerInterface
, añadirías los dos métodos necesarios, llamarías al servicio decorado... y cambiarías el argumento para que fuera un tipo de unión deNormalizerInterface
y DenormalizerInterface
.
Por último, el servicio que estás decorando para la desnormalización es diferente: esapi_platform.serializer.normalizer.item
. Sin embargo, si quieres decorar tanto el normalizador como el desnormalizador en la misma clase, tendrías que eliminar#[AsDecorator]
y mover la configuración de la decoración a services.yaml
... porque un único servicio no puede decorar dos cosas a la vez. API Platform lo explica en sus documentos.
De acuerdo, voy a deshacer todo eso... y limitarme a añadir owner:read
. A continuación: ahora que tenemos un normalizador personalizado, podemos hacer fácilmente locuras como añadir un campo totalmente personalizado a nuestra API que no existe en nuestra clase.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.1.2
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.8.3
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.1
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.66.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.16.1
"symfony/asset": "6.2.*", // v6.2.5
"symfony/console": "6.2.*", // v6.2.5
"symfony/dotenv": "6.2.*", // v6.2.5
"symfony/expression-language": "6.2.*", // v6.2.5
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.5
"symfony/property-access": "6.2.*", // v6.2.5
"symfony/property-info": "6.2.*", // v6.2.5
"symfony/runtime": "6.2.*", // v6.2.5
"symfony/security-bundle": "6.2.*", // v6.2.6
"symfony/serializer": "6.2.*", // v6.2.5
"symfony/twig-bundle": "6.2.*", // v6.2.5
"symfony/ux-react": "^2.6", // v2.7.1
"symfony/ux-vue": "^2.7", // v2.7.1
"symfony/validator": "6.2.*", // v6.2.5
"symfony/webpack-encore-bundle": "^1.16", // v1.16.1
"symfony/yaml": "6.2.*" // v6.2.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.3
"symfony/browser-kit": "6.2.*", // v6.2.5
"symfony/css-selector": "6.2.*", // v6.2.5
"symfony/debug-bundle": "6.2.*", // v6.2.5
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.2.5
"symfony/stopwatch": "6.2.*", // v6.2.5
"symfony/web-profiler-bundle": "6.2.*", // v6.2.5
"zenstruck/browser": "^1.2", // v1.2.0
"zenstruck/foundry": "^1.26" // v1.28.0
}
}
Hello,
I'm trying to make a denormalizer but I don't fit in it while I fit well in
if ($this->denormalizer instanceof DenormalizerInterface) {
Thanks for your help