Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Métodos de usuario personalizados y el usuario en un servicio

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

Sabemos cómo obtener el objeto usuario actual en un controlador. ¿Y desde Twig? Dirígete a base.html.twig. Veamos... aquí es donde se renderizan nuestros enlaces de "cierre de sesión" e "inicio de sesión". Intentemos renderizar el nombre del usuario aquí mismo.

App.user en Twig

¿Cómo? En Twig, tenemos acceso a una única variable global llamada app, que tiene un montón de cosas útiles, como app.session y app.request. También tieneapp.user, que será el objeto actual User o null. Así que podemos decirapp.user.firstName:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ app.user.firstName }}
... line 41
{% else %}
... lines 43 - 44
{% endif %}
</div>
</div>
</nav>
... lines 49 - 53
</body>
</html>

Esto es seguro porque estamos dentro de la comprobación de is_granted()... así que sabemos que hay un User.

¡Vamos a probarlo! Cierra el perfilador, actualiza la página y... ¡perfecto! ¡Parece que me llamo Tremayne!

Ahora que tenemos esto... es hora de hacerlo más elegante. Dentro de la comprobación de is_granted(), voy a pegar un gran menú de usuario: puedes conseguirlo en el bloque de código de esta página:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
class="dropdown-toggle btn"
type="button"
id="user-dropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<img
src="https://ui-avatars.com/api/?name=John+Doe&size=32&background=random"
alt="John Doe Avatar">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="#">Log Out</a>
</li>
</ul>
</div>
{{ app.user.firstName }}
<a class="nav-link text-black-50" href="{{ path('app_logout') }}">Log Out</a>
{% else %}
... lines 61 - 62
{% endif %}
</div>
</div>
</nav>
... lines 67 - 71
</body>
</html>

Esto está completamente codificado para empezar... ¡pero se renderiza muy bien!

Vamos a hacerlo dinámico... hay algunos puntos. Para la imagen, estoy utilizando una API de avatar. Sólo tenemos que quitar la parte "Juan Pérez" e imprimir el nombre real del usuario: app.user.firstName. Y luego canalizar eso en |url_encodepara que sea seguro poner una URL. También renderiza app.user.firstName dentro del texto alt:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
src="https://ui-avatars.com/api/?name={{ app.user.firstName|url_encode }}&size=32&background=random"
alt="{{ app.user.firstName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Para el enlace de "cierre de sesión", roba la función path() de abajo... y ponla aquí:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
... lines 41 - 51
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="{{ path('app_logout') }}">Log Out</a>
</li>
</ul>
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Elimina lo anterior en la parte inferior para terminar con esto:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
class="dropdown-toggle btn"
type="button"
id="user-dropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<img
src="https://ui-avatars.com/api/?name={{ app.user.firstName|url_encode }}&size=32&background=random"
alt="{{ app.user.firstName }} Avatar">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="{{ path('app_logout') }}">Log Out</a>
</li>
</ul>
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

¡Genial! Cuando refresquemos... ¡voilà! Un verdadero menú desplegable de usuario.

Añadir métodos personalizados al usuario

He mencionado varias veces que nuestra clase User es nuestra clase...., por lo que somos libres de añadirle los métodos que queramos. Por ejemplo, imagina que necesitamos obtener la URL del avatar del usuario en algunos lugares de nuestro sitio... y no queremos repetir esta larga cadena.

Copia esto y luego ve a abrir la clase User: src/Entity/User.php. En la parte inferior, crea un nuevo public function getAvatarUri(). Dale un argumento int $size que por defecto sea 32... y un tipo de retorno string:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
... lines 158 - 162
}
}

Pega la URL como ejemplo. Devolvamos la primera parte de eso... añade un ? -que se me acaba de olvidar- y luego usa http_build_query():

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
... lines 159 - 161
]);
}
}

Pásale un array... con el primer parámetro de consulta que necesitamos: name ajustado a$this->getFirstName().

Pero podemos ser aún más inteligentes. Si te desplazas hacia arriba, la propiedad firstName puede ser null:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 31
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
... lines 36 - 163
}

Es algo opcional que el usuario puede proporcionar. Así que, de vuelta al método, utiliza getFirstName() si tiene un valor... si no, vuelve al correo electrónico del usuario. Para size, que es el segundo parámetro de consulta, establécelo en $size... y también necesitamos establecer background en random para que las imágenes sean más divertidas:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getFirstName() ?: $this->getEmail(),
'size' => $size,
'background' => 'random',
]);
}
}

Gracias a este pequeño y bonito método, en base.html.twig podemos sustituir todo esto por app.user.avatarUri:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
src="{{ app.user.avatarUri }}"
... line 50
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

También podemos decir getAvatarUri(): ambos harán lo mismo.

Si lo probamos... ¡imagen rota! Ryan: ve a añadir el ? que has olvidado, cabeza de chorlito.http_build_query añade el & entre los parámetros de consulta, pero seguimos necesitando el primer ?:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getFirstName() ?: $this->getEmail(),
... lines 160 - 161
]);
}
}

Ahora... ¡mucho mejor!

Pero podemos hacerlo aún mejor En base.html.twig, utilizamosapp.user.firstName:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
... line 49
alt="{{ app.user.firstName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Como acabamos de ver, esto puede estar vacío. Así que vamos a añadir un método de ayuda más aUser llamado getDisplayName(), que devolverá un string:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 164
public function getDisplayName(): string
{
... line 167
}
}

Robaré algo de la lógica de arriba... y devolverá eso:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 164
public function getDisplayName(): string
{
return $this->getFirstName() ?: $this->getEmail();
}
}

Así que devolvemos el nombre o el correo electrónico. Podemos usar esto engetAvatarUri() - getDisplayName():

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getDisplayName(),
... lines 160 - 161
]);
}
... lines 164 - 168
}

Y también en base.html.twig:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
... line 49
alt="{{ app.user.displayName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Cuando actualizamos... ¡sí! ¡Sigue funcionando!

Servicio de Seguridad: Obtención del usuario en un servicio

Bien: ahora hemos recuperado el objeto User desde un controlador a través de $this->getUser()... y en Twig a través de app.user. El único otro lugar donde necesitarás obtener el objetoUser es desde un servicio.

Por ejemplo, hace un par de tutoriales, creamos este servicio MarkdownHelper:

... lines 1 - 8
class MarkdownHelper
{
... lines 11 - 23
public function parse(string $source): string
{
if (stripos($source, 'cat') !== false) {
$this->logger->info('Meow!');
}
if ($this->isDebug) {
return $this->markdownParser->transformMarkdown($source);
}
return $this->cache->get('markdown_'.md5($source), function() use ($source) {
return $this->markdownParser->transformMarkdown($source);
});
}
}

Le pasamos markdown, lo convierte en HTML... y luego... aprovecha... o algo así. Imaginemos que necesitamos el objeto User dentro de este método: vamos a utilizarlo para registrar otro mensaje.

Si necesitas el objeto User actualmente autentificado de un servicio, puedes obtenerlo a través de otro servicio llamado Security. Añade un nuevo argumento de tipo Security -el de Symfony\Component - llamado $security. PulsaAlt + Enter y ve a "Inicializar propiedades" para crear esa propiedad y establecerla:

... lines 1 - 6
use Symfony\Component\Security\Core\Security;
... lines 8 - 9
class MarkdownHelper
{
... lines 12 - 17
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache, bool $isDebug, LoggerInterface $mdLogger, Security $security)
{
... lines 20 - 23
$this->security = $security;
}
... lines 26 - 46
}

Como estoy usando PHP 7.4, esto añadió un tipo a mi propiedad.

A continuación, vamos a registrar un mensaje si el usuario está conectado. Para ello, di si $this->security->getUser():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
if (stripos($source, 'cat') !== false) {
$this->logger->info('Meow!');
}
if ($this->security->getUser()) {
... lines 34 - 36
}
... lines 38 - 45
}
}

Realmente, esta es la forma de obtener el objeto User... pero también podemos usarlo para ver si el User está conectado porque esto devolverá null si no lo está. Una forma más "oficial" de hacer esto sería usar isGranted() - que es otro método de la clase Security - y comprobar IS_AUTHENTICATED_REMEMBERED:

class MarkdownHelper
{
    // ...
    public function parse(string $source): string
    {
        // ...
        if ($this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
            // ...
        }
        // ...
    }
}

De todos modos, dentro de digamos $this->logger->info() con:

Renderización de markdown para {usuario}

Pasar un array de contexto con user establecido en $this->security->getUser()->getEmail():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
... lines 29 - 32
if ($this->security->getUser()) {
$this->logger->info('Rendering markdown for {user}', [
'user' => $this->security->getUser()->getUserIdentifier()
]);
}
... lines 38 - 45
}
}

Como antes, sabemos que este será nuestro objeto User... pero nuestro editor sólo sabe que es algún UserInterface. Así que podríamos usar getEmail()... pero me quedo con getUserIdentifier():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
... lines 29 - 32
if ($this->security->getUser()) {
$this->logger->info('Rendering markdown for {user}', [
'user' => $this->security->getUser()->getUserIdentifier()
]);
}
... lines 38 - 45
}
}

¡Vamos a probarlo! Tenemos markdown en esta página... así que actualiza... y luego haz clic en cualquier enlace de la barra de herramientas de depuración web para saltar al perfilador. Ve a los registros y... ¡ya está! Hay un montón de registros porque llamamos a este método un montón de veces.

A continuación, vamos a hablar de una función súper útil llamada "jerarquía de roles". Esto te da el poder de asignar roles adicionales a cualquier usuario que tenga algún otro rol.

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
    }
}