Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Aprovechando el propietario de la pregunta

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

Ahora que cada Question tiene un owner -un objeto User -, ¡es hora de celebrarlo! En el frontend, podemos empezar a renderizar datos reales... en lugar de tener siempre la misma foto del gato y la misma pregunta escrita por la misma Tisha. Ambas están codificadas, aunque nos encanta la gata Tisha aquí en SymfonyCasts.

Empieza en la página de inicio. Abre templates/question/homepage.html.twig. Y... aquí es donde hacemos un bucle con las preguntas. Primero, para el avatar, podemos utilizar el método de ayuda que hemos creado antes: {{ question.owner.avatarUri }}:

... lines 1 - 2
{% block body %}
... lines 4 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in pager %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
<div class="col-2 text-center">
<img src="{{ question.owner.avatarUri }}" width="100" height="100" alt="{{ question.owner.displayName}} avatar">
... lines 24 - 29
</div>
... lines 31 - 38
</div>
</div>
... lines 41 - 45
</div>
</div>
{% endfor %}
... lines 49 - 50
</div>
</div>
{% endblock %}
... lines 54 - 55

A continuación... hacia abajo, aquí es donde imprimimos el nombre del propietario de la pregunta. Vamos a utilizar question.owner.displayName:

... lines 1 - 2
{% block body %}
... lines 4 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in pager %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
... lines 22 - 30
<div class="col">
... line 32
<div class="q-display p-3">
... lines 34 - 35
<p class="pt-4"><strong>--{{ question.owner.displayName }}</strong></p>
</div>
</div>
</div>
</div>
... lines 41 - 45
</div>
</div>
{% endfor %}
... lines 49 - 50
</div>
</div>
{% endblock %}
... lines 54 - 55

100 puntos de experiencia por utilizar dos métodos personalizados seguidos.

Y ahora... ¡nuestra página empieza a parecer real! Haz clic en una pregunta. Hagamos lo mismo para la página del programa. Abre esa plantilla: show.html.twig.

Para el avatar, utiliza question.owner.avatarUri:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
<div class="col-2 text-center">
<img src="{{ question.owner.avatarUri }}" width="100" height="100" alt="{{ question.owner.displayName}} avatar">
... lines 15 - 32
</div>
... lines 34 - 41
</div>
</div>
</div>
</div>
</div>
... lines 47 - 59
</div>
{% endblock %}

Luego... aquí abajo, para el nombre, {{ question.owner.displayName }}:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 33
<div class="col">
<h1 class="q-title-show">{{ question.name }}</h1>
<div class="q-display p-3">
... lines 37 - 38
<p class="pt-4"><strong>--{{ question.owner.displayName }}</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
... lines 47 - 59
</div>
{% endblock %}

Ah, y se me ha olvidado hacer una cosa. Copia eso, vuelve a subir al avatar... para que también podamos actualizar el atributo alt:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
<div class="col-2 text-center">
<img src="{{ question.owner.avatarUri }}" width="100" height="100" alt="{{ question.owner.displayName}} avatar">
... lines 15 - 32
</div>
... lines 34 - 41
</div>
</div>
</div>
</div>
</div>
... lines 47 - 59
</div>
{% endblock %}

También tengo que hacerlo en la página de inicio... aquí está:

... lines 1 - 2
{% block body %}
... lines 4 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in pager %}
<div class="col-12 mb-3">
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container p-4">
<div class="row">
<div class="col-2 text-center">
<img src="{{ question.owner.avatarUri }}" width="100" height="100" alt="{{ question.owner.displayName}} avatar">
... lines 24 - 29
</div>
... lines 31 - 38
</div>
</div>
... lines 41 - 45
</div>
</div>
{% endfor %}
... lines 49 - 50
</div>
</div>
{% endblock %}
... lines 54 - 55

¡Probemos esto! Actualiza la página y... ¡somos dinámicos!

Crear la página de edición de preguntas

En un sitio real, probablemente vamos a necesitar una página en la que el propietario de esta pregunta pueda editar sus detalles. No vamos a construir esto hasta el final -no quiero sumergirme en el sistema de formularios- pero vamos a ponerlo en marcha. Y esto nos va a llevar a una situación de seguridad realmente interesante.

En src/Controller/QuestionController.php... encuentra la acción show(). Vamos a hacer trampa copiando esto y pegándolo. Cambia la URL a /questions/edit/{slug}, modifica el nombre de la ruta y actualiza el nombre del método. Dentro, sólo hay que renderizar una plantilla: question/edit.html.twig:

... lines 1 - 18
class QuestionController extends AbstractController
{
... lines 21 - 69
/**
* @Route("/questions/edit/{slug}", name="app_question_edit")
*/
public function edit(Question $question)
{
return $this->render('question/edit.html.twig', [
'question' => $question,
]);
}
... lines 79 - 98
}

¡Genial! En templates/question/, crea esto: edit.html.twig.

Pondré una plantilla básica:

{% extends 'base.html.twig' %}
{% block title %}Edit Question: {{ question.name }}{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="my-4">Edit Question</h1>
<blockquote>{{ question.question }}</blockquote>
TODO
</div>
</div>
</div>
{% endblock %}

Aquí no hay nada especial, excepto que estoy imprimiendo el texto de la pregunta dinámica. En realidad no hay un formulario... ya que nos estamos centrando en la seguridad... pero haz como si lo hubiera.

Enlace a la página de edición

Antes de probar esta página, vuelve a la plantilla de presentación de preguntas. Vamos a añadir un enlace de edición para ayudar al propietario. En realidad, busca el h1. Aquí vamos.

Envuelve esto en un div con class="d-flex justify-content-between"... y luego ciérralo y haz una sangría:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 33
<div class="col">
<div class="d-flex justify-content-between">
<h1 class="q-title-show">{{ question.name }}</h1>
... lines 37 - 40
</div>
... lines 42 - 46
</div>
</div>
</div>
</div>
</div>
</div>
... lines 53 - 65
</div>
{% endblock %}

Ahora añade un enlace con href= path('app_question_edit') . Y, por supuesto, tenemos que pasarle a esto el comodín: id ajustado a question.id. Oh... espera, en realidad, el comodín es slug:

... lines 1 - 18
class QuestionController extends AbstractController
{
... lines 21 - 69
/**
* @Route("/questions/edit/{slug}", name="app_question_edit")
*/
public function edit(Question $question)
{
... lines 75 - 77
}
... lines 79 - 98
}

Así que usa slug ajustado a question.slug:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 33
<div class="col">
<div class="d-flex justify-content-between">
<h1 class="q-title-show">{{ question.name }}</h1>
<a href="{{ path('app_question_edit', {
slug: question.slug
}) }}" class="btn btn-secondary btn-sm mb-2">Edit</a>
</div>
... lines 42 - 46
</div>
</div>
</div>
</div>
</div>
</div>
... lines 53 - 65
</div>
{% endblock %}

Genial. Después, di "Editar"... y dale a esto unas cuantas clases para que quede más bonito.

Gracias a esto... ¡tenemos un botón de edición! Oh, ¡pero necesitamos un poco de margen! Añade mb-2:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 33
<div class="col">
<div class="d-flex justify-content-between">
... lines 36 - 37
<a href="{{ path('app_question_edit', {
slug: question.slug
}) }}" class="btn btn-secondary btn-sm mb-2">Edit</a>
</div>
... lines 42 - 46
</div>
</div>
</div>
</div>
</div>
</div>
... lines 53 - 65
</div>
{% endblock %}

y... mucho mejor. Haz clic en eso. Esta es la página de edición de la pregunta... que en realidad no es una página de edición... pero finge que lo es.

Ahora volvamos al tema de la seguridad. Porque... no podemos dejar que cualquiera acceda a esta página: sólo el propietario de esta pregunta debe poder editarla.

Así que dentro de QuestionController, necesitamos una comprobación de seguridad. Primero tenemos que asegurarnos de que el usuario está conectado. Hazlo con $this->denyAccessUnlessGranted()pasando por IS_AUTHENTICATED_REMEMBERED:

... lines 1 - 18
class QuestionController extends AbstractController
{
... lines 21 - 72
public function edit(Question $question)
{
$this->denyAccessUnlessGranted('ROLE_USER');
... lines 76 - 82
}
... lines 84 - 103
}

Gracias a esto, tenemos garantizado que obtendremos un objeto User si decimos $this->getUser(). Podemos utilizarlo: si $question->getOwner() no es igual a $this->getUser(), entonces alguien que no es el propietario está intentando acceder a esta página. Niega el acceso con throw $this->createAccessDeniedException(). Diré:

¡No eres el propietario!

Pero, recuerda, estos mensajes de error sólo se muestran a los desarrolladores:

... lines 1 - 18
class QuestionController extends AbstractController
{
... lines 21 - 72
public function edit(Question $question)
{
$this->denyAccessUnlessGranted('ROLE_USER');
if ($question->getOwner() !== $this->getUser()) {
throw $this->createAccessDeniedException('You are not the owner!');
}
... lines 79 - 82
}
... lines 84 - 103
}

Vale, pues ahora mismo no estoy conectado en absoluto. Así que si refrescamos, nos devuelve a la página de inicio de sesión. Así que... ¡bien! ¡Acabamos de evitar con éxito que cualquier persona que no sea el propietario acceda a esta página de edición!

Pero... malas noticias amigos: No me gusta esta solución. No me gusta poner ninguna lógica de seguridad manual dentro de mi controlador. ¿Por qué? Porque significa que vamos a tener que repetir esa lógica en Twig para ocultar o mostrar el botón de edición. ¿Y qué pasa si nuestra lógica se vuelve más compleja? ¿Qué pasa si puedes editar una pregunta si eres el propietario o si tienes ROLE_ADMIN? Ahora tendríamos que actualizar y mantener la lógica duplicada en dos lugares como mínimo. No, no queremos duplicar nuestras reglas de seguridad.

Así que a continuación vamos a aprender sobre el sistema de votantes, que es la clave para centralizar toda esta lógica de autorización de una forma bonita.

Leave a comment!

2
Login or Register to join the conversation
gazzatav Avatar
gazzatav Avatar gazzatav | posted hace 1 año | edited

Gary Taverner • 6 minutes ago
Hi
At the end of this chapter I found the behaviour when I try to edit a question different from the video. If I'm not authenticated I go to the symfony local server's EOF page (traffic lights, red stripe on blue background # EOF). Searching for 'EOF' in symfony docs gets exactly 0 matches for EOF (which surprised me, and no, 'effective' is not what I was searching for).

Unauthenticated - my question -> # EOF
Authenticated - my question -> app_question_edit
Unauthenticated - not my question -> # EOF
Authenticated - not my question -> 403 Access Denied

The redirection to the login page works if I go to /api/me while unauthenticated.

In the logs I can see that three authenticators are tried and then I'm referred to the entry point which is form_login:

<blockquote>[Application] Aug 6 15:51:17 |DEBUG | SECURI Checking for authenticator support. authenticators=3 firewall_name="main"
[Application] Aug 6 15:51:17 |DEBUG | SECURI Checking support on authenticator. authenticator="App\Security\DummyAuthenticator"
[Application] Aug 6 15:51:17 |DEBUG | SECURI Authenticator does not support the request.
[Application] Aug 6 15:51:17 |DEBUG | SECURI Checking support on authenticator. authenticator="Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator"
[Application] Aug 6 15:51:17 |DEBUG | SECURI Authenticator does not support the request.
[Application] Aug 6 15:51:17 |DEBUG | SECURI Checking support on authenticator. authenticator="Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator"
[Application] Aug 6 15:51:17 |DEBUG | SECURI Authenticator does not support the request.
[Application] Aug 6 15:51:17 |DEBUG | DOCTRI SELECT t0.id AS id_1, t0.title AS title_2, t0.slug AS slug_3, t0.question AS question_4, t0.asked_at AS asked_at_5, t0.votes AS votes_6, t0.created_at AS created_at_7, t0.updated_at AS updated_at_8, t0.owner_id AS owner_id_9 FROM question t0 WHERE t0.slug = ? LIMIT 1 0="et-sunt-ut-numquam-sit"
[Application] Aug 6 15:51:17 |DEBUG | SECURI Access denied, the user is not fully authenticated; redirecting to authentication entry point.
[Application] Aug 6 15:51:17 |DEBUG | SECURI Calling Authentication entry point.
[Web Server ] Aug 6 15:51:18 |ERROR | SERVER GET (502) /questions/edit/et-sunt-ut-numquam-sit ip="::1"
</blockquote>

Under form_login in security.yaml I have:


login_path: app_login
check_path: app_login
username_parameter: email
password_parameter: password
csrf_parameter: _csrf_token
csrf_token_id: authenticate
enable_csrf: true

My form_login leads to app_login so I'm mystified as to why this fails. In the address bar I can see the path /questions/edit/et-sunt-ut-numquam-sit. The end of the query looks odd though 't0.slug = ? LIMIT 1 0="et-sunt-ut-numquam-sit"'.

Any ideas?

Reply

Hey @gazzatav!

Ah, I know that "EOF" page that you're talking about, and it's super frustrating. When this happens, basically, something inside your app/PHP is exploding in a horrible way. This is... almost the web server's "segfault": something went so wrong that it can't even show it. Often you just need to stop the server and restart it. But it's also very possible that there is some sort of bug/bad behavior in the code... but unfortunately, it's so "bad" (e.g. infinite recursion) that it can't be shown.. which makes it pretty tough to debug.

Looking at the situation above, it seems like the "common thread" is that if you go to ANY question (your question or another question) while not authenticated, you get the EOF. We need to figure out why. I would temporarily, GREATLY reduce the code on the question page - heck even put a "return new Response('foo')" in that controller to see if you can get it to load. Assuming that works, slowly re-add code to your page until you trigger that EOF. Hopefully that'll help you dig down into the problem. If there's a better way to debug the EOF, I'm not aware of it. Also, if you don't have XDebug installed, try that: that can help handle things like infinite recursion better.

Let me know what you find out :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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

What PHP libraries does this tutorial use?

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