Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Rutas inteligentes: Sólo GET y Validar {Comodines}

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Ahora que tenemos una nueva página, en tu terminal, ejecuta de nuevo debug:router.

php bin/console debug:router

Sí, ¡ahí está nuestra nueva ruta! Observa que la tabla tiene una columna llamada "Método" que dice "cualquiera". Esto significa que puedes hacer una petición a esta URL utilizando cualquier método HTTP -como GET o POST- y coincidirá con esa ruta.

Restringir las rutas sólo a GET o POST

Pero el objetivo de nuestra nueva ruta API es permitir a los usuarios hacer una petición GET para obtener datos de la canción. Técnicamente, ahora mismo, también podrías hacer una petición POST a esto... y funcionaría perfectamente. Puede que no nos importe, pero a menudo con las APIs, querrás restringir una ruta para que sólo funcione con un método específico como GET, POST o PUT. ¿Podemos hacer que esta ruta, de alguna manera, sólo funcione con peticiones GET?

Sí! Añadiendo otra opción a la Route. En este caso, se llama methods, ¡incluso se autocompleta! Establece esto como un array y, pon GET.

<?php
... lines 3 - 9
class SongController extends AbstractController
{
#[Route('/api/songs/{id}', methods: ['GET'])]
public function getSong($id): Response
{
... lines 15 - 22
}
}

Voy a mantener pulsado Comando y a hacer clic en la clase Route de nuevo... para que podamos ver que... ¡sí! methods es uno de los argumentos.

Volvemos a debug:router:

php bin/console debug:router

Bien. La ruta ahora sólo coincidirá con las peticiones GET. Es... un poco difícil probar esto, ya que un navegador siempre hace peticiones GET si vas directamente a una URL... pero aquí es donde otro comando de bin/console resulta útil: router:match.

Si lo ejecutamos sin argumentos

php bin/console router:match

Nos da un error, ¡pero muestra cómo se utiliza! Inténtalo:

php bin/console router:match /api/songs/11

Y... ¡eso coincide con nuestra nueva ruta! Pero ahora pregúntate qué pasaría si hiciéramos una petición POST a esa URL con --method=POST:

php bin/console router:match /api/songs/11 --method=POST

¡Ninguna ruta coincide con esta ruta con ese método! Pero dice que casi coincide con nuestra ruta.

Restringir los comodines de ruta mediante Regex

Vamos a hacer una cosa más para restringir nuestra nueva ruta. Voy a añadir una pista de tipo int al argumento $id.

<?php
... lines 3 - 9
class SongController extends AbstractController
{
#[Route('/api/songs/{id}', methods: ['GET'])]
public function getSong(int $id): Response
{
... lines 15 - 22
}
}

Eso... no cambia nada, excepto que ahora PHP tomará la cadena id de la URL que Symfony pasa a este método y la convertirá en un int, lo cual es... agradable porque entonces estamos tratando con un verdadero número entero en nuestro código.

Puedes ver la sutil diferencia en la respuesta. Ahora mismo, el campo id es una cadena. Cuando actualizamos, id es ahora un número verdadero en JSON.

Pero... si alguien se hiciera el remolón... y pasara a /api/songs/apple... ¡vaya! ¡Un error PHP, que, en producción, sería una página de error 500! Eso no me gusta.

Pero... ¿qué podemos hacer? El error se produce cuando Symfony intenta llamar a nuestro controlador y le pasa ese argumento. Así que no podemos poner código en el controlador para comprobar si $id es un número: ¡es demasiado tarde!

¿Y si, en cambio, pudiéramos decirle a Symfony que esta ruta sólo debe coincidir si el comodín id es un número? ¿Es posible? Totalmente

Por defecto, cuando tienes un comodín, coincide con cualquier cosa. Pero puedes cambiarlo para que coincida con una expresión regular personalizada. Dentro de las llaves, justo después del nombre, añade un <, luego > y, entre medias, \d+. Es una expresión regular que significa "un dígito de cualquier longitud".

<?php
... lines 3 - 9
class SongController extends AbstractController
{
#[Route('/api/songs/{id<\d+>}', methods: ['GET'])]
public function getSong(int $id): Response
{
... lines 15 - 22
}
}

¡Pruébalo! Actualiza y... ¡sí! A 404. No se ha encontrado ninguna ruta: simplemente no ha coincidido con esta ruta. Un 404 está muy bien... pero un error 500... eso es algo que queremos evitar. Y si volvemos a /api/songs/5... eso sigue funcionando.

A continuación: si me preguntaras cuál es la parte más central e importante de Symfony, no lo dudaría: son los servicios. Descubramos qué es un servicio y cómo es la clave para liberar el potencial de Symfony.

Leave a comment!

2
Login or Register to join the conversation

Is it possible to show a standard custom error page for these types of errors? Like when someone requests api/songs/a, that I can use the Smart route like shown in the video while explaining what is wrong with the request with a special page instead of showing a simple 404? If not directly, then can I somehow pass arguments to my 404 page for prod?

Reply

Hey @Brentspine ,

Yes, you can customize error pages in Symfony by overriding default templates :) Take a look at this docs page: https://symfony.com/doc/current/controller/error_pages.html

Well, you can't pass vars to that template directly, but you can create a custom Twig function for example that will return you some data you need and call it inside that template :)

Cheers!

1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    },
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3
    }
}
userVoice