
Parámetros de la petición Symfony, OptionsResolver/MapQueryParameter
Las siguientes novedades de las que quiero hablar están relacionadas con la obtención de datos de la petición. Normalmente es un trabajo un poco aburrido.
Cuando desarrollamos una api o aplicación es muy común tener un grupo de parámetros para filtrar los resultados, los cuales son enviados mediante una petición get: ejemplo http://domain.com/?page=1&query=destacados.
Esto parámetros necesitan ser obtenidos y validados luego en nuestro controlador, en este articulo mostrare algunas vías para realizar fácilmente esa obtención y validación aplicando las ultimas versiones y novedades de symfony.
1. Vía común:
Obtenemos los parámetros directamente dentro del controlador y manejamos las validaciones
Cuando desarrollamos una api o aplicación es muy común tener un grupo de parámetros para filtrar los resultados, los cuales son enviados mediante una petición get: ejemplo http://domain.com/?page=1&query=destacados.
Esto parámetros necesitan ser obtenidos y validados luego en nuestro controlador, en este articulo mostrare algunas vías para realizar fácilmente esa obtención y validación aplicando las ultimas versiones y novedades de symfony.
1. Vía común:
Obtenemos los parámetros directamente dentro del controlador y manejamos las validaciones
# Vía mas usada, pero se complica un poco cuando manejas múltiples filtros public function posts(Request $request){ $page = $request->query->get('page', 1); $query = $request->query->get('query', ""); # validar los datos y procesar }
2. Usando OptionsResolver:
Mediante esta vía definimos nuestro objeto, con nuestras reglas de validación, permitiendo realizar normalizar los parámetros obtenidos.
Mediante esta vía definimos nuestro objeto, con nuestras reglas de validación, permitiendo realizar normalizar los parámetros obtenidos.
namespace App\OptionsResolver; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class PaginatorOptionsResolver extends OptionsResolver { public function configurePage(): self { return $this ->setDefined(["page", "query"]) ->setDefault("page", 1) ->setDefault("query", "") ->setAllowedTypes("page", "numeric") ->setAllowedTypes("query", "string") ->setAllowedValues("page", function ($page) { $validatedValue = filter_var($page, FILTER_VALIDATE_INT, [ 'flags' => FILTER_NULL_ON_FAILURE, ]); if(null === $validatedValue || $validatedValue < 1) { return false; } return true; }) ->setNormalizer("page", fn (Options $options, $page) => (int) $page); } }
## Usamos el PaginatorOptionsResolver en nuestro Controllador #[Route(path: '/blog', name: 'app_blog')] public function blog(Request $request, PaginatorOptionsResolver $optionsResolver): Response { $queryParams = $optionsResolver ->configurePage() ->resolve($request->query->all()); dump($queryParams["page"], $queryParams["query"]); }
Si pasamos el page como un string, automáticamente se retorna el error de parámetros.
3. Usar el atributo recién agregado en las ultimas versiones de symfony MapQueryParameter
public function homepage(#[MapQueryParameter] string $query = ''): Response { dump($query); }
Este atributo también tiene algunas opciones. Por ejemplo, si tu parámetro de consulta se llama de forma diferente a tu argumento, podrías ponerlo aquí.
Y más allá de coger el valor de la petición, este sistema también realiza la validación. Observa: duplica esto y añade un argumento int $page = 1. Ah, y quería que el argumento $query fuera opcional para que no tenga que estar en la URL. A continuación, vuelca $page.
public function homepage(#[MapQueryParameter] string $query = '', #[MapQueryParameter] int $page = 1): Response { dump($query, $page); }
Vale, si añadimos ?page=3 a la URL... no hay sorpresa: vuelca 3. Pero está bien que obtengamos un entero 3: no una cadena. Ahora prueba con page=banana. Un 404! El sistema ve que tenemos un tipo int y realiza la validación.
3.1 Validaciones filter_var()
Todo este sistema es manejado por algo llamado QueryParameterValueResolver. Así que si realmente quieres profundizar, comprueba esa clase. Internamente, utiliza una función PHP llamada filter_var() para realizar la validación. Le pasas un valor, uno o varios filtros... y te dice si ese valor satisface esos filtros. También puedes pasarle opciones para controlar los filtros.
Añade un argumento llamado $limit que por defecto sea 10. Vuelca esto abajo. Pero quiero que el límite esté entre 1 y 10. Para forzarlo, pasa dos opciones especiales a filter_var: min_range puesto a 1 y max_range puesto a 10.
3.1 Validaciones filter_var()
Todo este sistema es manejado por algo llamado QueryParameterValueResolver. Así que si realmente quieres profundizar, comprueba esa clase. Internamente, utiliza una función PHP llamada filter_var() para realizar la validación. Le pasas un valor, uno o varios filtros... y te dice si ese valor satisface esos filtros. También puedes pasarle opciones para controlar los filtros.
Añade un argumento llamado $limit que por defecto sea 10. Vuelca esto abajo. Pero quiero que el límite esté entre 1 y 10. Para forzarlo, pasa dos opciones especiales a filter_var: min_range puesto a 1 y max_range puesto a 10.
public function homepage( #[MapQueryParameter] string $query = '', #[MapQueryParameter] int $page = 1, #[MapQueryParameter(options: ['min_range' => 1, 'max_range' => 10])] int $limit = 10, ): Response { dump($query, $page, $limit); }
¡Vamos a probarlo! Digamos ?limit=3. Funciona como esperamos. Pero cuando probamos limit=13. filter_var() falla y ¡obtenemos un 404!
Esto se puede utilizar incluso para manejar arreglos. Copia y crea un argumento más: una matriz de $filters que por defecto sea una matriz vacía. Vuelca eso.
public function homepage( #[MapQueryParameter] string $query = '', #[MapQueryParameter] int $page = 1, #[MapQueryParameter(options: ['min_range' => 1, 'max_range' => 10])] int $limit = 10, #[MapQueryParameter] array $filters = [], ): Response { dump($query, $page, $limit, $filters); }
En el navegador, añade ?filters[] igual a plátano, &filters[] igual a manzana. ¡Comprueba ese array en la barra de herramientas de depuración web! También funciona para arreglos asociativos: añade foo y bar entre []. ¡Sí! Un arreglo asociativo.
Es una función muy bien diseñada para obtener parámetros de consulta.
3.2 Contenido
Además, si necesitas obtener el cuerpo de una petición, en Symfony 6.3 hay un nuevo método llamado $request->getPayload().
¿Construyendo una API? Cuando tu cliente envíe JSON en el cuerpo, utiliza $request->getPayload() para descodificarlo en un arreglo asociativo.
¡Eso está muy bien! Pero además, si tu usuario envía un formulario HTML normal, $request->getPayload() también funciona en ese caso. Detecta que se está enviando un formulario HTML y descodifica los datos de $_POST en un arreglo. Así que no importa si estás utilizando una API o un formulario normal, tenemos un método uniforme para obtener la carga útil de la petición.
3.3 MapRequestPayload
3.2 Contenido
Además, si necesitas obtener el cuerpo de una petición, en Symfony 6.3 hay un nuevo método llamado $request->getPayload().
¿Construyendo una API? Cuando tu cliente envíe JSON en el cuerpo, utiliza $request->getPayload() para descodificarlo en un arreglo asociativo.
¡Eso está muy bien! Pero además, si tu usuario envía un formulario HTML normal, $request->getPayload() también funciona en ese caso. Detecta que se está enviando un formulario HTML y descodifica los datos de $_POST en un arreglo. Así que no importa si estás utilizando una API o un formulario normal, tenemos un método uniforme para obtener la carga útil de la petición.
3.3 MapRequestPayload
Hablando de JSON, también es habitual utilizar el serializador para deserializar la carga útil en un objeto. Esto está relacionado con otra nueva función llamada#[MapRequestPayload].
En este caso, __invoke es la acción del controlador. Esto dice: toma el JSON de la petición y deserialízalo en un ProductReviewDto, que es la clase de ejemplo anterior. Después de enviar el JSON a través del serializador, incluso realiza la validación. Así que otra característica bien pensada.
public function __invoke( #[MapRequestPayload] ProductReviewDto $productReview, ): Response { dump($productReview) }
3.4 Para mas información:
New in Symfony 6.3: Query Parameters Mapper (Symfony Blog)
New in Symfony 6.3: Mapping Request Data to Typed Objects (Symfony Blog)
New in Symfony 6.3: Query Parameters Mapper (Symfony Blog)
New in Symfony 6.3: Mapping Request Data to Typed Objects (Symfony Blog)
Conoces otras formas? Comenta!!
Deja un comentario: