База даних: пагінація
Вступ
В інших фреймворках пагінація буває справжньою мукою. Сподіваємося, підхід Laravel стане для вас ковтком свіжого повітря. Пагінатор Laravel інтегрований із конструктором запитів та Eloquent ORM і дає зручну, просту пагінацію записів бази даних без жодних налаштувань.
За замовчуванням HTML, який генерує пагінатор, сумісний із фреймворком Tailwind CSS; втім, доступна й підтримка пагінації Bootstrap.
Tailwind
Якщо ви користуєтеся стандартними представленнями пагінації Laravel для Tailwind разом із Tailwind 4.x, файл resources/css/app.css вашого застосунку вже буде правильно налаштований на @source для представлень пагінації Laravel:
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
Базове використання
Пагінація результатів конструктора запитів
Розбити елементи на сторінки можна кількома способами. Найпростіший - метод paginate конструктора запитів або запиту Eloquent. Метод paginate сам подбає про «limit» і «offset» запиту відповідно до сторінки, яку зараз переглядає користувач. За замовчуванням поточна сторінка визначається за значенням параметра page у рядку запиту HTTP-запиту. Laravel визначає це значення автоматично й так само автоматично підставляє його в посилання, які генерує пагінатор.
У цьому прикладі методу paginate передається єдиний аргумент - кількість елементів, які ви хочете показувати «на сторінці». Задаймо 15 елементів на сторінку:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Show all application users.
*/
public function index(): View
{
return view('user.index', [
'users' => DB::table('users')->paginate(15)
]);
}
}
Проста пагінація
Перш ніж дістати записи з бази даних, метод paginate рахує загальну кількість записів, що відповідають запиту. Це потрібно, щоб пагінатор знав, скільки всього сторінок. Але якщо ви не збираєтеся показувати загальну кількість сторінок в інтерфейсі застосунку, цей підрахунок зайвий.
Тож коли вам потрібні лише прості посилання «Далі» й «Назад», скористайтеся методом simplePaginate - він виконає один ефективний запит:
$users = DB::table('users')->simplePaginate(15);
Пагінація результатів Eloquent
Ви також можете розбивати на сторінки запити Eloquent. У цьому прикладі ми розіб'ємо на сторінки модель App\Models\User по 15 записів на сторінку. Як бачите, синтаксис майже не відрізняється від пагінації результатів конструктора запитів:
use App\Models\User;
$users = User::paginate(15);
Звісно, метод paginate можна викликати після того, як ви задали інші обмеження запиту - наприклад, умови where:
$users = User::where('votes', '>', 100)->paginate(15);
Для моделей Eloquent так само доступний метод simplePaginate:
$users = User::where('votes', '>', 100)->simplePaginate(15);
Аналогічно, для курсорної пагінації моделей Eloquent є метод cursorPaginate:
$users = User::where('votes', '>', 100)->cursorPaginate(15);
Кілька екземплярів пагінатора на сторінці
Іноді вам потрібно вивести два окремі пагінатори на одному екрані. Але якщо обидва екземпляри зберігають поточну сторінку в параметрі page рядка запиту, вони конфліктуватимуть. Щоб вирішити цей конфлікт, передайте назву параметра рядка запиту, у якому зберігатиметься поточна сторінка пагінатора, третім аргументом методів paginate, simplePaginate і cursorPaginate:
use App\Models\User;
$users = User::where('votes', '>', 100)->paginate(
$perPage = 15, $columns = ['*'], $pageName = 'users'
);
Курсорна пагінація
Якщо paginate і simplePaginate будують запити з SQL-виразом «offset», то курсорна пагінація створює умови «where», які порівнюють значення стовпців сортування із запиту. Це дає найкращу продуктивність бази даних з-поміж усіх методів пагінації Laravel. Такий спосіб особливо доречний для великих наборів даних і «нескінченного» прокручування в інтерфейсі.
На відміну від пагінації за зміщенням, яка додає номер сторінки до рядка запиту згенерованих URL, курсорна пагінація кладе в рядок запиту рядок-«курсор». Курсор - це закодований рядок, у якому міститься позиція, з якої наступний запит має продовжити пагінацію, і напрямок руху:
http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0
Створити екземпляр курсорного пагінатора можна методом cursorPaginate конструктора запитів. Він повертає екземпляр Illuminate\Pagination\CursorPaginator:
$users = DB::table('users')->orderBy('id')->cursorPaginate(15);
Отримавши екземпляр курсорного пагінатора, ви можете відобразити результати пагінації так само, як робите це з paginate і simplePaginate. Докладніше про методи екземпляра курсорного пагінатора читайте в документації з методів курсорного пагінатора.
Щоб скористатися курсорною пагінацією, ваш запит обов'язково має містити вираз «order by». Крім того, стовпці, за якими сортується запит, мають належати таблиці, яку ви розбиваєте на сторінки.
Курсорна пагінація проти пагінації за зміщенням
Щоб проілюструвати різницю між пагінацією за зміщенням і курсорною, погляньмо на приклади SQL-запитів. Обидва наведені нижче запити покажуть «другу сторінку» результатів таблиці users, відсортованої за id:
# Offset Pagination...
select * from users order by id asc limit 15 offset 15;
# Cursor Pagination...
select * from users where id > 15 order by id asc limit 15;
Запит із курсорною пагінацією має такі переваги перед пагінацією за зміщенням:
- На великих наборах даних курсорна пагінація працює швидше, якщо стовпці з «order by» проіндексовані. Причина в тому, що вираз «offset» сканує всі раніше відібрані дані.
- На даних із частими записами пагінація за зміщенням може пропускати записи чи показувати дублікати, якщо на сторінку, яку зараз переглядає користувач, нещодавно щось додали або з неї щось видалили.
Втім, курсорна пагінація має такі обмеження:
- Як і
simplePaginate, курсорна пагінація придатна лише для посилань «Далі» й «Назад» і не вміє генерувати посилання з номерами сторінок. - Вона вимагає, щоб сортування спиралося щонайменше на один унікальний стовпець або на комбінацію унікальних стовпців. Стовпці зі значеннями
nullне підтримуються. - Вирази запитів у «order by» підтримуються лише тоді, коли вони мають псевдонім і додані також до виразу «select».
- Вирази запитів із параметрами не підтримуються.
Створення пагінатора вручну
Іноді вам може знадобитися створити пагінатор вручну, передавши йому масив елементів, які вже є в пам'яті. Залежно від потреби, створіть екземпляр Illuminate\Pagination\Paginator, Illuminate\Pagination\LengthAwarePaginator або Illuminate\Pagination\CursorPaginator.
Класам Paginator і CursorPaginator не потрібно знати загальну кількість елементів у наборі результатів; саме тому вони й не мають методів для отримання індексу останньої сторінки. LengthAwarePaginator приймає майже ті самі аргументи, що й Paginator, але додатково вимагає загальну кількість елементів у наборі результатів.
Іншими словами, Paginator відповідає методу simplePaginate конструктора запитів, CursorPaginator - методу cursorPaginate, а LengthAwarePaginator - методу paginate.
Створюючи пагінатор вручну, ви маєте самі «нарізати» масив результатів, який передаєте пагінатору. Якщо не певні, як це зробити, погляньте на PHP-функцію array_slice.
Налаштування URL пагінації
За замовчуванням посилання, які генерує пагінатор, збігаються з URI поточного запиту. Втім, метод withPath дозволяє задати власний URI для генерації посилань. Наприклад, якщо ви хочете, щоб пагінатор генерував посилання на кшталт http://example.com/admin/users?page=N, передайте /admin/users до методу withPath:
use App\Models\User;
Route::get('/users', function () {
$users = User::paginate(15);
$users->withPath('/admin/users');
// ...
});
Додавання значень до рядка запиту
Ви можете додавати значення до рядка запиту посилань пагінації методом appends. Наприклад, щоб додати sort=votes до кожного посилання, викличте appends так:
use App\Models\User;
Route::get('/users', function () {
$users = User::paginate(15);
$users->appends(['sort' => 'votes']);
// ...
});
Якщо ви хочете додати до посилань пагінації всі значення рядка запиту з поточного HTTP-запиту, скористайтеся методом withQueryString:
$users = User::paginate(15)->withQueryString();
Додавання хеш-фрагментів
Якщо до згенерованих пагінатором URL потрібно додати «хеш-фрагмент», скористайтеся методом fragment. Наприклад, щоб додати #users у кінець кожного посилання пагінації, викличте метод fragment так:
$users = User::paginate(15)->fragment('users');
Відображення результатів пагінації
Метод paginate повертає екземпляр Illuminate\Pagination\LengthAwarePaginator, метод simplePaginate - екземпляр Illuminate\Pagination\Paginator, а метод cursorPaginate - екземпляр Illuminate\Pagination\CursorPaginator.
Ці об'єкти мають кілька методів, які описують набір результатів. Крім цих допоміжних методів, екземпляри пагінатора є ітераторами, тож їх можна обходити в циклі як масив. Отже, отримавши результати, ви можете вивести їх і відрендерити посилання на сторінки за допомогою Blade:
<div class="container">
@foreach ($users as $user)
{{ $user->name }}
@endforeach
</div>
{{ $users->links() }}
Метод links рендерить посилання на решту сторінок набору результатів. Кожне з цих посилань уже містить потрібну змінну page у рядку запиту. Пам'ятайте: HTML, який генерує метод links, сумісний із фреймворком Tailwind CSS.
Налаштування вікна посилань пагінації
Коли пагінатор виводить посилання, він показує номер поточної сторінки, а також посилання на три сторінки до та після неї. Методом onEachSide ви можете керувати тим, скільки додаткових посилань показувати з кожного боку від поточної сторінки в середньому рухомому вікні посилань:
{{ $users->onEachSide(5)->links() }}
Перетворення результатів на JSON
Класи пагінаторів Laravel реалізують контракт Illuminate\Contracts\Support\Jsonable і мають метод toJson, тож перетворити результати пагінації на JSON дуже просто. Ви також можете отримати JSON, просто повернувши екземпляр пагінатора з маршруту чи дії контролера:
use App\Models\User;
Route::get('/users', function () {
return User::paginate();
});
JSON від пагінатора міститиме метаінформацію: total, current_page, last_page та інше. Самі записи результатів доступні за ключем data. Ось приклад JSON, який утворюється, коли з маршруту повертають екземпляр пагінатора:
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"current_page_url": "http://laravel.app?page=1",
"first_page_url": "http://laravel.app?page=1",
"last_page_url": "http://laravel.app?page=4",
"next_page_url": "http://laravel.app?page=2",
"prev_page_url": null,
"path": "http://laravel.app",
"from": 1,
"to": 15,
"data":[
{
// Record...
},
{
// Record...
}
]
}
Налаштування представлення пагінації
За замовчуванням представлення, які рендерять посилання пагінації, сумісні з фреймворком Tailwind CSS. Втім, якщо ви не користуєтеся Tailwind, ви вільні описати власні представлення для цих посилань. Викликаючи метод links на екземплярі пагінатора, передайте назву представлення першим аргументом:
{{ $paginator->links('view.name') }}
<!-- Passing additional data to the view... -->
{{ $paginator->links('view.name', ['foo' => 'bar']) }}
Проте найпростіший спосіб налаштувати представлення пагінації - експортувати їх до каталогу resources/views/vendor командою vendor:publish:
php artisan vendor:publish --tag=laravel-pagination
Ця команда покладе представлення до каталогу resources/views/vendor/pagination вашого застосунку. Файл tailwind.blade.php у цьому каталозі відповідає представленню пагінації за замовчуванням. Відредагуйте цей файл, щоб змінити HTML пагінації.
Якщо ви хочете призначити представленням пагінації за замовчуванням інший файл, викличте методи defaultView і defaultSimpleView пагінатора в методі boot класу App\Providers\AppServiceProvider:
<?php
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Paginator::defaultView('view-name');
Paginator::defaultSimpleView('view-name');
}
}
Використання Bootstrap
Laravel містить представлення пагінації, побудовані на Bootstrap CSS. Щоб використати їх замість стандартних представлень Tailwind, викличте методи пагінатора useBootstrapFour або useBootstrapFive у методі boot класу App\Providers\AppServiceProvider:
use Illuminate\Pagination\Paginator;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Paginator::useBootstrapFive();
Paginator::useBootstrapFour();
}
Методи екземплярів Paginator / LengthAwarePaginator
Кожен екземпляр пагінатора надає додаткову інформацію про пагінацію через такі методи:
| Метод | Опис |
|---|---|
$paginator->count() |
Отримати кількість елементів на поточній сторінці. |
$paginator->currentPage() |
Отримати номер поточної сторінки. |
$paginator->firstItem() |
Отримати порядковий номер першого елемента в результатах. |
$paginator->getOptions() |
Отримати опції пагінатора. |
$paginator->getUrlRange($start, $end) |
Створити діапазон URL пагінації. |
$paginator->hasPages() |
Визначити, чи достатньо елементів, щоб розбити їх на кілька сторінок. |
$paginator->hasMorePages() |
Визначити, чи є ще елементи у сховищі даних. |
$paginator->items() |
Отримати елементи поточної сторінки. |
$paginator->lastItem() |
Отримати порядковий номер останнього елемента в результатах. |
$paginator->lastPage() |
Отримати номер останньої доступної сторінки. (Недоступний із simplePaginate). |
$paginator->nextPageUrl() |
Отримати URL наступної сторінки. |
$paginator->onFirstPage() |
Визначити, чи пагінатор перебуває на першій сторінці. |
$paginator->onLastPage() |
Визначити, чи пагінатор перебуває на останній сторінці. |
$paginator->perPage() |
Кількість елементів, які показуються на сторінці. |
$paginator->previousPageUrl() |
Отримати URL попередньої сторінки. |
$paginator->total() |
Визначити загальну кількість відповідних елементів у сховищі даних. (Недоступний із simplePaginate). |
$paginator->url($page) |
Отримати URL для заданого номера сторінки. |
$paginator->getPageName() |
Отримати змінну рядка запиту, у якій зберігається сторінка. |
$paginator->setPageName($name) |
Задати змінну рядка запиту, у якій зберігається сторінка. |
$paginator->through($callback) |
Перетворити кожен елемент за допомогою колбека. |
Методи екземпляра курсорного пагінатора
Кожен екземпляр курсорного пагінатора надає додаткову інформацію про пагінацію через такі методи:
| Метод | Опис |
|---|---|
$paginator->count() |
Отримати кількість елементів на поточній сторінці. |
$paginator->cursor() |
Отримати поточний екземпляр курсора. |
$paginator->getOptions() |
Отримати опції пагінатора. |
$paginator->hasPages() |
Визначити, чи достатньо елементів, щоб розбити їх на кілька сторінок. |
$paginator->hasMorePages() |
Визначити, чи є ще елементи у сховищі даних. |
$paginator->getCursorName() |
Отримати змінну рядка запиту, у якій зберігається курсор. |
$paginator->items() |
Отримати елементи поточної сторінки. |
$paginator->nextCursor() |
Отримати екземпляр курсора для наступного набору елементів. |
$paginator->nextPageUrl() |
Отримати URL наступної сторінки. |
$paginator->onFirstPage() |
Визначити, чи пагінатор перебуває на першій сторінці. |
$paginator->onLastPage() |
Визначити, чи пагінатор перебуває на останній сторінці. |
$paginator->perPage() |
Кількість елементів, які показуються на сторінці. |
$paginator->previousCursor() |
Отримати екземпляр курсора для попереднього набору елементів. |
$paginator->previousPageUrl() |
Отримати URL попередньої сторінки. |
$paginator->setCursorName() |
Задати змінну рядка запиту, у якій зберігається курсор. |
$paginator->url($cursor) |
Отримати URL для заданого екземпляра курсора. |