knp_paginator con consulta SQL personalizada (createNativeQuery)

El Bundle KnpPaginator es una excelente ayuda que nos permite paginar de una forma bastante amigable y facil, normalmente su uso se refiera a pasarle una query que formamos mediante createQuery y este la procesa y nos entrega el resultado paginado segun los parametros que definamos pero… ¿Que sucede cuando deseamos pasarle una consulta personalizada? la respuesta es tajante, no lo soporta, pero nos entrega herramientas para hacerlo.

En su documentación hace referencia a Creating custom subscriber, esto especificamente nos permite personalizar la funcion que recibe los item de nuestra paginacion, siguiendo los mismos pasos dados en la documentación tenemos

1) Creamos el archivo PaginateDirectorySubscriber en la misma ruta indicada pero dentro de tu Bundle

<?php

//Ruta donde esta el archivo dentro de nuestro Bundle
namespace AN\PruebaBundle\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Knp\Component\Pager\Event\ItemsEvent;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\NativeQuery;

class PaginateDirectorySubscriber implements EventSubscriberInterface
{
    protected $em;

    public function __construct(EntityManager $entityManager)
    {
        $this->em = $entityManager;
    }

    public function items(ItemsEvent $event)
    {
        //Aqui ira nuestra logica a procesar
    }

    public static function getSubscribedEvents()
    {
        return array(
             /*increased priority to override any internal*/
            'knp_pager.items' => array('items', 1)
        );
    }
}

Obviamente la documentación nos indica que debemos agregar Finder, pero para el uso que deseamos darle no lo ocuparemos, asi que lo omitimos.

Además si te das cuenta agregue EntityManager, principalmente por si requieres hacer una consulta interna u otra cosa, para este ejemplo no lo ocuparemos, pero lo dejaremos incluido por si lo requieren.

Por ultimo, agregue NativeQuery el cual nos ayudara a determinar si es una query normal o no.

2) Creamos nuestro archivo paginate.xml como yo personalmente ocupo YAML, creare el archivo paginate.yml, la ruta donde ira sera similar a la que nos indican


/* Ruta donde esta este archivo :

symfony\src\AN\PruebaBundle\Resources\config\paginate.yml

*/

services:
    an_knp_paginator.subscriber:
        class: AN\PruebaBundle\Subscriber\PaginateDirectorySubscriber
        scope: request
        tags:
            - { name: knp_paginator.subscriber }
        arguments: [@doctrine.orm.entity_manager]

Como dije anteriormente, le pasamos como arguments el @doctrine.orm.entity_manager, asi podremos ocupar nuestra EntityManager de requerirla.

3) Agregamos a la configuración nuestro archivo generado en el paso 2


<?php
//Ruta donde esta
namespace AN\PruebaBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class ANPruebaExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
        // Aqui agregamos nuestro archivo creado en el paso 2 para que lo cargue
        $loader->load('paginate.yml');
    }
}

4) Antes de ocuparlo, nos falta lo mas importante, agregar la logica que necesitamos en nuestro PaginateDirectorySubscriber, dentro de la funcion items agregamos lo siguiente, quedando asi


    public function items(ItemsEvent $event)
    {
        //Si es una instancia de "NativeQuery"
        if ($event->target instanceof NativeQuery)
        {
            //Seteamos y asignamos
            $query = $event->target;
            $event->count = count($query->getArrayResult());
            
            $event->items = array_slice(
                $query->getArrayResult(),
                $event->getOffset(),
                $event->getLimit()
            );
            
            $event->stopPropagation();
        }
    }

Revisemos:

if ($event->target instanceof NativeQuery)

Aqui preguntamos si es una instancia de NativeQuery o no, esto nos dira si fue creada con createNativeQuery o createQuery, lo cual nos permite distinguir entre una consulta creada normalmente con DQL (createQuery) y una personalizada (createNativeQuery), ya que si no agregamos esto, todas nuestra consultas normales pasaran por aqui y se producira un error ya que estas requieren otro tratamiento.

5) Por último, lo llamamos desde nuestro Controller, aquí asumimos que estamos creando una consulta SQL donde llamamos a Usuarios, esto obviamente lo puedes hacer con DQL pero es solo un ejemplo. Además debemos guiarnos por la documentación que nos entrega Doctrine sobre Native SQL

/* Consulta SQL pura, fijandonos bien en los nombres de los
campos que estan en nuestra Base de Datos */
$sql = "SELECT u.id,u.nombre FROM Usuario u";

Ya tenemos nuestra consulta SQL, ahora debemos agregar ResultSetMapping que es requerido para generar nuestra consulta personalizada, agregamos la libreria primero

use Doctrine\ORM\Query\ResultSetMapping;

Y ahora el codigo referente a esto

//Esto es requerido y debe ser coherente con nuestra consulta SQL
// O no veremos los campos en el resultado
$rsm = new ResultSetMapping;
$rsm->addEntityResult('ANPruebaBundle:Usuario', 'u');
$rsm->addScalarResult('id', 'id');
$rsm->addScalarResult('nombre', 'nombre');

En la documentación nos indica que podemos ocupar addFieldResult en vez de addScalarResult, personalmente esto me generaba internamente otro array es decir el resultado que entregaba nuestro paginador era

Con addFieldResult:

array (
         array (
                    array (
                                ["id"] => 123
                                ["nombre"] => Ariel
                            )   
                   )  
          ) 

Con addScalarResult:

array (
         array (
                                ["id"] => 123
                                ["nombre"] => Ariel
                )
       )   

Ademas esta ultima nos permite ocupar campos alias, pero de esto hablaremos en otro POST o nos extenderemos mucho mas.

Volviendo a nuestra consulta original, ya tenemos la consulta en SQL y agregamos ResultSetMapping, pues bien, ahora podemos generarla

//Doctrine
$em = $this->getDoctrine()->getManager();

$query = $em->createNativeQuery($sql, $rsm);

Por ultimo, lo agregamos a nuestro paginador

$paginator  = $this->get('knp_paginator');

$pagination = $paginator->paginate(
            $query,
            $this->get('request')->query->get('page', 1)/*page number*/,
            10 /*limit per page*/
        );

Y listo, ahora podriamos probarla, si tienes el Bundle LadyBug para depurar puedes ocupar esto

ladybug_dump_die($pagination->getItems());

Si no, pues de la forma tradicional

var_dump($pagination->getItems());
exit();

En resumen, lo mas complicado es generar la consulta con createNativeQuery, ya que si no se genera bien con su respectivo ResultSetMapping los campos no apareceran, antes de pasarsela a nuestro paginador te recomiendo probar los resultados que entrega

ladybug_dump(count($query->getArrayResult()));

Fijate que ocupe getArrayResult, si ocupas otro como getResult no te entregara ningun resultado.

Saludos