MidТеория6 min

Пагинация (Pagination)

paginate(), simplePaginate(), cursorPaginate(), кастомная пагинация, пагинация API, настройка Paginator

Laravel предоставляет три метода пагинации: paginate() (с полной информацией о страницах), simplePaginate() (только prev/next) и cursorPaginate() (курсорная пагинация). Каждый метод подходит для определённых сценариев.

Типы пагинации

paginate() -- Полная пагинация

Выполняет два запроса: COUNT(*) для общего числа записей и SELECT с LIMIT/OFFSET для текущей страницы.

use App\Models\User;

// Basic pagination (15 items per page by default)
$users = User::paginate();

// Custom per page
$users = User::paginate(perPage: 25);

// With conditions
$users = User::where('active', true)
    ->orderBy('name')
    ->paginate(20);

// Specifying the page column name
$users = User::paginate(
    perPage: 15,
    pageName: 'users_page'
);

// With specific columns
$users = User::paginate(
    perPage: 15,
    columns: ['id', 'name', 'email']
);

simplePaginate() -- Упрощённая пагинация

Выполняет один запрос (без COUNT). Знает только о наличии следующей/предыдущей страницы.

use App\Models\Post;

// Simple pagination (no total count)
$posts = Post::simplePaginate(15);

// More efficient for large datasets
$posts = Post::where('published', true)
    ->orderBy('created_at', 'desc')
    ->simplePaginate(20);

cursorPaginate() -- Курсорная пагинация

Использует cursor-based подход (keyset pagination). Не использует OFFSET. Идеален для бесконечной прокрутки и больших таблиц.

use App\Models\User;

// Cursor-based pagination
$users = User::orderBy('id')->cursorPaginate(15);

// The cursor is based on the order column
$users = User::orderBy('created_at', 'desc')
    ->orderBy('id', 'desc')
    ->cursorPaginate(15);

Сравнение методов пагинации

Характеристика paginate() simplePaginate() cursorPaginate()
SQL-запросов 2 (COUNT + SELECT) 1 (SELECT) 1 (SELECT)
Общее число страниц Да Нет Нет
Номера страниц Да Нет Нет
Прямой переход на страницу Да Нет Нет
Навигация Полная Prev/Next Prev/Next
Производительность Средняя Хорошая Лучшая
Большие таблицы Медленно (COUNT) Хорошо Отлично
URL-параметр ?page=5 ?page=5 ?cursor=eyJpZ...
Стабильность при INSERT Может дублировать Может дублировать Стабильная

Использование с Query Builder

use Illuminate\Support\Facades\DB;

// Query Builder pagination
$users = DB::table('users')->paginate(15);
$users = DB::table('users')->simplePaginate(15);
$users = DB::table('users')->orderBy('id')->cursorPaginate(15);

Работа с результатами пагинации

Свойства и методы

$users = User::paginate(15);

// Paginator methods
$users->count();           // Number of items on current page
$users->currentPage();     // Current page number
$users->firstItem();       // Index of the first item
$users->getOptions();      // Paginator options
$users->getUrlRange(1, 5); // URLs for pages 1-5
$users->hasMorePages();    // Are there more pages?
$users->hasPages();        // Enough items for multiple pages?
$users->items();           // Items on the current page
$users->lastItem();        // Index of the last item
$users->lastPage();        // Last page number (paginate only)
$users->nextPageUrl();     // URL for the next page
$users->onFirstPage();     // Is on the first page?
$users->onLastPage();      // Is on the last page?
$users->perPage();         // Items per page
$users->previousPageUrl(); // URL for the previous page
$users->total();           // Total items (paginate only)
$users->url(5);            // URL for a specific page

// Cursor paginator specific
$cursorUsers = User::orderBy('id')->cursorPaginate(15);
$cursorUsers->cursor();           // Current cursor
$cursorUsers->nextCursor();       // Cursor for next page
$cursorUsers->previousCursor();   // Cursor for previous page
$cursorUsers->getCursorForItem($user); // Cursor for a specific item

Итерация

$users = User::paginate(15);

// Iterate over items
foreach ($users as $user) {
    echo $user->name;
}

// Append query string to pagination links
$users->appends(['sort' => 'name'])->links();

// Or append all current query string
$users->withQueryString()->links();

// Custom fragment
$users->fragment('users')->links();

Отображение в Blade

{{-- Display paginated items --}}
<div class="container">
    @foreach ($users as $user)
        <p>{{ $user->name }} - {{ $user->email }}</p>
    @endforeach
</div>

{{-- Display pagination links --}}
{{ $users->links() }}

{{-- Using a specific view --}}
{{ $users->links('vendor.pagination.bootstrap-5') }}

{{-- Simple pagination links --}}
{{ $users->links('pagination::simple-default') }}

Кастомизация пагинации Blade

# Publish pagination views
php artisan vendor:publish --tag=laravel-pagination
# Views will be placed in resources/views/vendor/pagination/

Дефолтный вид пагинации

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Use Bootstrap 5 for pagination
        Paginator::useBootstrapFive();

        // Or Bootstrap 4
        Paginator::useBootstrapFour();

        // Default: Tailwind CSS
        Paginator::useTailwind();

        // Custom default view
        Paginator::defaultView('custom-pagination');
        Paginator::defaultSimpleView('custom-simple-pagination');
    }
}

API пагинация (JSON)

При возврате пагинатора из маршрута Laravel автоматически преобразует результат в JSON.

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

final class UserController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        return UserResource::collection(
            User::where('active', true)
                ->orderBy('name')
                ->paginate(15)
        );
    }
}

JSON-структура paginate()

{
    "data": [
        {"id": 1, "name": "John"},
        {"id": 2, "name": "Jane"}
    ],
    "links": {
        "first": "http://example.com/api/users?page=1",
        "last": "http://example.com/api/users?page=10",
        "prev": null,
        "next": "http://example.com/api/users?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 10,
        "links": [
            {"url": null, "label": "&laquo; Previous", "active": false},
            {"url": "http://example.com/api/users?page=1", "label": "1", "active": true},
            {"url": "http://example.com/api/users?page=2", "label": "2", "active": false},
            {"url": "http://example.com/api/users?page=2", "label": "Next &raquo;", "active": false}
        ],
        "path": "http://example.com/api/users",
        "per_page": 15,
        "to": 15,
        "total": 150
    }
}

JSON-структура cursorPaginate()

{
    "data": [
        {"id": 1, "name": "John"},
        {"id": 2, "name": "Jane"}
    ],
    "path": "http://example.com/api/users",
    "per_page": 15,
    "next_cursor": "eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
    "next_page_url": "http://example.com/api/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
    "prev_cursor": null,
    "prev_page_url": null
}

Кастомная пагинация

Ручная пагинация

use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;

// Create a paginator from a collection
$collection = collect([/* items */]);

$page = Paginator::resolveCurrentPage();
$perPage = 15;
$items = $collection->slice(($page - 1) * $perPage, $perPage)->values();

$paginator = new LengthAwarePaginator(
    items: $items,
    total: $collection->count(),
    perPage: $perPage,
    currentPage: $page,
    options: [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => 'page',
    ]
);

Пагинация с трансформацией

$users = User::paginate(15);

// Transform items while keeping pagination
$users->through(function ($user) {
    return [
        'id' => $user->id,
        'name' => $user->name,
        'initials' => strtoupper(substr($user->name, 0, 2)),
    ];
});

// Or use getCollection() / setCollection()
$items = $users->getCollection()->map(function ($user) {
    $user->formatted_name = strtoupper($user->name);
    return $user;
});

$users->setCollection($items);

Пагинация в Eloquent Relationships

// Paginate a relationship
$user = User::find(1);
$posts = $user->posts()->paginate(10);

// With eager loading
$posts = $user->posts()
    ->with('comments')
    ->where('published', true)
    ->paginate(10);

Ограничения курсорной пагинации

// ✅ Works: ordered by single column
User::orderBy('id')->cursorPaginate(15);

// ✅ Works: ordered by multiple columns
User::orderBy('name')->orderBy('id')->cursorPaginate(15);

// ❌ Does NOT work: no ORDER BY
User::cursorPaginate(15); // Requires explicit orderBy

// ❌ Does NOT work: raw expressions in orderBy
User::orderByRaw('FIELD(status, "active", "pending")')
    ->cursorPaginate(15);

// ⚠️ Limited: cursor values must be serializable to string

Тестирование пагинации

<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

final class UserPaginationTest extends TestCase
{
    public function test_users_are_paginated(): void
    {
        User::factory()->count(50)->create();

        $response = $this->getJson('/api/users');

        $response->assertOk()
            ->assertJsonCount(15, 'data')
            ->assertJsonPath('meta.current_page', 1)
            ->assertJsonPath('meta.last_page', 4)
            ->assertJsonPath('meta.per_page', 15)
            ->assertJsonPath('meta.total', 50)
            ->assertJsonStructure([
                'data' => [['id', 'name', 'email']],
                'links' => ['first', 'last', 'prev', 'next'],
                'meta' => ['current_page', 'from', 'last_page', 'per_page', 'to', 'total'],
            ]);
    }

    public function test_cursor_pagination(): void
    {
        User::factory()->count(30)->create();

        $response = $this->getJson('/api/users?per_page=10');

        $response->assertOk()
            ->assertJsonCount(10, 'data')
            ->assertJsonStructure([
                'data',
                'next_cursor',
                'next_page_url',
                'per_page',
                'prev_cursor',
                'prev_page_url',
            ]);

        // Follow next page
        $nextUrl = $response->json('next_page_url');
        $nextResponse = $this->getJson($nextUrl);

        $nextResponse->assertOk()
            ->assertJsonCount(10, 'data');
    }

    public function test_second_page(): void
    {
        User::factory()->count(30)->create();

        $response = $this->getJson('/api/users?page=2&per_page=10');

        $response->assertOk()
            ->assertJsonPath('meta.current_page', 2)
            ->assertJsonCount(10, 'data');
    }
}

Проверь себя

Сколько SQL-запросов выполняет paginate() и simplePaginate()?

Почему cursorPaginate() более производителен на больших таблицах, чем paginate()?

Какое обязательное требование к запросу при использовании cursorPaginate()?

Метод through() на пагинаторе используется для: