Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Campos totalmente personalizados

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

Pongámonos salvajes. Quiero añadir un nuevo campo totalmente personalizado y loco a nuestra API DragonTreasure que no se corresponda con ninguna propiedad de nuestra clase. Bueno, en realidad, en la parte 1 de esta serie aprendimos que es posible añadir campos personalizados creando un método getter y añadiendo un grupo de serialización sobre él. Pero esa solución sólo funciona si podemos calcular el valor del campo únicamente a partir de los datos del objeto. Si, por ejemplo, necesitamos llamar a un servicio para obtener los datos, entonces no tendremos suerte.

Añadir un nuevo campo cuyos datos se calculen a partir de un servicio es otro as en la manga del normalizador personalizado. Y como ya tenemos uno configurado, he pensado que podríamos utilizarlo para ver cómo funciona.

Prueba del campo IsMe

Ve a DragonTreasureResourceTest y buscatestOwnerCanSeeIsPublishedField(). Cámbiale el nombre atestOwnerCanSeeIsPublishedAndIsMineFields():

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 158
public function testOwnerCanSeeIsPublishedAndIsMineFields(): void
{
... lines 161 - 178
}
}

Esto es un poco tonto, pero si tenemos un DragonTreasure, vamos a añadir una nueva propiedad booleana llamada $isMine establecida en true. Así que, abajo del todo, diremos isMine y esperaremos que sea true:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 158
public function testOwnerCanSeeIsPublishedAndIsMineFields(): void
{
... lines 161 - 166
$this->browser()
... lines 168 - 175
->assertJsonMatches('isPublished', false)
->assertJsonMatches('isMine', true)
;
}
}

Copia ese nombre de método, luego gira y ejecuta esta prueba:

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedAndIsMineFields

¡Tada! Es null porque el campo aún no existe.

Devolver el campo personalizado

¿Cómo podemos añadirlo? Ahora que hemos pasado por el engorro de configurar el normalizador, ¡es fácil! El sistema normalizador hará lo suyo, devolverá los datos normalizados y luego, entre eso y la declaración return, podemos... ¡joder!

... lines 1 - 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';
}
$normalized = $this->normalizer->normalize($object, $format, $context);
... lines 26 - 30
return $normalized;
}
... lines 33 - 44
}

Copia la sentencia if de aquí arriba. Podría ser más inteligente y reutilizar código, pero está bien. Si el objeto es un DragonTreasure y poseemos esteDragonTreasure, diremos $normalized['isMine'] = true:

... lines 1 - 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
{
... lines 21 - 24
$normalized = $this->normalizer->normalize($object, $format, $context);
if ($object instanceof DragonTreasure && $this->security->getUser() === $object->getOwner()) {
$normalized['isMine'] = true;
}
return $normalized;
}
... lines 33 - 44
}

¡Ya está! Cuando ejecutemos la prueba:

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedAndIsMineFields

¡Todo verde!

Campos personalizados que faltan en los documentos

Pero estos campos personalizados tienen un inconveniente práctico: no estarán documentados en nuestra API. ¡Nuestros documentos de la API no tienen ni idea de que esto existe!

Si necesitas un supercampo personalizado que requiera lógica de servicio... y necesitas que esté documentado, tienes dos opciones. En primer lugar, podrías añadir una propiedad no persistente isMe a tu clase y luego rellenarla con un proveedor de estado. Aún no hemos hablado de los proveedores de estado, pero son la forma en que se cargan los datos. Por ejemplo, nuestras clases ya utilizan un proveedor de estado Doctrine entre bastidores para consultar la base de datos. Hablaremos de los proveedores de estado en la parte 3 de esta serie.

La segunda solución sería utilizar el normalizador personalizado como hicimos nosotros, y luego intentar añadir el campo a los documentos OpenAPI manualmente mediante el truco de la fábrica OpenAPI que mostramos antes.

A continuación: supongamos que un usuario tiene permiso para editar algo... pero hay ciertos cambios en los datos que no puede hacer -por ejemplo, podría establecer un campo enfoo pero no puede cambiarlo a bar porque no tiene suficientes permisos. ¿Cómo debemos manejar esto? Es la seguridad unida a la validación.

Leave a comment!

2
Login or Register to join the conversation
Joel-L Avatar
Joel-L Avatar Joel-L | posted hace 1 mes | edited

Hello,

i'm eager to watch the part 3 of this series, when is it scheduled?

Especially State Provider :)

cheers

Reply

Hey @Joel-L!

I'm eager to record it! I'm in the planning/outline stage now - I'd say a few weeks. I'd like to get it out ASAP - but I'm also gone for a week at the end of July. But it's not far off.

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