- Racó tècnic - http://www.racotecnic.com -

Ordenar por columnas en Laravel 4 o cómo extender Laravel 4 a tu gusto

Si has trasteado o estás trasteando con Laravel 4 [1] habrás notado que está algo verde. Entre varias cosas se echa en falta que no tenga una manera sencilla para poder ordenar los resultados por columnas.

Dado que yo vengo de CakePHP, me he inspirado en él para crear el sistema de ordenación por columnas, así que si en Cake podemos crear un enlace para ordenar así:

<tr>
    <th><?php echo $this->Paginator->sort('id') ?></th>
    <th><?php echo $this->Paginator->sort('name', 'Nom') ?></th>
    <th><?php echo $this->Paginator->sort('address', 'Adreça') ?></th>
</tr>

En este tutorial verás cómo hacer para poder ordenar así, utilizando blade (el sistema de plantillas por defecto de Laravel 4):

<tr>
    <th>{{ $posts->sort('id') }}</th>
    <th>{{ $posts->sort('name', 'Nom') }}</th>
    <th>{{ $posts->sort('address', 'Adreça') }}</th>
</tr>

laravel_paginate [2]

Para poder hacer esto tendrás que extender el paginador de Laravel para añadirle las funciones que necesites (como sort).

Dado que lo que es ordenar en sí no tiene ninguna complicación y lo puedes encontrar documentado, en este tutorial me centraré más en cómo extender classes de Laravel 4 para adaptarlo a tus necesidades y de paso verás cómo tener la ordenación por columnas bien organizada (a nivel código) para mejor reutilización en futuros proyectos.

Nota: Debido al continuo desarrollo de Laravel este tutorial ha quedado rápidamente desfasado. He actualizado todos los enlaces para que funcionen correctamente pero ten en cuenta que algunos de estos ficheros han cambiado mucho o directamente ya no existen.

Antes de empezar me gustaría aclarar que no soy experto en Laravel ni mucho menos, simplemente me puse a trastear con él, vi que no tenía esta funcionalidad y exploré hasta encontrar la manera de integrarlo mejor en mi aplicación. Gracias a ello aprendí mucho mejor cómo funciona Laravel (internamente) y es en parte esto lo que quiero compartir contigo.

Por otro lado, a pesar de que el siguiente post rebosa de referencias externas a documentación, código incrustado y código en github, no estaría de más que hubieras leído la documentación de Laravel 4 antes de empezar.

Entendiendo el paginador

La Facade Paginator

Nota: Si no sabes qué es una Facade, repásate la documentación [3].

Para hacer todo esto lo primero que hay que hacer es entender cómo funciona el paginador actual de Laravel 4. Vayamos al grano.

Si accedes al directorio vendor/laravel/framework/src/Illuminate/Pagination [4] verás que hay un directorio llamado views [5], un fichero composer.json y cuatro ficheros php.

En la carpeta views hay tres plantillas para mostrar los botones de la paginación [6]. Si no leíste la documentación de Laravel sobre la paginación, gracias a este directorio acabas de averiguar que los botones de paginación tienen plantillas y que hay tres definidas por defecto.

El fichero composer.json ignóralo y de los ficheros PHP, fíjate en los tres siguientes:

El fichero Paginator.php es el que realmente te interesa ya que aquí es donde residen métodos como links() [10] y por tanto es donde te interesa añadir el método sort().

No obstante, para poder hacer todo esto posible necesitarás hacer tu propio PaginationServiceProvider.php y tu propio Environment.php, ya que es en esos dos ficheros donde se inicializa el paginador.

Para entender esto mejor, abre tu fichero app.php y en el array de providers fíjate que, entre varios providers, hay el siguiente [11]:

return array(
    // ... otros parámetros de configuración ...

    'providers' => array(
        // ... otros providers ...
        'Illuminate\Pagination\PaginationServiceProvider',
        // ... más providers ...
    )

    // ... más parámetros de configuración ...
);

Así pues, para cargar el paginador lo primero que hace Laravel es cargar la clase PaginationServiceProvider.

Si abres este fichero podrás observar cómo en el método register [12] se inicializa la clase Environment.

/**
 * Register the service provider.
 *
 * @return void
 */
public function register()
{
    $this->app['paginator'] = $this->app->share(function($app)
    {
        $paginator = new Environment($app['request'], $app['view'], $app['translator']);

        $paginator->setViewName($app['config']['view.pagination']);

        return $paginator;
    });
}

Y en la clase Enviornment, finalmente, en el método make() [13] se innicializa la clase Paginator.

/**
 * Get a new paginator instance.
 *
 * @param  array  $items
 * @param  int    $total
 * @param  int    $perPage
 * @return \Illuminate\Pagination\Paginator
 */
public function make(array $items, $total, $perPage)
{
    $paginator = new Paginator($this, $items, $total, $perPage);

    return $paginator->setupPaginationContext();
}

Resumiendo: En app.php se carga el PaginationServiceProvider que carga Environment que a su vez carga Paginator; así que habrá que crear tres clases para poder crear el método sort().

El método paginate de Eloquent

Esto que has visto únicamente es para añadir el método sort(), que básicamente pintará enlaces en las vistas, pero todavía tienes que hacer que la paginación te haga caso y ordene según los parámetros establecidos.

Si recuerdas la documentación de Laravel [6], para hacer la paginación de elementos debes hacer algo así (los ejemplos a continuación son de un controlador):

public function index()
{
    $posts = Post::paginate(20);

    return View::make('posts.admin.index')->with('posts', $posts);
}

Éste método paginate() [14] forma parte del Builder de Eloquent, así que también habrá que extenderlo si quieres que la paginación tome parámetros adicionales de la URL para ordenar los resultados.

Si en lugar de paginar como en el ejemplo anterior lo hubiera hecho así:

public function index()
{
    $posts = Db::table('posts')->paginate('15');

    return View::make('posts.admin.index')->with('posts', $posts);
}

En vez de modificar el Builder de Eloquent tendrías que modificar el Builder de queries [15]. Aun así, en este tutorial me centraré en el método paginate() del Builder de Eloquent (primer ejemplo).

No obstante, el Builder de Eloquent no se carga igual que el paginador. Éste está inyectado en los modelos, por lo que podrías modificar el método paginate directamente redefiniéndolo en el modelo deseado, ya que éste extiende de Eloquent:

// app/models/Post.php
class Post extends Eloquent
{
    public function paginate($perPage, $columns = array('*'), $orderBy = array())
    {
        // nuestros cambios para ordenar por columna
    }
}

Fácilmente puedes añadirlo en un nuevo modelo padre del cual extender, por ejemplo AppModel (ya que digo que vengo de CakePHP, voy a demostrarlo):

// app/models/AppModel.php
class AppModel extends Eloquent
{
    public function paginate($perPage, $columns = array('*'), $orderBy = array())
    {
        // nuestros cambios para ordenar por columna
    }
}

// app/models/Post.php
class Post extends AppModel
{

}

Pero esto en realidad no sería muy reusable, así que en su lugar, y ya que Laravel está pensado para ser utilizado con PHP >= 5.4, lo que harás será crear un trait [16].

Habiendo creado el trait simplemente habrá que indicarlo en aquellos modelos donde quieras utilizarlo:

class Post extends Eloquent
{
    use OrderBy; // donde OrderBy es el nombre que has puesto al trait
}

Los cambios

Confiando en que has entendido algo de lo que he explicado hasta ahora, resumiré los cambios a hacer y enlazaré hacia los ficheros que he publicado en github [17].

Yo tengo todas mis extensiones dentro del directorio app/library/Minombre/Extensions, pero tienes que poder ponerlas en cualquier directorio que cargue clases Laravel siempre y cuando respetes los namespaces.

Yendo al grano

Si quieres ahorrarte el proceso que viene a continuación puedes añadir mi proyecto [17] como un submódulo de tu proyecto fácilmente:

git submodule add https://github.com/elboletaire/laravel-utils-and-extensions.git app/library/Elboletaire

Si no utilizas git descarga directamente el zip de github [18] y descomprime los contenidos del fichero en un nuevo directorio app/library/Elboletaire.

El método sort

Volviendo al resumen, tienes que crear tu propio PaginationServiceProvider [19] para en él cargar un nuevo Environment [20] que finalmente será quien cargue el Paginator [21].

Es en ésta última clase Paginator donde deberás definir el método sort y otros métodos necesarios para que éste funcione correctamente. Como digo muy a menudo, el código habla por sí solo, así que no me centraré en explicarlo.

Una vez has creado tus ficheros (con su contenido, evidentemente) tienes que modificar el array de providers para que cargue tu PaginationServiceProvider en lugar de el de Laravel:

return array(
    // [...]

    'providers' => array(
        // reemplazas el original
        // 'Illuminate\Pagination\PaginationServiceProvider',
        // por el tuyo
        'Elboletaire\Extensions\Pagination\PaginationServiceProvider'
    )

    // [...]
);

El método paginate

Crea un trait en el mismo directorio donde has creado los ficheros anteriores; éste será el encaragdo de modificar la consulta hacia la base de datos a partir de los datos recibidos vía GET.

Puedes ponerle el nombre que quieras, al mío lo he llamado PaginatorSort [22], pero como digo es cuestión de gustos.

Una vez creado y hecha la lógica sólo hay que llamarlo desde el modelo que quieras que tenga ordenación por columnas:

class Post extends Eloquent
{
    use Elboletaire\Extensions\Pagination\PaginatorSort;
}

Añadiendo paginación

Hechos estos dos sencillos pasos, ya puedes añadir a tus vistas los enlaces para poder ordenar por columna:

<tr>
    <th>{{ $posts->sort('id') }}</th>
    <th>{{ $posts->sort('name', 'Nombre') }}</th>
</tr>

Y mientras tu controlador (o ruta) cargue los datos de la paginación mediante el modelo (al que previamente has cargado el trait creado):

public function index()
{
    $posts = Post::paginate(20);

    return View::make('posts.admin.index')->with('posts', $posts);
}

…la paginación estaría funcionando. Congrats my friend 😀

Si quieres puedes especificar el orden por defecto pasándolo como tercer parámetro:

public function index()
{
    $posts = Post::paginate(20, null, 'name asc');
    // o bien pasado como array:
    $posts = Post::paginate(20, null, array(
        'direction' => 'asc',
        'sort'      => 'name'
    ));

    return View::make('posts.admin.index')->with('posts', $posts);
}

Fin [referencias y esas cosas]

Si eres lector asiduo (aunque aquí no nos vaya mucho esto de la asiduidad..) sabrás que estoy abierto a que me preguntéis lo que sea —otra cosa es si sabré contestar…

El repositorio está colgado en github, entre otras cosas, para que la gente colabore; así que si te animas ya sabes.

Más información y fuentes: