Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

La API de usuario y el serializador

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

La mayoría de nuestras páginas hasta ahora han sido páginas HTML normales. Así que vamos a crear una ruta pura de la API que devuelva datos JSON sobre el usuario autentificado en ese momento. Puede ser una ruta a la que llamemos desde nuestro propio JavaScript... o quizás estés creando una API para que la consuma otra persona. Más adelante hablaremos de ello.

Vamos a crear un nuevo controlador para esto llamado UserController... y hagamos que extienda nuestra clase BaseController:

... lines 1 - 2
namespace App\Controller;
... lines 4 - 7
class UserController extends BaseController
{
... lines 10 - 17
}

Dentro, añade un método llamado apiMe(). Dale un @Route() - autocompleta el del Componente Symfony - y establece la URL como /api/me:

... lines 1 - 5
use Symfony\Component\Routing\Annotation\Route;
class UserController extends BaseController
{
/**
* @Route("/api/me")
... line 12
*/
public function apiMe()
{
... line 16
}
}

Este no es un endpoint muy "restful", pero a menudo es conveniente tenerlo. Para requerir la autenticación para usar este endpoint, añade@IsGranted("IS_AUTHENTICATED_REMEMBERED"):

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 6 - 7
class UserController extends BaseController
{
/**
... line 11
* @IsGranted("IS_AUTHENTICATED_REMEMBERED")
*/
public function apiMe()
{
... line 16
}
}

En este proyecto estoy utilizando una mezcla de anotaciones y código PHP para denegar el acceso. Elige el que más te guste para tu aplicación. Dentro del método, podemos decir simplemente: devolver $this->json() y pasarle el usuario actual: $this->getUser():

... lines 1 - 7
class UserController extends BaseController
{
/**
* @Route("/api/me")
* @IsGranted("IS_AUTHENTICATED_REMEMBERED")
*/
public function apiMe()
{
return $this->json($this->getUser());
}
}

¡Qué bonito! Vamos a probarlo. Ahora mismo estamos conectados... así que podemos ir a/api/me y ver... ¡absolutamente nada! ¡Sólo corchetes vacíos!

Por defecto, cuando llamas a $this->json(), pasa los datos a la claseJsonResponse de Symfony. Y entonces esa clase llama a la función json_encode() de PHP en nuestro objeto User. En PHP, a menos que hagas un trabajo extra, cuando pasas un objeto ajson_encode(), lo único que hace es incluir las propiedades públicas. Como nuestra clase Userno tiene ninguna propiedad pública:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 19
private $id;
... lines 21 - 24
private $email;
... lines 26 - 29
private $roles = [];
... lines 31 - 34
private $firstName;
... lines 36 - 39
private $password;
private $plainPassword;
... lines 43 - 168
}

Obtenemos de vuelta una respuesta aburrida.

Aprovechando el serializador

Esto... no es suficiente. Así que, en su lugar, vamos a aprovechar el componente serializador de Symfony. Para instalarlo, en tu terminal, ejecuta:

composer require "serializer:1.0.4"

Esto instala el paquete del serializador, que incluye el componente Serializer de Symfony, así como algunas otras librerías que le ayudan a funcionar de forma realmente inteligente. Pero no tiene una receta que haga nada del otro mundo: sólo instala el código.

Una de las cosas buenas de utilizar $this->json() es que, en cuanto se instala el serializador de Symfony, automáticamente se empieza a utilizar para serializar los datos en lugar del normal json_encode(). En otras palabras, cuando actualizamos la ruta, ¡funciona!

Añadir grupos de serialización

No vamos a hablar demasiado de cómo funciona el serializador de Symfony, ya que hablamos mucho de él en nuestros tutoriales de la Plataforma API. Pero al menos vamos a dar unas pinceladas básicas.

Por defecto, el serializador serializará cualquier propiedad pública o cualquier propiedad que tenga un "getter" en ella. Incluso serializará displayName -que no es una propiedad real- porque hay un método getDisplayName().

En realidad... esto es demasiada información para incluirla en la ruta. Así que tomemos más control. Podemos hacerlo diciéndole al serializador que sólo serialice los campos que están en un grupo específico. Pasa 200 para el código de estado, un array de cabeceras vacío -ambos son los valores por defecto- para que podamos llegar al cuarto argumento de $context:

... lines 1 - 7
class UserController extends BaseController
{
... lines 10 - 13
public function apiMe()
{
return $this->json($this->getUser(), 200, [], [
... line 17
]);
}
}

Es una especie de "opciones" que pasas al serializador. Pasa una llamadagroups establecida en un array. Voy a inventar un grupo llamado user:read... porque estamos "leyendo" de "usuario":

... lines 1 - 7
class UserController extends BaseController
{
... lines 10 - 13
public function apiMe()
{
return $this->json($this->getUser(), 200, [], [
'groups' => ['user:read']
]);
}
}

Copia ese nombre de grupo. Ahora, dentro de la entidad User, tenemos que añadir este grupo a todos los campos que queramos incluir en la API. Por ejemplo, vamos a incluir id. Sobre la propiedad, añade una anotación o atributo PHP: @Groups(). Asegúrate de autocompletar el del serializador de Symfony para obtener la declaración useen la parte superior. Dentro, pegaré user:read:

... lines 1 - 8
use Symfony\Component\Serializer\Annotation\Groups;
... lines 10 - 13
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
... lines 17 - 19
* @Groups("user:read")
*/
private $id;
... lines 23 - 175
}

Copia eso y... vamos a exponer email, no queremos exponer roles, sí afirstName y... ya está:

... lines 1 - 13
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
... lines 17 - 19
* @Groups("user:read")
*/
private $id;
/**
... line 25
* @Groups("user:read")
*/
private $email;
... lines 29 - 34
/**
... line 36
* @Groups("user:read")
*/
private $firstName;
... lines 40 - 175
}

También podríamos poner el grupo encima de getDisplayName() si quisiéramos incluirlo... o getAvatarUri()... en realidad lo añadiré ahí:

... lines 1 - 13
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 16 - 159
/**
* @Groups("user:read")
*/
public function getAvatarUri(int $size = 32): string
{
... lines 165 - 169
}
... lines 171 - 175
}

¡Intentémoslo! Refresca y... ¡superguay! ¡Tenemos esos 4 campos!

Y fíjate en una cosa: aunque se trata de una "ruta de la API"... y nuestra ruta de la API requiere que estemos conectados, podemos acceder totalmente a esto... aunque no tengamos un sistema de autenticación de tokens de la API. Tenemos acceso gracias a nuestra cookie de sesión normal.

Así que eso nos lleva a nuestra siguiente pregunta: si tienes rutas de API como ésta, ¿necesitas un sistema de autenticación por token de API o no? Abordemos ese tema a continuación.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}