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 SubscribeCopia 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 | |
'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:read
adicional, 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.
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.
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 | |
} |
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.
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!
// 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
}
}
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 :
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