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 SubscribePongá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.
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.
¿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!
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.
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!
// 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 eager to watch the part 3 of this series, when is it scheduled?
Especially State Provider :)
cheers