Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

El objeto de petición

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

Nuevo equipo objetivo: permitir que los usuarios voten a favor y en contra de una mezcla. Para conseguirlo, en la entidad VinylMix, cuando un usuario vota, necesitamos enviar una consulta UPDATE para cambiar la propiedad entera $votes en la base de datos.

Añadir un formulario sencillo

Centrémonos primero en la interfaz de usuario. Abre templates/mix/show.html.twig. Para empezar, imprime {{ mix.votesString }} votes para que podamos verlo aquí.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 11
<div class="col-12 col-md-8 ps-5">
... lines 13 - 15
{{ mix.votesString }} votes
... lines 17 - 22
</div>
... line 24
</div>
{% endblock %}

Y... ¡perfecto! Para añadir la funcionalidad de upvote y downvote, podríamos utilizar algún JavaScript sofisticado. Pero vamos a hacerlo sencillo añadiendo un botón que publique un formulario. En realidad, esto será más elegante de lo que parece. En el primer tutorial, instalamos la biblioteca Turbo JavaScript. Así que aunque usaremos una etiqueta y un botón normales de<form>, Turbo lo enviará automáticamente vía AJAX para una experiencia fluida.

Por cierto, Symfony tiene un componente de formulario y hablaremos de él en un futuro tutorial. Pero este formulario va a ser tan sencillo que realmente no lo necesitamos. Añade una bonita y aburrida etiqueta <form> con action establecida en la funciónpath().

El formulario se enviará a un nuevo controlador que... ¡todavía tenemos que crear!

Dirígete a MixController y añade un nuevo public function llamado vote(). Dale el atributo #[Route()] con la URL /mix/{id}/vote. Y como tenemos que enlazar con esto, añade un nombre: app_mix_vote.

... lines 1 - 11
class MixController extends AbstractController
{
... lines 14 - 42
#[Route('/mix/{id}/vote', name: 'app_mix_vote', methods: ['POST'])]
public function vote(VinylMix $mix): Response
... lines 45 - 47
}

El comodín de la ruta {id} contendrá el id del VinylMix específico que el usuario está votando. Para consultarlo, utiliza el truco que aprendimos antes: añade un argumento de tipo VinylMix y llámalo $mix. Ah, y aunque no es necesario, añadiré el tipo de retorno Response. Añadir esto es sólo una buena práctica.

Dentro, para asegurarnos de que las cosas funcionan, dd($mix).

... lines 1 - 43
public function vote(VinylMix $mix): Response
{
dd($mix);
}
... lines 48 - 49

¡Genial! Copia el nombre de la ruta, vuelve a la plantilla - show.html.twig - y dentro de path(), pega. Y como esta ruta tiene un comodín {id}, pasaid a mix.id. También dale al formulario method="POST"... porque siempre que el envío de un formulario cambie los datos en tu servidor, debe enviarse con POST.

... lines 1 - 4
{% block body %}
... lines 6 - 11
<div class="col-12 col-md-8 ps-5">
... lines 13 - 16
<form action="{{ path('app_mix_vote', {id: mix.id }) }}" method="POST">
... lines 18 - 21
</form>
</div>
... lines 24 - 25
{% endblock %}

Incluso podemos imponer este requisito en nuestra ruta añadiendomethods: ['POST']. Eso es opcional, pero ahora, si alguien, por alguna razón, va directamente a esta URL, que es una petición GET, no coincidirá con la ruta. ¡Muy útil!

... lines 1 - 11
class MixController extends AbstractController
{
... lines 14 - 42
#[Route('/mix/{id}/vote', name: 'app_mix_vote', methods: ['POST'])]
public function vote(VinylMix $mix): Response
... lines 45 - 47
}

Vuelve al formulario. Este formulario... será algo extraño. En lugar de tener campos en los que el usuario pueda escribir, sólo necesitamos un botón. Añade <button> contype="submit"... y luego algunas clases para el estilo. Para el texto, utiliza un icono de Font Awesome: un <span> con class="fa fa-thumbs-up".

... lines 1 - 4
{% block body %}
... lines 6 - 16
<form action="{{ path('app_mix_vote', {id: mix.id }) }}" method="POST">
<button
type="submit"
class="btn btn-outline-primary"
><span class="fa fa-thumbs-up"></span></button>
</form>
... lines 23 - 25
{% endblock %}

¡Perfecto! Vamos a comprobarlo. Actualiza y... ¡pulgares arriba! Y cuando hagamos clic en él... ¡hermoso! ¡Llega a la ruta! Fíjate en que la URL no ha cambiado... eso es porque Turbo ha enviado el formulario vía Ajax... y luego nuestro dd() lo ha detenido todo.

Bien, en un minuto, vamos a añadir otro botón con el pulgar hacia abajo. Así que, de alguna manera, en nuestro controlador, vamos a tener que averiguar qué botón, el de arriba o el de abajo, se acaba de pulsar.

Para ello, en el botón, añade name="direction" y value="up". Ahora, si pulsamos este botón, se enviará un dato POST llamado direction con el valor up... casi como si el usuario escribiera la palabra up en un campo de texto.

... lines 1 - 16
<form action="{{ path('app_mix_vote', {id: mix.id }) }}" method="POST">
<button
... lines 19 - 20
name="direction"
value="up"
><span class="fa fa-thumbs-up"></span></button>
</form>
... lines 25 - 29

Obtención del DAta de petición

Bien... ¿pero cómo leemos los datos POST en Symfony? Siempre que necesites leer algo de la petición -como datos POST, parámetros de consulta, archivos subidos o cabeceras- necesitarás el objeto Request de Symfony. Y hay dos formas de obtenerlo.

La primera es autocableando un servicio llamado RequestStack. Entonces puedes obtener la petición actual diciendo $requestStack->getCurrentRequest().

Esto funciona en cualquier lugar donde puedas autocablear un servicio. Pero en un controlador, hay una forma más fácil. Deshaz eso... y en su lugar, añade un argumento que se indique conRequest. Consigue el de HttpFoundation de Symfony. Llamémoslo $request.

... lines 1 - 8
use Symfony\Component\HttpFoundation\Request;
... lines 10 - 12
class MixController extends AbstractController
{
... lines 15 - 44
public function vote(VinylMix $mix, Request $request): Response
{
... line 47
}
}

Al principio, esto parece un autocable, ¿no? Parece que Request es un servicio y lo estamos autocableando como argumento. Pero... ¡sorpresa! Request no es un servicio. No, es otra "cosa" que puedes tener como argumento para tu controlador.

Repasemos. Ahora conocemos cuatro tipos diferentes de argumentos que puedes tener en un método del controlador. Uno: puedes tener comodines de ruta como $id. Dos: Puedes autoconectar servicios. Tres: Puedes tener entidades con sugerencias de tipo. Y cuatro: Puedes teclear la clase Request. Sí, el objeto Request es tan importante que Symfony ha creado un caso especial sólo para él.

Y... es bastante bonito. Todo nuestro trabajo como desarrolladores es "leer la petición entrante" y utilizarla para "crear una respuesta". Así que es... casi poético que podamos tener un método que tome el Request como argumento y devuelva un Response. Entrada Request, salida Response.

Obtención de datos POST

Pero estoy divagando. Hay un montón de métodos y propiedades diferentes en la petición para obtener lo que necesites. Para leer datos POST, di $request->request->get() y luego el nombre del campo. En este caso, direction.

... lines 1 - 44
public function vote(VinylMix $mix, Request $request): Response
{
dd($request->request->get('direction'));
}
... lines 49 - 50

No vamos a hablar mucho del objeto Request... porque es... un simple objeto que contiene datos. Si necesitas leer algo de él, sólo tienes que mirar la documentación y te dirá cómo hacerlo.

Muy bien, vuelve aquí, refresca la página... sube la nota y... ¡ya está! Bien, quita el dd() y ponlo en una variable de dirección con $direction =.

Si, por alguna razón, faltan los datos de direction POST (esto no debería ocurrir a no ser que alguien esté trasteando con nuestro sitio), ponlo por defecto en up.

... lines 1 - 44
public function vote(VinylMix $mix, Request $request): Response
{
$direction = $request->request->get('direction', 'up');
}
... lines 49 - 50

Ahora vamos a añadir el voto negativo. Copia todo el botón... pégalo... cambia el valor a down y actualiza la clase del icono a fa fa-thumbs-down.

... lines 1 - 4
{% block body %}
... lines 6 - 16
<form action="{{ path('app_mix_vote', {id: mix.id }) }}" method="POST">
... lines 18 - 23
<button
type="submit"
class="btn btn-outline-primary"
name="direction"
value="down"
><span class="fa fa-thumbs-down"></span></button>
</form>
... lines 31 - 33
{% endblock %}

Bien, ya sabemos que el valor será o bien up o bien down. En nuestro controlador, usemos esto. if ($direction === 'up'), entonces$mix->setVotes($mix->getVotes() + 1). Si no, haz lo mismo... excepto que será - 1. Abajo, dd($mix).

... lines 1 - 12
class MixController extends AbstractController
{
... lines 15 - 44
public function vote(VinylMix $mix, Request $request): Response
{
$direction = $request->request->get('direction', 'up');
if ($direction === 'up') {
$mix->setVotes($mix->getVotes() + 1);
} else {
$mix->setVotes($mix->getVotes() - 1);
}
dd($mix);
}
}

En un sitio real, probablemente también almacenaremos qué usuario está votando para que no pueda votar una y otra vez. Aprenderemos a hacerlo en un futuro tutorial, pero esto funcionará bien por ahora.

Muy bien, vuelve a actualizar. Tenemos 49 votos. Si hacemos clic en el botón de "upvote"... ¡50! Si refrescamos y hacemos clic en el botón de "downvote"... ¡48!

¡Sí! Pero todavía no hemos guardado este valor en la base de datos. Cuando actualizamos, siempre vuelve al "49" original.

Así que... a continuación, ¡vamos a hacerlo! Haremos una consulta UPDATE a la base de datos y también terminaremos la ruta redirigiendo a otra página.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}