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 SubscribeSabemos 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.
¿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_encode
para 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.
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!
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.
Hey Riya J.
Thanks for sharing your solution. Could you tell me what error were you getting?
Oh, by the way, you can run yarn dev
instead of npm run build
so it will compile your assets using the dev configuration
Cheers!
Yeah, it was the time when on clicking the Avatar image, the dropdown was not popping up!
And also no any other JS functionality were working. So, I performed the above mentioned steps and the issue resolved.
😀
// composer.json
{
"require": {
"php": ">=8.1",
"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
}
}
If your DropDown and Other Bootstrap Functionality doesn’t work, Do try this:
Inside <b>config > packages > twig.yaml</b> add
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
// start the Stimulus application
import './bootstrap';
// activates collapse functionality
import { Collapse } from 'bootstrap';
// Need jQuery?
import $ from 'jquery';
/**
*/
`
Then Execute following commands in your Terminal: