Skip to content

5.20 Paginación, Filtrado y Ordenación en Laravel


5.20.1 Introducción

En aplicaciones con grandes volúmenes de datos, mostrar toda la información en una sola página es poco práctico y perjudica el rendimiento y la experiencia del usuario. La paginación es una técnica para dividir los resultados en páginas, permitiendo mostrar un número limitado de elementos por página.

Además, la filtración y la ordenación son funcionalidades comunes que permiten al usuario buscar y organizar los datos según sus criterios.

En este tema construiremos una aplicación que permitirá paginar, filtrar y ordenar una lista de productos de manera sencilla y clara.


5.20.2 Preparación del entorno: Base de datos y datos de prueba

5.20.2.1 Creación de la tabla products

Vamos a crear la tabla products que tendrá los campos básicos: nombre y precio.

Ejecuta:

php artisan make:model Product -m

En la migración generada (database/migrations/YYYY_MM_DD_create_products_table.php):

migracion

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 8, 2);
        $table->integer('stock')->default(0);
        $table->timestamps();
    });
}

Ejecuta la migración:

php artisan migrate

5.20.2.2 Crear Factory para Product

Crea el factory para generar datos de prueba:

php artisan make:factory ProductFactory --model=Product

Edita database/factories/ProductFactory.php:

ProductFactory

1
2
3
4
5
6
7
8
public function definition()
{
    return [
        'name' => $this->faker->word(),
        'price' => $this->faker->randomFloat(2, 1, 1000),
        'stock' => $this->faker->numberBetween(1, 100)
    ];
}

No olvidar indicar en el modelo Product que use el factory:

Product

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Product extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'price',
        'stock',
    ];
}

5.20.2.3 Crear Seeder para products

Crea un seeder:

php artisan make:seeder ProductSeeder

En database/seeders/ProductSeeder.php:

seeder

1
2
3
4
public function run()
{
    Product::factory()->count(100)->create();
}

Registra el seeder en DatabaseSeeder.php:

$this->call(ProductSeeder::class);

Ejecuta:

php artisan db:seed

Verifica que hay 100 productos en la tabla y que los datos son correctos.


5.20.3 Implementación básica de paginación

Ahora que tenemos los datos, vamos primero a implemnentar la ruta y el controlador para mostrar la lista de productos.

5.20.3.1 Ruta y controlador

En routes/web.php:

1
2
3
use App\Http\Controllers\ProductController;

Route::get('/', [ProductController::class, 'index'])->name('products.index');

Crea el controlador:

php artisan make:controller ProductController

En app/Http/Controllers/ProductController.php:

Creamos el método index para mostrar la lista de productos, en principio sin paginación para comprobar que todo funciona correctamente.

Controller

1
2
3
4
5
    public function index()
    {
        $products = Product::all();
        return view('products.index', compact('products'));
    }

Ahora creamos la vista resources/views/products/index.blade.php:

Vista

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lista de Productos</title>
</head>
<body>
    <h1>Lista de Productos</h1>
    <table>
        <thead><tr><th>Nombre</th><th>Precio</th></tr></thead>
        <tbody>
        @foreach ($products as $product)
            <tr><td>{{ $product->name }}</td><td>{{ $product->price }}</td></tr>
        @endforeach
        </tbody>
    </table>
</body>
</html>

Con esto podemos comprobar que la lista de productos se muestra correctamente y que son demasiados para mostrarlos todos a la vez. Vamos ahora a implementar la paginación para que se muestre un número limitado de productos por página.

Comenzamos por modificar el método index del controlador para que use la paginación de Laravel.

Controller

1
2
3
4
5
6
7
use App\Models\Product;

public function index()
{
    $products = Product::paginate(10); // Paginación 10 elementos por página
    return view('products.index', compact('products'));
}

¿Qué hace paginate(10)? Carga 10 productos por página y genera la lógica necesaria para manejar la paginación. Esta lógica nos permitirá mostrar los enlaces de paginación en la vista.

5.20.3.2 Vista paginada

En resources/views/products/index.blade.php:

vista paginada

<table>
    <thead><tr><th>Nombre</th><th>Precio</th></tr></thead>
    <tbody>
    @foreach ($products as $product)
        <tr><td>{{ $product->name }}</td><td>{{ $product->price }}</td></tr>
    @endforeach
    </tbody>
</table>
<br>
{{ $products->links() }}

Podemos ver como en la url se añaden parámetros para la paginación, como page=2, y se generan enlaces para navegar entre las páginas.

Explicación

  • paginate(10): Carga 10 productos por página.
  • links(): Genera la botonera de paginación.
  • appends(): Mantiene los parámetros de la URL al cambiar de página.
  • Parámetros opcionales para paginate() permiten personalizar la página actual y otros aspectos.

Diferencia con simplePaginate()

$products = Product::simplePaginate(10);
  • No muestra número de páginas, solo enlaces para siguiente/anterior.
  • Más eficiente para grandes conjuntos de datos.

Claro, aquí tienes una versión extendida y muy didáctica del apartado sobre personalización avanzada de paginación en Laravel, enfocada en que los alumnos entiendan todo el proceso para usar plantillas de paginación con Bootstrap o Tailwind y puedan personalizarlas fácilmente.


5.20.4 Personalización avanzada de paginación

Cuando utilizamos la paginación con Laravel, el método links() genera automáticamente los controles de paginación (botonera, enlaces, etc.) utilizando una vista por defecto. Esta vista puede ser personalizada para ajustarse a la estética y funcionalidades específicas de tu aplicación.

5.20.4.1 ¿Dónde están las vistas de paginación?

Laravel incluye varias vistas de paginación listas para usar, adaptadas a diferentes frameworks CSS:

  • Tailwind CSS (usada por defecto en Laravel 8+).
  • Bootstrap 4.
  • Bootstrap 5.
  • Simple pagination (versión simplificada sin botones numéricos).

Estas vistas se encuentran dentro del paquete de Laravel, normalmente en:

vendor/laravel/framework/src/Illuminate/Pagination/resources/views

Si deseas modificarlas, no debes tocar esos archivos directamente, sino copiarlas a tu carpeta de vistas para sobreescribirlas.


5.20.4.2 Cómo copiar las vistas para personalizar la paginación

Ejecuta el siguiente comando para publicar las vistas de paginación en tu carpeta resources:

php artisan vendor:publish --tag=laravel-pagination

Esto copiará las vistas a:

resources/views/vendor/pagination

Ahí encontrarás los archivos:

  • tailwind.blade.php
  • bootstrap-4.blade.php
  • bootstrap-5.blade.php
  • simple-tailwind.blade.php
  • simple-bootstrap-4.blade.php

Ahora, puedes editar cualquiera de estos archivos para personalizar el marcado HTML, clases CSS, o la estructura según tus necesidades.


5.20.4.3 Estructura típica de una vista de paginación

Vamos a analizar rápidamente el archivo bootstrap-4.blade.php como ejemplo.

  • Está compuesto por una lista (<ul>) con elementos (<li>) para cada página.
  • Cada enlace tiene clases de Bootstrap para que la botonera se vea acorde al framework.
  • Hay controles para página anterior, siguiente, y botones numerados.
  • Se usa la variable $paginator para acceder a la información de paginación (como el número de página actual, URLs, etc.).

Por ejemplo, un fragmento típico para mostrar un botón activo se ve así:

<li class="page-item active" aria-current="page"><span class="page-link">{{ $page }}</span></li>

El fragmento para botones con enlaces normales:

<li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>

Puedes cambiar las clases, agregar iconos o modificar cualquier aspecto del HTML para que la paginación encaje con tu diseño.


5.20.4.4 Cómo usar la plantilla Bootstrap o Tailwind en la vista

Para usar explícitamente la paginación con una plantilla específica, por ejemplo, Bootstrap 5, debes pasar el nombre de la vista al método links().

En tu vista Blade:

{{ $products->links('pagination::bootstrap-5') }}

Esto fuerza a Laravel a usar la plantilla bootstrap-5.blade.php para generar la paginación.

link a bootstrap 5

Si usas Bootstrap 5, asegúrate de tener la versión correcta instalada y configurada en tu proyecto. Puedes instalar Bootstrap 5 con npm o incluirlo directamente desde un CDN en tu layout principal.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">

Si quieres usar Tailwind (que es el valor por defecto en Laravel 8+):

{{ $products->links() }}

O explícitamente:

{{ $products->links('pagination::tailwind') }}

link a tailwind

Si usas Tailwind CSS, asegúrate de tenerlo instalado y configurado en tu proyecto. Puedes instalar Tailwind CSS con npm o incluirlo directamente desde un CDN en tu layout principal.

<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">


5.20.4.5 Cambiar el número de elementos por página

Puedes cambiar el número de elementos que se muestran por página simplemente pasando un valor diferente a paginate(). Por ejemplo, para mostrar 25 productos por página:

$products = Product::paginate(25);

Recuerda que si usas filtros o parámetros GET, deberás mantenerlos con appends() para que no se pierdan al cambiar de página.

$products = Product::paginate(25)->appends(request()->query());
Esto asegura que los filtros aplicados se mantengan al navegar entre las páginas.

5.20.5 Paginación con filtros

Crear formulario de filtrado

Lo primero será permitir al usuario qintroducir datos que se conviertan en filtros a la hora de mostrar los productos. Para ello, crearemos un formulario en la vista que permita al usuario introducir criterios de búsqueda.

Añade en la vista:

filtros

1
2
3
4
5
6
<form method="GET" action="{{ route('products.index') }}">
    <input type="text" name="name" value="{{ request('name') }}" placeholder="Buscar por nombre">
    <input type="number" name="price_min" value="{{ request('price_min') }}" placeholder="Precio mínimo">
    <input type="number" name="price_max" value="{{ request('price_max') }}" placeholder="Precio máximo">
    <button type="submit">Filtrar</button>
</form>

Modificar controlador

Controller

public function index(Request $request)
{
    $query = Product::query();

    if ($request->filled('name')) {
        $query->where('name', 'like', '%' . $request->name . '%');
    }
    if ($request->filled('price_min')) {
        $query->where('price', '>=', $request->price_min);
    }
    if ($request->filled('price_max')) {
        $query->where('price', '<=', $request->price_max);
    }

    $products = $query->paginate(10)->appends($request->all());

    return view('products.index', compact('products'));
}
  • appends($request->all()) mantiene los filtros al cambiar de página.

5.20.6 Ordenación de resultados

Modificar vista para ordenar

Añade enlaces para ordenar:

enlaces

1
2
3
4
<a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'name', 'direction' => 'asc'])) }}">Nombre ↑</a>
<a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'name', 'direction' => 'desc'])) }}">Nombre ↓</a>
<a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'price', 'direction' => 'asc'])) }}">Precio ↑</a>
<a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'price', 'direction' => 'desc'])) }}">Precio ↓</a>

Controlador para ordenar

mofificación del controlador

public function index(Request $request)
{
    $query = Product::query();

    // Filtros como antes...

    if ($request->filled('sort') && $request->filled('direction')) {
        $query->orderBy($request->sort, $request->direction);
    }

    $products = $query->paginate(10)->appends($request->all());

    return view('products.index', compact('products'));
}

Mejora visual

Para finalizar podemos mejorar el aspecto visual de la página utilizando Bootstrap o Tailwind CSS. Simplemente añade el CDN correspondiente en tu layout principal. También vamos a poner los botones de editar, borrar y añadir en la parte superior de la tabla para que el usuario pueda interactuar con los productos de forma más sencilla. Aunque no están implemnentados, los enlaces estarán ahí para que el alumno pueda ver cómo se haría.

mejora visual

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Lista de Productos</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>

<body>
    <div class="container mt-4">
        <div class="d-flex justify-content-between align-items-center mb-3">
            <h1>Lista de Productos</h1>
            <a href="#" class="btn btn-success">Crear Producto</a>
        </div>

        <form method="GET" action="{{ route('products.index') }}" class="row g-3 mb-3">
            <div class="col-auto">
                <input type="text" name="name" value="{{ request('name') }}" class="form-control" placeholder="Buscar por nombre" />
            </div>
            <div class="col-auto">
                <input type="number" name="price_min" value="{{ request('price_min') }}" class="form-control" placeholder="Precio mínimo" />
            </div>
            <div class="col-auto">
                <input type="number" name="price_max" value="{{ request('price_max') }}" class="form-control" placeholder="Precio máximo" />
            </div>
            <div class="col-auto">
                <button type="submit" class="btn btn-primary">Filtrar</button>
            </div>
        </form>

        <div class="mb-3">
            <strong>Ordenar por:</strong>
            <a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'name', 'direction' => 'asc'])) }}" rel="nofollow" class="btn btn-outline-secondary btn-sm">Nombre ↑</a>
            <a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'name', 'direction' => 'desc'])) }}" rel="nofollow" class="btn btn-outline-secondary btn-sm">Nombre ↓</a>
            <a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'price', 'direction' => 'asc'])) }}" rel="nofollow" class="btn btn-outline-secondary btn-sm">Precio ↑</a>
            <a href="{{ route('products.index', array_merge(request()->all(), ['sort' => 'price', 'direction' => 'desc'])) }}" rel="nofollow" class="btn btn-outline-secondary btn-sm">Precio ↓</a>
        </div>

        <table class="table table-striped table-bordered align-middle">
            <thead>
                <tr>
                    <th>Nombre</th>
                    <th>Precio</th>
                    <th style="width: 200px;">Acciones</th>
                </tr>
            </thead>
            <tbody>
                @foreach ($products as $product)
                    <tr>
                        <td>{{ $product->name }}</td>
                        <td>{{ number_format($product->price, 2, ',', '.') }} €</td>
                        <td>
                            <a href="#" class="btn btn-sm btn-primary me-1">Editar</a>
                            <button type="button" class="btn btn-sm btn-danger">Eliminar</button>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>

        {{ $products->links('pagination::bootstrap-5') }}
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>

</html>

Vista de productos paginada con Bootstrap


5.20.7 Conclusiones

  • La paginación mejora rendimiento y UX.
  • Se puede combinar con filtros y ordenación sin perder el estado.
  • Laravel facilita la personalización de la vista de paginación.
  • Buenas prácticas para consultas eficientes.

5.20.8 Paginación en APIs con Laravel

¿Por qué usar paginación en APIs?

Cuando una API devuelve grandes volúmenes de datos, paginar los resultados es fundamental para evitar enviar demasiada información de golpe, lo que puede afectar el rendimiento y el consumo de recursos tanto en servidor como en cliente.

Laravel facilita la paginación en APIs y ofrece una respuesta JSON que incluye información extra útil para manejar la paginación desde el cliente.


Implementación básica de paginación en un controlador API

Ejemplo de método index en un controlador API para productos:

modificación del controlador

use App\Models\Product;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

public function index(Request $request): JsonResponse
{
    $products = Product::paginate(10);

    return response()->json($products);
}

Estructura de la respuesta JSON paginada

La respuesta JSON que devuelve Laravel contiene, además de los datos (por ejemplo, los productos), otros campos útiles para manejar la paginación en el cliente:

response JSON

{
"current_page": 1,
"data": [
    { "id": 1, "name": "Producto 1", "price": "100.00" },
    { "id": 2, "name": "Producto 2", "price": "200.00" }
],
"first_page_url": "http://api.example.com/products?page=1",
"from": 1,
"last_page": 10,
"last_page_url": "http://api.example.com/products?page=10",
"links": [
    { "url": null, "label": "&laquo; Previous", "active": false },
    { "url": "http://api.example.com/products?page=1", "label": "1", "active": true },
    { "url": "http://api.example.com/products?page=2", "label": "2", "active": false }
],
"next_page_url": "http://api.example.com/products?page=2",
"path": "http://api.example.com/products",
"per_page": 10,
"prev_page_url": null,
"to": 10,
"total": 100
}

Campos importantes en la respuesta paginada

Campo Descripción
current_page Página actual que está viendo el cliente.
data Array con los elementos de la página actual.
first_page_url URL para la primera página.
last_page Número total de páginas disponibles.
last_page_url URL para la última página.
links Array con enlaces de paginación para navegación.
next_page_url URL para la siguiente página (null si es la última).
prev_page_url URL para la página anterior (null si es la primera).
per_page Número de elementos por página.
total Total de elementos en la consulta.

Personalización y filtros en API paginada

Al igual que en vistas, puedes combinar la paginación con filtros y ordenación, pasando los parámetros como query strings y aplicándolos en la consulta antes de llamar a paginate().

Ejemplo:

modificación del controlador

1
2
3
4
5
6
7
8
9
$query = Product::query();

if ($request->filled('name')) {
    $query->where('name', 'like', "%{$request->name}%");
}

$products = $query->paginate(10);

return response()->json($products);

Resumen

  • La paginación en APIs es esencial para controlar el volumen de datos enviados.
  • Laravel ofrece un formato JSON con toda la información necesaria para que el cliente pueda navegar entre páginas.
  • Puedes combinar paginación con filtros y ordenación para APIs más flexibles.