У будь-якій адмінці і в більшості публічних розділів є списки: новини, теми, клієнти, рахунки, логи, медіа. Якщо список без пагінації — він швидко стає повільним і незручним. Якщо фільтри зроблені “на кнопках” без URL-параметрів — користувач не може поділитися посиланням, не може повернутися назад і втрачає контекст. Тому стандарт у Laravel простий: списки мають бути з пагінацією, а фільтри — через query string у URL.
Пагінація означає, що ти показуєш дані частинами: 20–50 записів на сторінку, а решту — на наступних сторінках. Це зменшує навантаження на базу, прискорює рендер і робить інтерфейс стабільним. У Laravel пагінація робиться на рівні запиту: ти не витягуєш “все”, а просиш базу одразу віддати сторінку.
Ключове правило: пагінація завжди має бути “останнім кроком” після всіх фільтрів. Тобто спочатку ти збираєш запит: пошук, статус, дата, автор, сортування. І тільки потім викликаєш paginate(). Якщо зробити навпаки або фільтрувати вже колекцію в PHP, ти або отримаєш неправильні результати, або вб’єш продуктивність.
Фільтри через query string — це коли параметри йдуть у URL, наприклад ?search=laravel&status=published&from=2025-01-01&page=2. Перевага така: URL стає “станом” сторінки. Ти можеш скопіювати посилання і відкрити рівно той самий список. Кнопки “назад/вперед” у браузері працюють логічно. І пагінація не губить умови, якщо ти правильно їх прокинув.
Як правильно будувати фільтри. Ти визначаєш набір дозволених параметрів: наприклад search, status, user_id, from, to, section, resource, sort. Далі береш їх з request, і для кожного параметра додаєш умову до query лише якщо параметр реально переданий. Це важливо: фільтр не має ламати запит, якщо користувач нічого не обрав.
Пошук search зазвичай робиться через LIKE по заголовку або опису. Тут треба бути дисциплінованим: пошук по великих текстових полях без індексів може бути дорогим. На старті це нормально, але в реальному продукті з часом або додають повнотекстовий пошук, або обмежують поля, або будують окремі рішення. Для навчання достатньо розуміти: пошук = додаткове навантаження, і його треба контролювати.
Фільтр по статусу — класика. Він повинен працювати через точне значення, а не через “якось”. Тобто status має бути обмежений списком дозволених станів, інакше ти отримаєш дивні запити і проблеми з безпекою в логіці. Важливо, щоб у базі був індекс по статусу, якщо це поле активно використовується у списках.
Фільтр по даті — ще одна типова річ, і тут головне не заплутатися, що саме фільтруємо: created_at чи published_at. У медіа-проєктах часто критично саме published_at, бо створили новину вчора, але опублікували сьогодні. Тому фільтр має явно пояснювати, по якій даті він працює, і мати правильні межі “від/до”.
Діапазон дат будується як дві умови: від певного дня і до певного дня. Важливо правильно включати “до кінця дня”, якщо ти фільтруєш по даті без часу. Інакше користувач вибирає “до 30 числа”, а записи 30-го після 00:00 можуть “зникати”. На практиці це вирішується нормалізацією меж: початок дня для from і кінець дня для to.
Фільтр по автору або користувачу (user_id) — ще один стандарт. Тут важливо не тягнути список користувачів “вічно”: ти або робиш пошук/селект, або даєш обмежений список. На рівні SQL це простий WHERE по user_id, але для продуктивності потрібен індекс. Для UI важливо, щоб після вибору автора параметр залишався в URL і не губився при переході по сторінках.
Сортування теж треба робити через query string, бо це частина стану списку. Наприклад sort=published_at і dir=desc. Мінімальний стандарт: дозволити сортування по 1–3 полях і жорстко обмежити список дозволених значень, щоб не підставляли довільні колонки. Це не лише про порядок, а й про безпеку та стабільність.
Переходимо до головної практичної проблеми: як зробити так, щоб пагінація не скидала фільтри. Якщо ти просто робиш paginate(50) і виводиш посилання, Laravel згенерує сторінки ?page=2, але без твоїх параметрів. Тому стандарт — “пришити” поточні параметри до пагінації. У Laravel це робиться через withQueryString() або через ручне appends().
Важливо зрозуміти логіку: URL параметри мають зберігатися на кожному кліку “наступна сторінка”. Якщо користувач відфільтрував список, він очікує, що сторінка 2 буде тим самим фільтром, а не “все підряд”. Тому withQueryString() — це не дрібниця, а обов’язковий стандарт для нормальних списків.
На рівні інтерфейсу фільтри мають бути формою з методом GET. Це простий і правильний підхід: користувач обирає параметри, натискає “Фільтрувати”, і сторінка відкривається з query string. GET важливий саме тим, що параметри видно в URL. Після цього ти у формі підставляєш поточні значення з request, щоб елементи не скидалися при перезавантаженні.
Для полів форми тут працює той самий принцип, що і для old(), але простіше: ти береш значення прямо з request('search'), request('status') тощо. Тобто фільтр “пам’ятає”, що було обрано, бо це в URL. Це робить сторінку предиктивною: користувач бачить, які умови активні.
Є ще одна деталь, яка економить нерви: коли користувач змінює фільтри, треба скидати сторінку на першу. Інакше він був на page=5, застосував новий фільтр і отримав порожньо, бо під фільтром існує лише 1–2 сторінки. Тому в UI або не передавай page при сабміті фільтра, або явно очищай його. Це дрібна річ, але вона зменшує “чому нічого не показує”.
З точки зору продуктивності головне правило таке: фільтри мають підтримуватися індексами. Якщо ти фільтруєш по published_at, status, user_id, то ці поля повинні бути індексовані, інакше на великій таблиці список стане повільним. Пагінація зменшує обсяг результату, але не прибирає проблему пошуку — база все одно має знайти потрібні рядки, і індекс тут вирішує.
Висновок: пагінація і query string-фільтри — це стандарт, який робить списки зручними і керованими. Правильний порядок завжди один: взяли параметри з URL, додали умови до query, застосували сортування, зробили paginate і обов’язково зберегли query string у посиланнях пагінації. Якщо ти освоїш цю схему, ти зможеш будувати будь-яку адмінську сторінку з фільтрами без хаосу, і вона не зламається, коли даних стане в рази більше.