Перейти к содержанию

Фильтр и страницы списка заявок

Оглавление серии «Laravel 13 и PHP: портал «Заявка»»

Первый Eloquent-список читает не больше fifty строк и не позволяет перейти к остальным. Добавим фильтр open или closed и paginate. Результат урока — ограниченная страница личных заявок с устойчивым порядком и сохранением условий в ссылках. Каждая новая страница остаётся запросом того же владельца; параметры просмотра не превращаются в источник права доступа.

Фильтр является GET-формой, потому что меняет представление без записи. Человек может скопировать адрес с status и page либо вернуться к нему. Не используйте POST только ради формы с кнопкой: смысл операции по-прежнему чтение. CSRF нужен изменяющим операциям, а GET-фильтр не должен закрывать заявки или отправлять уведомления по самому факту перехода.

Условия перед пагинацией

Полностью заменим app/Http/Controllers/TicketController.php. Значение status проходит in, владельца берём из request user, порядок задаём id по убыванию. paginate получает twenty строк, withQueryString сохраняет параметры фильтра в навигации страниц.

<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\Http\Request;
use Illuminate\View\View;
class TicketController extends Controller
{
    public function index(Request $request): View
    {
        $filters = $request->validate(['status' => ['nullable', 'in:open,closed']]);
        $status = $filters['status'] ?? null;
        $tickets = Ticket::query()->where('user_id', $request->user()->id)
            ->when($status, fn ($query) => $query->where('status', $status))
            ->orderByDesc('id')->paginate(20)->withQueryString();
        return view('tickets.index', compact('tickets', 'status'));
    }
}

Документация пагинации Laravel описывает paginate и сохранение query string. Для нашего builder важно применить owner и status до paginate, чтобы количество и страницы относились к разрешённой выборке. Фильтрация уже полученной страницы в PHP дала бы неполное число и случайно пустые страницы при наличии подходящих заявок дальше.

Полностью заменим index.blade.php. Для навигации используем собственные ссылки previousPageUrl и nextPageUrl, чтобы серия не зависела от оформления CSS-фреймворка стандартного paginator. Это небольшая последовательная навигация, а не полный набор номеров всех страниц.

@extends('layouts.app')
@section('content')
<h1>Мои заявки</h1>
<a href="{{ route('tickets.create') }}">Новая заявка</a>
<form method="get" action="{{ route('tickets.index') }}">
  <label>Статус <select name="status">
    <option value="">Любой</option>
    <option value="open" @selected($status === 'open')>Открытые</option>
    <option value="closed" @selected($status === 'closed')>Закрытые</option>
  </select></label>
  @error('status')<p role="alert">{{ $message }}</p>@enderror
  <button type="submit">Показать</button>
</form>
<p>Подходящих заявок: {{ $tickets->total() }}.</p>
<ul>@forelse($tickets as $ticket)
  <li><a href="{{ route('tickets.show', $ticket) }}">{{ $ticket->title }}</a>
    — {{ $ticket->status === 'open' ? 'Открыта' : 'Закрыта' }}</li>
@empty<li>Заявок с этим условием нет.</li>@endforelse</ul>
<nav aria-label="Страницы заявок">
  @if($tickets->previousPageUrl())<a href="{{ $tickets->previousPageUrl() }}">Предыдущая</a>@endif
  <span>Страница {{ $tickets->currentPage() }} из {{ $tickets->lastPage() }}</span>
  @if($tickets->nextPageUrl())<a href="{{ $tickets->nextPageUrl() }}">Следующая</a>@endif
</nav>
@endsection

Ожидаемый адрес /tickets?status=open показывает только открытые заявки текущего пользователя. При наличии следующей страницы её ссылка сохраняет status=open. Выбор нового фильтра отправляет форму без старого page, поэтому начинается первая страница новой выборки. Если оставить page в скрытом поле, переход от большой коллекции к маленькой мог бы сразу открыть пустую далёкую страницу и создать ложное ощущение отсутствия записей.

Количество и состав страницы

total обозначает число записей всей подходящей выборки, а не длину текущей страницы. count коллекции paginator мог бы дать число показанных строк. Эти значения совпадают только на малом наборе, поэтому важно выбрать подпись до появления множества данных. В нашем шаблоне «Подходящих заявок» относится к total, а навигация обозначает номер страницы. Мы не называем двадцать видимых строк всей командой портала.

paginate обычно выполняет подсчёт общего количества вместе с выборкой страницы. Это удобно для lastPage, но на больших данных подсчёт может иметь отдельную стоимость. В текущей работе не измерялись запросы и планы, поэтому выбор метода объясняется результатом интерфейса. Если продукту достаточно следующей и предыдущей страницы без общего числа, simplePaginate может лучше соответствовать договору; тогда нельзя продолжать вызывать total и lastPage как раньше.

Cursor pagination решает другие свойства движения по большому упорядоченному набору. Ей нужен устойчивый договор ключа, а интерфейс номера страницы меняется. Мы не подменяем paginate cursor-методом без объяснения, потому что цель урока включает простые номера и общий счётчик. Расширение следует оценивать по требованиям чтения и поведению изменяющейся коллекции, а не по названию «быстрее».

Неизвестный фильтр

Полезный неправильный случай — status=all или произвольный текст. Правило in отклоняет его вместо передачи неизвестного машинного значения в SQL. Для обычного браузерного запроса ошибка проверки может вернуться на предыдущий адрес; полноценный продукт должен показать понятное сообщение фильтра. В текущем шаблоне можно добавить @error status рядом с select, и итоговый архивный файл содержит такой вывод. Игнорировать причину отказа и показывать пустую коллекцию было бы неверно.

Фильтр не меняет статус заявки. Название «Закрытые» описывает запрос, а не команду close. Это полезно проверить при чтении маршрутов: GET /tickets имеет только index, POST /tickets/{ticket}/close имеет отдельный handler и CSRF. Если в index поставить «для удобства» фоновое закрытие просроченных обращений, переход по ссылке неожиданно станет записью и нарушит назначение GET.

where user_id присутствует независимо от status и page. Даже если client отправит user_id другого сотрудника в query, контроллер его не использует. withQueryString может сохранить лишний query параметр в ссылке, но он не становится разрешённым условием builder. Для более строгого внешнего адреса можно использовать appends только известных фильтров. Эта разница относится к чистоте URL и договору API, а не к изменению источника владельца.

Изменяющийся список

Offset-страницы не создают неподвижный снимок всего списка между запросами. Если новая заявка появилась наверху, состав следующей страницы может сместиться. Для учебного кабинета это допустимо, но не следует обещать, что просмотр не повторит и не пропустит строку при одновременной записи. Для строгого обхода понадобятся курсор или момент снимка с отдельным договором. Устойчивый order id уменьшает неопределённость, но не замораживает базу.

После закрытия карточка возвращается на себя, а список открытых при следующем чтении больше не включает эту заявку. Она остаётся в закрытых и в любом общем фильтре. Это демонстрирует, что close не удаляет историю. Если пользователь находится на далёкой странице и записи исчезают из фильтра, пустая страница возможна; интерфейс может предложить первую или предыдущую, сохранив условия.

Перед следующей главой сравните лимит первых fifty строк с paginate twenty. Новое поведение добавляет договор количества и движения, а не только меняет число в SQL. Самостоятельно опишите адрес первой открытой страницы, её следующую ссылку и новый адрес после выбора закрытых. Затем перейдём к очереди: завершённая заявка получит отдельную фоновую подготовку квитанции после фиксации транзакции.

Адрес страницы и выбранный критерий

Номер page относится к конкретному фильтру. Страница three открытых заявок не означает ту же группу записей, что страница three закрытых. Поэтому GET-форма при смене status начинает новый просмотр без старого page. Скопированная ссылка, напротив, сохраняет оба значения и обозначает конкретное представление. Различайте эти действия, когда добавляете дополнительные условия: новое намерение пользователя и повторное открытие готового адреса могут иметь разные правила восстановления страницы.

Для самостоятельного воспроизведения результата «Ограничить выборку и сохранить параметры фильтра» выберите lesson-11 в архиве исходников. Статус выполнения этого состояния: not_run.