Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Normalizador personalizado

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

Copia el método de prueba - testOwnerCanSeeIsPublishedField. Acabamos de añadir algo de magia para que los usuarios administradores puedan ver la propiedad isPublished. Este método prueba nuestra próxima misión: que los propietarios de un DragonTreasure también puedan verlo.

Ejecútalo con:

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField

Y... falla: esperaba que null fuera igual que false, porque el campo no se devuelve en absoluto.

Para solucionarlo, en DragonTreasure, añade un tercer grupo especial: owner:read:

... lines 1 - 88
class DragonTreasure
{
... lines 91 - 128
#[Groups(['admin:read', 'admin:write', 'owner:read'])]
private bool $isPublished = false;
... lines 131 - 249
}

¿Ves adónde queremos llegar con esto? Si somos propietarios de un DragonTreasure, añadiremos este grupo y entonces se incluirá el campo. Sin embargo, conseguir esto es complicado.

Como hablamos en el último vídeo, los grupos de normalización empiezan siendo estáticos: viven aquí arriba, en nuestra configuración. El constructor de contexto nos permite hacer que estos grupos sean dinámicos por petición. Así, si somos un usuario administrador, podemos añadir un grupo admin:readadicional, que se utilizará al serializar cada objeto para toda esta petición.

Pero en esta situación, necesitamos que el grupo sea dinámico por objeto. Imagina que devolvemos 10 DragonTreasure's: puede que el usuario sólo sea propietario de uno de ellos, por lo que sólo ese DragonTreasure debería normalizarse utilizando este grupo extra.

El trabajo de los normalizadores

Para manejar este nivel de control, necesitamos un normalizador personalizado. Los normalizadores son el núcleo del serializador de Symfony. Son responsables de convertir un dato -como un objeto ApiResource o un objeto DateTime que vive en una propiedad- en un valor escalar o de matriz. Creando un normalizador personalizado, ¡puedes hacer prácticamente cualquier cosa rara que quieras!

Busca tu terminal y ejecuta:

php  bin/console debug:container --tag=serializer.normalizer

Esto me encanta: ¡nos muestra todos y cada uno de los normalizadores de nuestra aplicación! Podemos ver cosas que se encargan de normalizar UUIDs.... esto es lo que normaliza cualquiera de nuestros objetos ApiResource a JSON-LD y aquí hay uno para un DateTime. Hay un montón de cosas interesantes.

Nuestro objetivo es crear nuestro propio normalizador, decorar un normalizador central existente y, a continuación, añadir el grupo dinámico antes de que se llame a ese normalizador central.

Crear la clase normalizador

Así que, ¡manos a la obra! En src/ -en realidad no importa cómo organicemos las cosas- voy a crear un nuevo directorio llamado Normalizer. Permíteme colapsar algunas cosas... para que sea más fácil verlo. Dentro de eso, añade una nueva clase llamada, qué tal, AddOwnerGroupsNormalizer. Todos los normalizadores deben implementarNormalizerInterface... luego ve a "Código"->"Generar" o Command+N en un Mac y selecciona "Implementar métodos" para añadir los dos que necesitamos:

... lines 1 - 2
namespace App\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class AddOwnerGroupsNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = [])
{
// TODO: Implement normalize() method.
}
public function supportsNormalization(mixed $data, string $format = null)
{
// TODO: Implement supportsNormalization() method.
}
}

Esto funciona así: en cuanto implementemos NormalizerInterface, cada vez que se normalice cualquier dato, se llamará a nuestro método supportsNormalization(). Allí, podemos decidir si sabemos o no normalizar esa cosa. Si devolvemostrue, el serializador llamará entonces a normalize(), nos pasará esos datos, y entonces devolveremos la versión normalizada.

Y en realidad, para evitar algunos errores de desaprobación, abre la clase padre. El tipo de retorno es esta cosa loca de array. Cópialo... y añádelo como tipo de retorno. No hace falta que lo hagas -todo funcionaría sin ello-, pero recibirías un aviso de obsoleto en tus pruebas.

Abajo para supportsNormalization(), en Symfony 7, habrá un argumento array $context... y el método devolverá un bool:

... lines 1 - 6
class AddOwnerGroupsNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
... lines 10 - 12
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
... lines 15 - 17
}

¿Qué Servicio Decoramos?

Antes de rellenar esto o configurar la decoración, tenemos que pensar qué servicio del núcleo vamos a decorar. Ésta es mi idea: si sustituimos el servicio principal del núcleonormalizer por esta clase, podríamos añadir el grupo y luego llamar al normalizador decorado... para que todo funcione entonces como siempre, excepto que tiene el grupo extra.

De vuelta al terminal, Ejecuta:

bin/console debug:container normalizer

Obtenemos un montón de resultados. Eso tiene sentido: hay un normalizer principal, pero luego el propio normalizer tiene montones de otros normalizadores dentro de él para manejar distintos tipos de datos. Entonces... ¿dónde está el normalizador de nivel superior? En realidad, ni siquiera está en esta lista: se llama serializer. Aunque, como veremos a continuación, ni siquiera eso es del todo correcto.

Leave a comment!

3
Login or Register to join the conversation
Cyril Avatar

Hi! I have an issue by using a custom normalizer (but not with API Platform). As mentioned in the Symfony documentation, it's possible to create a custom normalizer and inject services in its constructor :

class MyCustomNormalizer implements NormalizerInterface
{
    public function __construct(
        private UrlGeneratorInterface $router,
        private ObjectNormalizer $normalizer,
    ) {
    }

But when I try to instantiate it by calling $serializer = new Serializer([new MyCustomNormalizer()], [new Converter()]) I have an issue like "to few arguments for MyCustomNormalizer: 0 passed, 2 expected..."

What is the correct way to do that?
Thanks for your help
Cyril

Reply

Hi,

I'm not fully understand what are you trying to do, but in Symfony(not ApiPlatform) all you need just create this normalizer and that's all, it will be automatically registered in system and it will be used for objects which are defined in support() method

$serializer = new Serializer([new MyCustomNormalizer()], [new Converter()])

Such things will never work, you are instantiating Classes manually so you need to get all arguments from service container or instantiate them as they are defined!

Cheers!

Reply
Cyril Avatar

Thanks for your help, I didn't realize custom normalizers don't need to be called manually!

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