Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Decoración del normalizador y "Normalizer Aware" (consciente del normalizador)

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Nuestra 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.

Configuración para la decoración

¡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
#[AsDecorator('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 tipo SerializerInterface, 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!

Decorar un normalizador de nivel inferior

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
#[AsDecorator('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!

Añadir el grupo dinámico

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.

Decorar también el desnormalizador

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.

Leave a comment!

2
Login or Register to join the conversation

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


namespace App\Serializer;

use App\Entity\DragonTreasure;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

#[AsDecorator('api_platform.serializer.normalizer.item')]
class PlainIdentifierDenormalizer implements /*NormalizerInterface, */ DenormalizerInterface, DenormalizerAwareInterface
{
    public function __construct(
        private DenormalizerInterface $denormalizer,
    ) {
    }

    public function setDenormalizer(DenormalizerInterface $denormalizer)
    {
        if ($this->denormalizer instanceof DenormalizerInterface) {
            $this->denormalizer->setSerializer($denormalizer);
        }
    }

    public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
    {
        dd('yes');
    }

    public function supportsDenormalization(mixed $data, string $type, string $format = null)
    {
        dd('ok');
        return $this->denormalizer->supportsDenormalization($data, $format);
    }
}
Reply

Hey @aratinau

I believe you're decorating the wrong service, this is the service you should decorate api_platform.jsonld.normalizer.item

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice