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 SubscribeTenemos una entidad User
... pero aún no forma parte de nuestra API. ¿Cómo hacemos que forme parte de la API? Ah, ¡ya lo sabemos! Ve encima de la clase y añade el atributo ApiResource
.
... lines 1 - 4 | |
use ApiPlatform\Metadata\ApiResource; | |
... lines 6 - 12 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 16 - 116 | |
} |
Actualiza la documentación. ¡Fíjate! ¡Seis nuevas rutas para la clase User
! Y gracias a nuestros fixtures, deberíamos poder ver los datos inmediatamente. Probemos la ruta de recogida. Ejecuta y... está vivo.
Aunque... es un poco raro que aparezcan campos como roles
y password
. Ah, nos preocuparemos de eso en un minuto.
Antes de seguir avanzando, quiero mencionar una cosa rápida sobre los UUID. Como puedes ver, estamos utilizando UUID autoincrementados para nuestra API: siempre es/api/users/
y luego el id de la entidad. Pero puedes utilizar un UUID
en su lugar. Y eso es algo que haremos en un futuro tutorial.
Pero... ¿por qué utilizar UUIDs? Bueno, a veces puede hacer la vida más fácil en JavaScript cuando se trabaja con frameworks frontales. De hecho, puedes generar elUUID
en JavaScript y luego enviarlo a tu API al crear un nuevo recurso. Esto puede ser útil porque tu JavaScript conoce el identificador del recurso inmediatamente y puede actualizar el estado... en lugar de esperar a que termine la petición Ajax para obtener el nuevo identificador autoincrementado.
En cualquier caso, lo que quiero decir es que API Platform admite UUIDs
. Podrías añadir una nueva columna UUID y decirle a API Platform que ese debe ser tu identificador. Ah, pero ten en cuenta que algunos motores de bases de datos -como MySQL- pueden tener un rendimiento deficiente si haces del UUID la clave primaria. En ese caso, mantén id
como clave principal y añade una columna UUID adicional.
En cualquier caso, ¡volvamos a nuestro recurso User
! Ahora mismo, devuelve demasiados campos. Afortunadamente, sabemos cómo solucionarlo. Arriba, en ApiResource
, añade una clavenormalizationContext
con groups
establecida en user:read
para seguir el mismo patrón que utilizamos en DragonTreasure
. Añade también denormalizationContext
fijado en user:write
.
... lines 1 - 13 | |
( | |
normalizationContext: ['groups' => ['user:read']], | |
denormalizationContext: ['groups' => ['user:write']], | |
) | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 20 - 123 | |
} |
Ahora sólo tenemos que decorar los campos que queramos en la API. No necesitamos id
... ya que siempre tenemos @id
, que es más útil. Pero sí queremos email
. Así que añade el atributo #Groups()
, pulsa tabulador para añadir esa declaración use
y pasa user:read
y user:write
.
... lines 1 - 9 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
... lines 11 - 13 | |
( | |
normalizationContext: ['groups' => ['user:read']], | |
denormalizationContext: ['groups' => ['user:write']], | |
) | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 20 - 25 | |
'user:read', 'user:write']) ([ | |
private ?string $email = null; | |
... lines 28 - 123 | |
} |
Copia eso... y baja a password
. Necesitamos que la contraseña sea escribible pero no legible. Así que añade user:write
.
... lines 1 - 9 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
... lines 11 - 13 | |
( | |
normalizationContext: ['groups' => ['user:read']], | |
denormalizationContext: ['groups' => ['user:write']], | |
) | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 20 - 25 | |
'user:read', 'user:write']) ([ | |
private ?string $email = null; | |
... lines 28 - 35 | |
'user:write']) ([ | |
private ?string $password = null; | |
... lines 38 - 123 | |
} |
Esto todavía no es del todo correcto. El campo password
debe contener la contraseña cifrada. Pero nuestros usuarios, por supuesto, enviarán las contraseñas en texto plano a través de la API cuando creen un usuario o actualicen su contraseña. Entonces haremos el hash. Eso es algo que resolveremos en un tutorial futuro, cuando hablemos más de seguridad, pero esto bastará por ahora.
Ah, y encima de username
, añade también user:read
y user:write
.
... lines 1 - 9 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
... lines 11 - 13 | |
( | |
normalizationContext: ['groups' => ['user:read']], | |
denormalizationContext: ['groups' => ['user:write']], | |
) | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 20 - 25 | |
'user:read', 'user:write']) ([ | |
private ?string $email = null; | |
... lines 28 - 35 | |
'user:write']) ([ | |
private ?string $password = null; | |
... lines 38 - 39 | |
'user:read', 'user:write']) ([ | |
private ?string $username = null; | |
... lines 42 - 123 | |
} |
¡Genial! Actualiza los documentos... y abre la ruta de las colecciones para probarlo. El resultado... ¡exactamente lo que queríamos! Sólo vuelven email
y username
.
Y si creáramos un nuevo usuario... ¡sí! Los campos escribibles son email
,username
, y password
.
Vale, ¿qué más nos falta? ¿Qué tal la validación? Si probamos la ruta POST con datos vacíos... obtendremos el desagradable error 500. ¡Hora de arreglarlo!
De nuevo en el archivo, empieza por encima de la clase para asegurarte de que tanto email
comousername
son unique
. Añade UniqueEntity
pasando fields
a email
... e incluso podemos incluir un mensaje. Repite lo mismo... pero cambia email
por username
.
... lines 1 - 10 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
... lines 12 - 18 | |
fields: ['email'], message: 'There is already an account with this email') ( | |
fields: ['username'], message: 'It looks like another dragon took your username. ROAR!') ( | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 23 - 129 | |
} |
A continuación, abajo en email
, añade NotBlank
... luego añadiré el Assert
delante... y retocaré la declaración use
para que funcione igual que la última vez.
... lines 1 - 10 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 13 - 18 | |
fields: ['email'], message: 'There is already an account with this email') ( | |
fields: ['username'], message: 'It looks like another dragon took your username. ROAR!') ( | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 23 - 29 | |
... line 31 | |
private ?string $email = null; | |
... lines 33 - 129 | |
} |
Bien. el correo electrónico necesita uno más - Assert\Email
- y encima de username
, añadir NotBlank
.
... lines 1 - 10 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 13 - 18 | |
fields: ['email'], message: 'There is already an account with this email') ( | |
fields: ['username'], message: 'It looks like another dragon took your username. ROAR!') ( | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 23 - 29 | |
private ?string $email = null; | |
... lines 33 - 45 | |
private ?string $username = null; | |
... lines 48 - 129 | |
} |
Ahora mismo no me preocupa demasiado password
... porque ya es un poco raro.
¡Vamos a probar esto! Desplázate hacia arriba y envía un campo password
. Y... ¡sí! El simpático código de estado 422 con errores de validación. Prueba ahora con datos válidos: pasa un email
y un username
... aunque no estoy seguro de que este tipo sea realmente un dragón... quizá necesitemos un captcha.
Pulsa Ejecutar. Ya está ¡Código de estado 201 con email
y username
devueltos!
Nuestro recurso tiene validación, paginación y contiene una gran información! E incluso podríamos añadir filtros fácilmente. En otras palabras, ¡lo estamos machacando!
Y ahora llegamos a la parte realmente interesante. Tenemos que "relacionar" nuestros dos recursos para que cada tesoro pertenezca a un usuario. ¿Qué aspecto tiene eso en API Platform? Es superinteresante, y es lo siguiente.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.0.8
"doctrine/annotations": "^1.0", // 1.14.2
"doctrine/doctrine-bundle": "^2.8", // 2.8.0
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.0
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.64.1
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.15.3
"symfony/asset": "6.2.*", // v6.2.0
"symfony/console": "6.2.*", // v6.2.3
"symfony/dotenv": "6.2.*", // v6.2.0
"symfony/expression-language": "6.2.*", // v6.2.2
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.3
"symfony/property-access": "6.2.*", // v6.2.3
"symfony/property-info": "6.2.*", // v6.2.3
"symfony/runtime": "6.2.*", // v6.2.0
"symfony/security-bundle": "6.2.*", // v6.2.3
"symfony/serializer": "6.2.*", // v6.2.3
"symfony/twig-bundle": "6.2.*", // v6.2.3
"symfony/ux-react": "^2.6", // v2.6.1
"symfony/validator": "6.2.*", // v6.2.3
"symfony/webpack-encore-bundle": "^1.16", // v1.16.0
"symfony/yaml": "6.2.*" // v6.2.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"symfony/debug-bundle": "6.2.*", // v6.2.1
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/stopwatch": "6.2.*", // v6.2.0
"symfony/web-profiler-bundle": "6.2.*", // v6.2.4
"zenstruck/foundry": "^1.26" // v1.26.0
}
}