Туторіал: пагінація в Laravel

Перекладено ШІ
Оригінал: Laravel News
Оновлено: 08 листопада, 2024
У цій статті ми розглянемо методи пагінації, їхні переваги та як впровадити їх у свої проекти, щоб покращити продуктивність і зручність для користувачів. Чи готові дізнатися, як оптимізувати свої веб-додатки? Читайте далі!

Пагінація — це поширена функція у веб-додатках. Практично кожен Laravel-додаток, над яким я працював, реалізував якусь форму пагінації.

Але що таке пагінація та навіщо вона потрібна? Як можна реалізувати пагінацію у наших Laravel-додатках? І як обрати метод пагінації?

У цій статті ми відповімо на ці питання та розглянемо, як використовувати пагінацію в Laravel як для Blade-видів, так і для API-ендпоінтів. Після прочитання статті ви будете впевнені в можливості впровадження пагінації у своїх проектах.

# Що таке пагінація?

Пагінація — це техніка, яка дозволяє розділити великий набір даних на менші частини (сторінки). Вона дозволяє виводити підмножину даних, а не всі значення одночасно.

Уявіть, що у вас є сторінка, яка виводить імена всіх користувачів вашого додатка. Якщо у вас тисячі користувачів, то не буде практично виводити їх усіх на одній сторінці. Замість цього ви можете використовувати пагінацію для відображення, наприклад, 10 користувачів одночасно і надавати можливість користувачам переходити між сторінками для перегляду інших користувачів (наступні 10).

Використання пагінації дозволяє:

Зазвичай пагінацію поділяють на два види:

Laravel надає три методи для пагінації запитів Eloquent:

Давайте розглянемо кожен із цих методів детальніше.

# Використання методу paginate

Метод paginate дозволяє отримати підмножину даних із бази, використовуючи зсув і ліміт (ми розглянемо їх детальніше пізніше, коли будемо аналізувати SQL-запити).

Ви можете використовувати метод paginate наступним чином:

use App\Models\User;
 
$users = User::query()->paginate();

Запустивши цей код, ви отримаєте $users як екземпляр Illuminate\Contracts\Pagination\LengthAwarePaginator, зазвичай це об'єкт Illuminate\Pagination\LengthAwarePaginator. Цей екземпляр пагінатора містить усю інформацію, що необхідна для відображення пагінованих даних у вашому додатку.

Метод paginate автоматично визначає запитуваний номер сторінки на основі параметра page у URL. Наприклад, якщо ви відвідаєте https://my-app.com/users?page=2, метод paginate отримає дані для другої сторінки.

За замовчуванням усі методи пагінації в Laravel використовують 15 записів за раз. Однак це значення можна змінити (ми розглянемо, як це зробити пізніше).

# Використання paginate у Blade-виді

Тепер розглянемо, як використовувати метод paginate при виведенні даних у Blade-виді.

Уявіть, що у нас є простий маршрут, який отримує користувачів з бази у пагинованому форматі та передає їх до виду:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    $users = User::query()->paginate();
 
    return view('users.index', [
        'users' => $users,
    ]);
});

Наш файл resources/views/users/index.blade.php може виглядати так:

<html>
<head>
    <title>Пагінація</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
 
<body>
    <div class="max-w-5xl mx-auto py-8">
        <h1 class="text-5xl">Пагінація</h1>
 
        <ul class="py-4">
            @foreach ($users as $user)
                <li class="py-1 border-b">{{ $user->name }}</li>
            @endforeach
        </ul>
 
        {{ $users->links() }}
    </div>
</body>
</html>

В результаті отримаємо сторінку, яка виглядатиме приблизно так:

Давайте розберемо, що відбувається у Blade-виді:

Також ми можемо бачити, що метод paginate надає огляд даних пагінації. Ми можемо бачити, що переглядаємо з 16-го по 30-й записи, з загальної кількості 50. Також видно, що ми на другій сторінці і в нас всього 4 сторінки.

Варто зауважити, що метод links повертає HTML, оформлений з використанням Tailwind CSS. Якщо ви хочете використовувати щось інше або самостійно стилізувати пагінаційні посилання, ви можете ознайомитися з документацією щодо кастомізації вигляду пагінації.

# Використання paginate в API-ендпоінтах

Також метод paginate можна використовувати в API-ендпоінтах. Laravel спрощує цей процес, автоматично конвертуючи пагіновані дані в JSON.

Наприклад, ми можемо створити ендпоінт /api/users (додавши наступний маршрут до нашого файлу routes/api.php), який повертає користувачів у пагінованому форматі JSON:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('paginate', function () {
    return User::query()->paginate();
});

Відвідавши ендпоінт /api/users, ви отримаєте JSON-відповідь, подібну до наступної (зверніть увагу, що я обмежив поле data лише 3 запиcами для зрозумілості):

{
  "current_page": 1,
  "data": [
    {
      "id": 1,
      "name": "Andy Runolfsson",
      "email": "teresa.wiegand@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 2,
      "name": "Rafael Cummings",
      "email": "odessa54@example.org",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 3,
      "name": "Reynold Lindgren",
      "email": "juwan.johns@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    }
  ],
  "first_page_url": "http://example.com/users?page=1",
  "from": 1,
  "last_page": 4,
  "last_page_url": "http://example.com/users?page=4",
  "links": [
    {
      "url": null,
      "label": "« Попередня",
      "active": false
    },
    {
      "url": "http://example.com/users?page=1",
      "label": "1",
      "active": true
    },
    {
      "url": "http://example.com/users?page=2",
      "label": "2",
      "active": false
    },
    {
      "url": "http://example.com/users?page=3",
      "label": "3",
      "active": false
    },
    {
      "url": "http://example.com/users?page=4",
      "label": "4",
      "active": false
    },
    {
      "url": "http://example.com/users?page=5",
      "label": "5",
      "active": false
    },
    {
      "url": "http://example.com/users?page=2",
      "label": "Далі »",
      "active": false
    }
  ],
  "next_page_url": "http://example.com/users?page=2",
  "path": "http://example.com/users",
  "per_page": 15,
  "prev_page_url": null,
  "to": 15,
  "total": 50
}

Давайте розберемо JSON-відповідь:

# Основні SQL запити

Використання методу paginate в Laravel призводить до двох SQL-запитів:

Отже, якщо ми хотіли б отримати першу сторінку користувачів (з 15 користувачами на сторінку), будуть виконані наступні SQL-запити:

select count(*) as aggregate from `users`

і

select * from `users` limit 15 offset 0

У другому запиті ми бачимо, що значення limit встановлено на 15. Це кількість записів, що повертаються за сторінку.

Значення offset обчислюється так:

Offset = Розмір сторінки * (Сторінка - 1)

Тож, якщо ми хотіли б отримати третю сторінку користувачів, значення offset обчислювалося б так:

Offset = 15 * (3 - 1)

Таким чином, значення offset становитиме 30, і ми отримували б 31-ий до 45-го записи. Запити для третьої сторінки виглядатимуть так:

select count(*) as aggregate from `users`

і

select * from `users` limit 15 offset 30

# Використання методу simplePaginate

Метод simplePaginate дуже схожий на метод paginate, але з однією ключовою відмінністю: simplePaginate не отримує загальну кількість записів у наборі.

Як ми вже побачили, коли ми використовуємо метод paginate, ми також отримуємо інформацію про загальну кількість записів і сторінок, що є у наборі даних. Цю інформацію можна використовувати для відображення таких даних, як загальна кількість сторінок у UI або API-відповіді.

Але якщо ви не плануєте показувати ці деталі користувачу (або розробнику, що використовує API), ви можете уникнути зайвого запиту до бази даних (для підрахунку загальної кількості записів), використовуючи метод simplePaginate.

Метод simplePaginate можна використовувати так само, як і метод paginate:

use App\Models\User;
 
$users = User::query()->simplePaginate();

Запустивши цей код, ви отримаєте $users як екземпляр Illuminate\Contracts\Pagination\Paginator, зазвичай це об'єкт Illuminate\Pagination\Paginator.

На відміну від об'єкта Illuminate\Pagination\LengthAwarePaginator, повернутого методом paginate, об'єкт Illuminate\Pagination\Paginator не містить інформації про загальну кількість записів у наборі даних і не має поняття про кількість сторінок або загальних записів. Він просто знає про поточну сторінку даних і чи є ще записи для отримання.

# Використання simplePaginate з Blade-видом

Давайте розглянемо, як ви можете використовувати метод simplePaginate у Blade-виді. Припустимо, у нас є той самий маршрут, як і раніше, але цього разу ми використовуємо метод simplePaginate:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    $users = User::query()->simplePaginate();
 
    return view('users.index', [
        'users' => $users,
    ]);
});

Ми побудуємо наш Blade-вид так само, як і раніше:

<html>
<head>
    <title>Проста пагінація</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
 
<body>
    <div class="max-w-5xl mx-auto py-8">
        <h1 class="text-5xl">Проста пагінація</h1>
 
        <ul class="py-4">
            @foreach ($users as $user)
                <li class="py-1 border-b">{{ $user->name }}</li>
            @endforeach
        </ul>
 
        {{ $users->links() }}
    </div>
</body>
</html>

В результаті отримаємо сторінку, схожу на цю:

Як ми бачимо, вихід методу $users->links() відрізняється від виходу, який ми отримали за допомогою методу paginate. Оскільки метод simplePaginate не отримує загальну кількість записів, він не містить інформації про загальну кількість сторінок чи записів, лише про те, чи є наступна сторінка. Тому ми бачимо лише посилання "Попередня" та "Наступна" в пагінаційних посиланнях.

# Використання simplePaginate в API-ендпоінтах

Метод simplePaginate також можна використовувати в API-ендпоінтах. Laravel автоматично конвертує пагіновані дані в JSON.

Давайте створимо ендпоінт /api/users, який повертає пагінованих користувачів у JSON-форматі:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    return User::query()->simplePaginate();
});

Коли ми викличемо цей маршрут, отримаємо JSON-відповідь, схожу на цю (я обмежив поле data до 3 записів заради стислості):

{
  "current_page": 1,
  "data": [
    {
      "id": 1,
      "name": "Andy Runolfsson",
      "email": "teresa.wiegand@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 2,
      "name": "Rafael Cummings",
      "email": "odessa54@example.org",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 3,
      "name": "Reynold Lindgren",
      "email": "juwan.johns@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    }
  ],
  "first_page_url": "http://example.com/users?page=1",
  "from": 1,
  "next_page_url": "http://example.com/users?page=2",
  "path": "http://example.com/users",
  "per_page": 15,
  "prev_page_url": null,
  "to": 15
}

Ми бачимо, що JSON-відповідь подібна до тієї, яку ми отримували при використанні методу paginate. Основна відмінність полягає в тому, що тут немає полів last_page, last_page_url, links та total.

# Основні SQL запити

Розглянемо SQL-запити, які виконуються під час використання методу simplePaginate.

Метод simplePaginate також опирається на значення limit і offset для отримання підмножини даних із бази. Однак він не виконує запит для отримання загальної кількості записів у наборі.

Значення offset обчислюється так само, як і раніше:

Offset = Розмір сторінки * (Сторінка - 1)

Однак значення limit розраховується трохи інакше, ніж у методі paginate. Воно обчислюється так:

Limit = Розмір сторінки + 1

Це пов'язано з тим, що метод simplePaginate потребує отримати один запис більше, ніж значення perPage, щоб визначити, чи є ще записи для отримання. Припустимо, ми отримуємо 15 записів за сторінку. Значення limit становитиме 16. Якщо нам буде повернено 16 записів, ми зрозуміємо, що доступна принаймні ще одна сторінка. Якщо повернеться менше ніж 16 записів, це означає, що ми на останній сторінці даних.

Отже, якщо ми хотіли б отримати першу сторінку користувачів (з 15 користувачами на сторінку), виконуються наступні SQL-запити:

select * from `users` limit 16 offset 0

Запит для другої сторінки виглядатиме так:

select * from `users` limit 16 offset 15

# Використання методу cursorPaginate

До цього часу ми розглянули методи paginate і simplePaginate, обидва з яких використовують пагінацію на основі зсуву. Тепер ми розглянемо метод cursorPaginate, який використовує пагінацію на основі курсора.

Спочатку пагінація на основі курсора може здаватися дещо заплутаною, тому не турбуйтеся, якщо ви не зрозумієте це одразу. Сподіваюся, до кінця статті у вас буде краще розуміння, як це працює. Я також залишу чудове відео наприкінці статті, яке детально пояснює пагінацію на основі курсора.

Під час пагінації на основі зсуву ми використовуємо значення limit і offset для отримання підмножини даних з бази. Ми можемо сказати: "пропустити перші 10 записів та отримати наступні 10 записів". Це легко зрозуміти і реалізувати. У випадку курсорної пагінації ми використовуємо курсор (зазвичай унікальний ідентифікатор для конкретного запису в базі) як відправну точку для отримання попередніх/наступних наборів записів.

Наприклад, якщо ми виконуємо запит на отримання перших 15 користувачів, нехай ID 15-го користувача буде 20. Коли ми хочемо отримати наступних 15 користувачів, ми використаємо ID 15-го користувача (20) як курсор і скажемо: "отримати наступних 15 користувачів з ID більшим за 20".

Іноді ви можете бачити курсори, які називаються "токенами", "ключами", "наступними", "попередніми" тощо. Вони, по суті, є посиланнями на конкретний запис у базі даних. Ми розглянемо структуру курсорів пізніше в цій секції, коли аналізуватимемо основні SQL-запити.

Laravel дозволяє легко використовувати пагінацію на основі курсора за допомогою методу cursorPaginate:

use App\Models\User;
 
$users = User::query()->cursorPaginate();

Запустивши цей код, ви отримаєте $users як екземпляр Illuminate\Contracts\Pagination\CursorPaginator, зазвичай це об'єкт Illuminate\Pagination\CursorPaginator. Цей екземпляр пагінатора містить усю інформацію, що необхідна для відображення пагінованих даних у вашому додатку.

Як і в методі simplePaginate, метод cursorPaginate не отримує загальну кількість записів у наборі, а лише знає про поточну сторінку даних і чи є ще записи для отримання, тому ми не відразу знаємо загальну кількість сторінок або записів.

# Використання cursorPaginate з Blade-видом

Розглянемо, як використовувати метод cursorPaginate при виведенні даних у Blade-виді. Аналогічно попереднім прикладам, ми передбачаємо, що у нас є простий маршрут, який отримує користувачів з бази у пагинованому форматі та передає їх у вид:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    $users = User::query()->cursorPaginate();
 
    return view('users.index', [
        'users' => $users,
    ]);
});

Blade-вид може виглядати так:

<html>
<head>
    <title>Курсорна пагінація</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
 
<body>
    <div class="max-w-5xl mx-auto py-8">
        <h1 class="text-5xl">Курсорна пагінація</h1>
 
        <ul class="py-4">
            @foreach ($users as $user)
                <li class="py-1 border-b">{{ $user->name }}</li>
            @endforeach
        </ul>
 
        {{ $users->links() }}
    </div>
</body>
</html>

Це б створило сторінку, схожу на цю:

Як видно, оскільки метод cursorPaginate не отримує загальну кількість записів у наборі, вихід методу $users->links() схожий на вихід, який ми отримали із методу simplePaginate. Ми бачимо лише посилання "Попередня" та "Наступна".

# Використання cursorPaginate в API-ендпоінтах

Laravel також дозволяє використовувати метод cursorPaginate в API-ендпоінтах і автоматично конвертує пагіновані дані в JSON.

Створимо ендпоінт /api/users, що повертає пагінованих користувачів у JSON-форматі:

use App\Models\User;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    return User::query()->cursorPaginate();
});

Коли ми знову викликаємо цей маршрут, отримаємо JSON-відповідь, схожу на цю (я обмежив поле data лише 3 записами для зрозумілості):

{
  "data": [
    {
      "id": 1,
      "name": "Andy Runolfsson",
      "email": "teresa.wiegand@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 2,
      "name": "Rafael Cummings",
      "email": "odessa54@example.org",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 3,
      "name": "Reynold Lindgren",
      "email": "juwan.johns@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    }
  ],
  "path": "http://example.com/users",
  "per_page": 15,
  "next_cursor": "eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
  "next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
  "prev_cursor": null,
  "prev_page_url": null
}

Як бачимо, JSON-відповідь дуже схожа на попередні відповіді, але з деякими невеликими відмінностями. Оскільки ми не отримуємо загальну кількість записів, у відповіді немає полів last_page, last_page_url, links або total. Ви також могли помітити, що немає полів from і to.

Замість них ми маємо next_cursor та prev_cursor, що містять курсори для наступних та попередніх сторінок даних. Оскільки ми на першій сторінці, поля prev_cursor та prev_page_url обидва null. Проте поля next_cursor та next_page_url заповнені.

Поле next_cursor є рядком, закодованим за допомогою base-64, що містить курсор для наступної сторінки даних. Якщо ми декодуємо це поле, ми отримаємо приблизно таке (для читабельності):

{
  "users.id": 15,
  "_pointsToNextItems": true
}

Курсор містить дві окремі частини інформації:

Давайте подивимося, якою може бути друга сторінка даних (знову ж, скорочено до 3 записів для зручності):

{
  "data": [
    {
      "id": 16,
      "name": "Durward Nikolaus",
      "email": "xkuhic@example.com",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 17,
      "name": "Dr. Glenda Cruickshank IV",
      "email": "kristoffer.schiller@example.org",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    },
    {
      "id": 18,
      "name": "Prof. Dolores Predovic",
      "email": "frankie.schultz@example.net",
      "email_verified_at": "2024-10-15T23:19:28.000000Z",
      "created_at": "2024-10-15T23:19:29.000000Z",
      "updated_at": "2024-10-15T23:19:29.000000Z"
    }
  ],
  "path": "http://example.com/users",
  "per_page": 15,
  "next_cursor": "eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
  "next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
  "prev_cursor": "eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9",
  "prev_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9"
}

Ми бачимо, що поля prev_cursor та prev_page_url тепер заповнені, а next_cursor та next_page_url оновлені з курсором для наступної сторінки даних.

# Основні SQL запити

Щоб краще зрозуміти, як працює пагінація з курсором, давайте розглянемо основні SQL-запити, які виконуються під час використання методу cursorPaginate.

На першій сторінці даних (що містить 15 записів) буде виконано наступний SQL-запит:

select * from `users` order by `users`.`id` asc limit 16

Ми бачимо, що ми отримуємо перші 16 записів з таблиці users, упорядковані за стовпцем id у порядку зростання. Подібно до методу simplePaginate, ми отримуємо 16 рядків, оскільки ми хочемо визначити, чи є ще записи для отримання.

Уявімо, що ми потім переходимо на наступну сторінку елементів з наступним курсором:

eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0

Коли цей курсор декодується, ми отримуємо наступний JSON об'єкт:

{
  "users.id": 15,
  "_pointsToNextItems": true
}

Laravel потім виконає наступний SQL-запит, щоб отримати наступний набір записів:

select * from `users` where (`users`.`id` > 15) order by `users`.`id` asc limit 16

Як ми бачимо, ми отримуємо наступні 16 записів з таблиці users, у яких id більший за 15 (оскільки 15 був останнім ID на попередній сторінці).

Тепер уявімо, що ID першого користувача на другій сторінці — це 16. Коли ми переходимо назад до першої сторінки даних з другої, ми будемо використовувати наступний курсор:

eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9

Коли це декодується, ми отримаємо наступний JSON об'єкт:

{
  "users.id": 16,
  "_pointsToNextItems": false
}

Коли ми переходимо до наступної сторінки результатів, останній отриманий запис використовується як курсор. Коли ми повертаємося на попередню сторінку результатів, перший отриманий запис використовується як курсор. Тому видно, що значення users.id у курсорі встановлено на 16. Ми також бачимо, що значення _pointsToNextItems встановлено на false, оскільки ми переходимо назад до попереднього набору записів.

У результаті виконуватиметься наступний SQL-запит для отримання попереднього набору записів:

select * from `users` where (`users`.`id` < 16) order by `users`.`id` desc limit 16

Як видно, умова where тепер перевіряє записи з id менше 16 (оскільки 16 був першим ID на другій сторінці), а результати упорядковані у порядку спадання.

# Використання API-ресурсів з пагінацією

Дотепер у наших API прикладах ми просто повертали пагіновані дані безпосередньо з контролера. Однак у реальному додатку вам, ймовірно, знадобиться обробити дані перед їх поверненням користувачеві. Це може бути будь-що: від додавання або видалення полів, перетворення типів даних або навіть трансформації даних у зовсім інший формат. Ось чому ви, ймовірно, захочете використовувати API ресурси, оскільки вони дозволяють послідовно перетворювати ваші дані перед їх поверненням.

Laravel дозволяє використовувати API ресурси разом із пагінацією. Розглянемо приклад, як це зробити.

Уявіть, що ми створили клас API ресурсу App\Http\Resources\UserResource, який трансформує дані користувача перед поверненням. Він може виглядати приблизно так:

declare(strict_types=1);
 
namespace App\Http\Resources;
 
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
 
final class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }
}

У методі toArray ми вказуємо, що під час обробки користувача через цей ресурс, ми хочемо повернути лише поля id, name та email.

Тепер створимо простий API-ендпоінт /api/users у нашому файлі routes/api.php, що повертає пагінованих користувачів з використанням App\Http\Resources\UserResource:

use App\Models\User;
use App\Http\Resources\UserResource;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function () {
    $users = User::query()->paginate();
 
    return UserResource::collection(resource: $users);
});

У коді вище ми отримуємо одну сторінку користувачів (припустимо, це перша сторінка, що містить 15 користувачів) з бази. Потім ми передаємо поле $users (що буде екземпляром Illuminate\Pagination\LengthAwarePaginator) методу UserResource::collection. Цей метод трансформує пагіновані дані за допомогою App\Http\Resources\UserResource перед поверненням їх користувачу.

Коли ми відвідуємо /api/users ендпоінт, ми отримуємо JSON-відповідь, схожу на цю (я обмежив поле data до 3 записів заради стислості):

{
  "data": [
    {
      "id": 1,
      "name": "Andy Runolfsson",
      "email": "teresa.wiegand@example.net"
    },
    {
      "id": 2,
      "name": "Rafael Cummings",
      "email": "odessa54@example.org"
    },
    {
      "id": 3,
      "name": "Reynold Lindgren",
      "email": "juwan.johns@example.net"
    }
  ],
  "links": {
    "first": "http://example.com/users?page=1",
    "last": "http://example.com/users?page=4",
    "prev": null,
    "next": "http://example.com/users?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "links": [
      {
        "url": null,
        "label": "« Попередня",
        "active": false
      },
      {
        "url": "http://example.com/users?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": "http://example.com/users?page=2",
        "label": "2",
        "active": false
      },
      {
        "url": "http://example.com/users?page=3",
        "label": "3",
        "active": false
      },
      {
        "url": "http://example.com/users?page=4",
        "label": "4",
        "active": false
      },
      {
        "url": "http://example.com/users?page=2",
        "label": "Далі »",
        "active": false
      }
    ],
    "path": "http://example.com/users",
    "per_page": 15,
    "to": 15,
    "total": 50
  }
}

Як видно з JSON-наведень, Laravel визначає, що ми працюємо з пагінованим набором даних і повертає пагіновані дані в подібному форматі, як раніше. Однак цього разу користувачі в полі data містять лише поля id, name та email, які ми вказали в класі API ресурсу. Інші поля (current_page, from, last_page, links, path, per_page, to та total) все ще повертаються, оскільки вони є частиною пагінованих даних, але вони розміщені у полі meta. Також є поле links, що містить first, last, prev і next посилання на різні сторінки даних.

# Зміна кількості записів на сторінці

При побудові видів з пагінованими даними, ви, можливо, захочете дозволити користувачеві змінювати кількість записів, що відображаються на сторінці. Це може бути через випадаюче меню або поле вводу.

Laravel спрощує зміну кількості записів, що відображаються на сторінці, через передачу параметра perPage до методів simplePaginate, paginate і cursorPaginate. Цей параметр дозволяє вказати кількість записів, які ви хочете відображати за сторінку.

Розглянемо простий приклад, як прочитати параметр запиту per_page та використати його для зміни кількості записів, що отримуються за сторінку:

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
 
Route::get('users', function (Request $request) {
    $perPage = $request->integer('per_page', default: 10);
 
    return User::query()->paginate(perPage: $perPage);
});

У прикладі вище ми отримуємо значення параметра запиту per_page. Якщо значення не надано, ми за замовчуванням встановлюємо 10. Потім передаємо це значення до параметра perPage методу paginate.

В результаті ми можемо отримувати різні URL:

# Як визначити, який метод пагінації використовувати

Тепер, коли ми розглянули різні типи пагінації та як їх використовувати в Laravel, давайте обговоримо, як вирішити, який із підходів використовувати у вашому додатку.

# Потрібні чі вам номер сторінки або загальна кількість записів?

Якщо ви створюєте UI або API-ендпоінт, який вимагає відображати загальну кількість записів або сторінок, то метод paginate буде логічним вибором.

Якщо ви не потребуєте жодної з цих функцій, тоді simplePaginate або cursorPaginate будуть більш ефективними, оскільки вони не виконують непотрібні запити для підрахунку загальної кількості записів.

# Потрібно чі вам стрибати на конкретну сторінку?

Якщо вам потрібно мати можливість стрибати на конкретну сторінку даних, тоді пагінація на основі зсуву більше підходить. Оскільки курсорна пагінація є станом, вона покладається на попередню сторінку, щоб знати, куди йти далі. Тому стрибати на конкретну сторінку не так просто.

На відміну від цього, використовуючи пагінацію на основі зсуву, ви зазвичай просто передаєте номер сторінки в запиті (можливо, як параметр запиту) та стрибаєте на цю сторінку, не маючи жодного контексту попередньої сторінки.

# Який розмір набору даних?

Залежно від того, як бази даних обробляють значення offset, пагінація на основі зсуву стає менш ефективною, коли номер сторінки збільшується. Це пов'язано з тим, що при використанні зсуву база даних все ще повинна просканувати всі записи до значення зсуву. Вони просто скидаються і не повертаються в результати запиту.

Ось чудова стаття, що пояснює це більш детально: https://use-the-index-luke.com/no-offset.

Отже, зростаючи загальній кількості даних у базі, пагінація на основі зсуву може стати менш ефективною. У цих випадках пагінація на основі курсора є більш продуктивною, особливо якщо поле курсора індексоване, оскільки попередні записи не зчитуються. Через це, якщо ви плануєте використовувати пагінацію на великому наборі даних, можливо, ви захочете обрати пагінацію на основі курсора замість пагінації на основі зсуву.

# Чи змінюватиметься набір даних часто?

Пагінація на основі зсуву може зіткнутися з проблемами, якщо основний набір даних змінюється між запитами.

Розглянемо приклад.

Припустимо, у нас є 10 користувачів у базі даних:

Ми виконуємо запит, щоб отримати першу сторінку (на якій містяться 5 користувачів) і отримуємо таких користувачів:

Коли ми переходимо на сторінку 2, ми очікуємо отримати користувачів 6 до 10. Але уявімо, що перед завантаженням сторінки 2 (доки ми ще на сторінці 1) Користувач 1 було видалено з бази. Оскільки розмір сторінки 5, запит на отримання наступної сторінки виглядатиме ось так:

select * from `users` limit 5 offset 5

Це означає, що ми пропускаємо перші 5 записів і отримуємо наступні 5.

Це призведе до того, що на сторінці 2 ми побачимо таких користувачів:

Як ми бачимо, Користувач 6 відсутній зі списку. Це пов'язано з тим, що тепер Користувач 6 є 5-тим записом у таблиці, тому він фактично на першій сторінці.

Пагінація на основі курсора не має цієї проблеми, оскільки ми не пропускаємо записи, а просто отримуємо наступний набір записів на основі курсора. Уявімо, що ми використали пагінацію на основі курсора у наведеному вище прикладі. Курсор для сторінки 2 було б ID Користувача 5 (припустимо, це 5), оскільки це був останній запис на першій сторінці. Тому наш запит для сторінки 2 виглядатиме так:

select * from `users` where (`users`.`id` > 5) order by `users`.`id` asc limit 6

Виконання вказаного запиту поверне користувачів 6 до 10, як і очікувалося.

Це має підкреслити, як пагінація на основі зсуву може стати проблемою в разі зміни основних даних під час їх зчитування. Це стає менш передбачуваним і може призвести до непередбачуваних результатів.

# Чи підтримуєте ви створення API?

Важливо пам'ятати, що ви не зобов'язані використовувати один тип пагінації у вашому додатку. У деяких місцях пагінація на основі зсуву може бути більш доречною (можливо, для UI), а в інших — пагінація на основі курсора буде ефективнішою (наприклад, при роботі з великим набором даних). Тому ви можете комбінувати різні методи пагінації у своєму додатку залежно від конкретного випадку.

Однак, якщо ви створюєте API, я б настійно радив використовувати одну й ту ж саму концепцію пагінації для всіх ваших ендпоінтів. Це полегшить розробникам зрозуміти, як користуватися вашим API, і уникнути плутанини.

Не хочете, щоб розробникам доводилося пам'ятати, які ендпоінти використовують пагінацію на основі зсуву, а які — пагінацію на основі курсора.

Звісно, це не суворе правило. Якщо вам дійсно потрібно застосувати інший метод пагінації в одному конкретному ендпоінті, будь ласка. Але просто переконайтеся, що це чітко вказано в документації, щоб полегшити розуміння для розробників.

# Вибір між відео чи текстом?

Якщо ви більше вважаєте себе візуальним учнем, можливо, вам захочеться переглянути це чудове відео від Аарона Франсіса, яке детально пояснює різницю між пагінацією на основі зсуву та курсора:

# Висновок

У цій статті ми розглянули різні методи пагінації в Laravel та їх використання. Також ми проаналізували основні SQL-запити та розглянули, як вирішити, який метод пагінації вибрати у вашому додатку.

Сподіваюся, ви відчуєте себе більш впевненими у використанні пагінації у ваших Laravel-додатках.