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

Модель Eloquent и чтение заявок

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

Таблица tickets описана, теперь свяжем её с моделью Eloquent и контроллером чтения. Результат урока — коллекция объектов из базы вместо массива маршрута. Учебные названия сохраняются, но источником становятся подготовленные строки. На этом переходном шаге чтение ещё открыто в локальной среде; вход и ограничение владельцем будут введены следующим уроком до любых действий записи.

Eloquent помогает описывать запросы через модель и получать объекты с полями таблицы. Он не создаёт базу сам по одному объявлению класса и не угадывает отсутствующую миграцию. Поэтому установленный Framework, применённая схема и учебные данные являются отдельными предпосылками. В нашей работе эти действия не выполнялись, а показанные строки и HTML остаются ожидаемыми результатами самостоятельной подготовки.

Модель и контроллер

Создадим полный app/Models/Ticket.php. Имя Ticket по соглашению связано с tickets. Разрешённые массовые поля ограничены title и body; владелец и статус позже назначаются серверной операцией явно.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Ticket extends Model
{
    protected $fillable = ['title', 'body'];
}

Ниже полный app/Http/Controllers/TicketController.php. Запрос имеет устойчивый порядок id по убыванию, а ограничение fifty строк защищает первый экран от неограниченного чтения. Постраничный просмотр появится позже.

<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\View\View;
class TicketController extends Controller
{
    public function index(): View
    {
        return view('tickets.index', ['tickets' => Ticket::query()->orderByDesc('id')->limit(50)->get()]);
    }
}

Руководство Eloquent описывает построение запроса и получение коллекции. Здесь вызов query создаёт builder, orderByDesc и limit уточняют его, get выполняет чтение и возвращает результат. Если остановиться до get и передать builder в шаблон, это уже другой объект и другой договор. Цепочка не является набором независимых немедленных запросов на каждой строке.

Маршрут теперь передаёт работу контроллеру. Это полный routes/web.php.

<?php
use App\Http\Controllers\TicketController;
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => redirect()->route('tickets.index'));
Route::get('/tickets', [TicketController::class, 'index'])->name('tickets.index');

Шаблон полностью заменяется на чтение свойств модели. Разметка и экранирование сохраняют прежнюю предметную задачу.

@extends('layouts.app')
@section('content')
  <h1>Заявки сотрудников</h1>
  <ul>
    @forelse($tickets as $ticket)
      <li><strong>{{ $ticket->title }}</strong>
        — {{ $ticket->status === 'open' ? 'Открыта' : 'Закрыта' }}
      </li>
    @empty
      <li>Учебных заявок пока нет.</li>
    @endforelse
  </ul>
@endsection

Начальные данные не являются схемой

В database/seeders/DatabaseSeeder.php сохраним локальное ограничение и пароль из окружения. Полная замена добавляет вымышленные обращения. user_id берётся из созданного пользователя, status устанавливается явно, а не проходит через массовый вход внешнего запроса.

<?php
namespace Database\Seeders;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        if (!app()->environment('local')) throw new \RuntimeException('Local seed only');
        $password = env('LAB_PASSWORD');
        if (!is_string($password) || strlen($password) < 12) throw new \RuntimeException('Set LAB_PASSWORD, min 12 bytes');
        $user = User::updateOrCreate(['email' => 'lab@example.test'], ['name' => 'Учебный сотрудник', 'password' => $password]);
        foreach (['Нужен доступ к макетам', 'Обновить описание проекта'] as $title) {
            if (!Ticket::query()->where('user_id', $user->id)->where('title', $title)->exists()) {
                $ticket = new Ticket(['title' => $title, 'body' => 'Вымышленное обращение для учебного портала.']);
                $ticket->user_id = $user->id;
                $ticket->status = 'open';
                $ticket->save();
            }
        }
    }
}

Seeder не является регистрацией рабочего аккаунта. Он предназначен для собственной local-базы и требует самостоятельно заданного пароля. Проверка exists уменьшает повторное создание одинаковых учебных строк, но не является универсальной конкурентной идемпотентностью. Здесь нет уникального индекса по user_id и title, потому что реальные одинаковые заголовки допустимы. Повторный seed также обновляет пароль local-пользователя согласно окружению.

Модель и массовое присваивание

fillable описывает разрешённые поля при массовом создании, а не полный список столбцов. status и user_id всё равно доступны для прямого серверного присваивания. Такое различие позволяет будущей форме передать только title и body, не получая возможность назначить владельца через лишнее поле HTTP. Но fillable не заменяет явный validated-вход: сервер также должен выбрать допустимые данные запроса.

Полезный неправильный случай — поставить guarded пустым и вызвать Ticket::create со всем request all. Даже если форма не показывает user_id, внешний клиент способен его отправить. База примет ссылку на существующего чужого пользователя, а кабинет потеряет свой договор владельца. В этом уроке нет внешней записи, но модель уже готовится к узкой границе следующего создания.

Чтение пока выбирает все локальные заявки. Это сознательная учебная ступень, а не приемлемая авторизация рабочего кабинета. Сервер первых шагов должен оставаться на localhost с вымышленными данными. Следующий урок добавит auth middleware и фильтр user_id из сессии. Не открывайте переходное состояние наружу только потому, что браузер показывает список без исключения.

Коллекция и ожидаемый порядок

После самостоятельного seed оба обращения открыты. Более поздний id располагается выше, поэтому порядок отличается от массива прошлого урока. Это ожидаемое следствие запроса, а не ошибка шаблона. Если нужен другой порядок, его задают builder, сохраняя стабильный дополнительный критерий при равных значениях. Порядок чтения базы без orderBy не следует считать гарантированным.

Ограничение fifty не сообщает общего числа заявок и не даёт перехода к остальным. Поэтому пока это первый ограниченный список, а не полноценная пагинация. При росте данных пользователь не должен получать обещание «все заявки», если часть строк не загружена. Позднейшая paginate согласует выборку, ссылки страниц и фильтр. Такая точность названия результата помогает развивать портал без скрытых изменений смысла.

Для самостоятельного разбора объясните путь title из строки базы до strong в HTML и отдельно путь user_id до связи с владельцем. Сейчас второй путь ещё не ограничивает чтение; следующим изменением это исправится. Сравните пустую таблицу, отсутствующую таблицу и наличие двух строк: это соответственно пустое представление, ошибка схемы и обычный результат, а не три варианта одного «пустого списка».

Прямое сохранение и служебные поля

new Ticket создаёт объект, а save записывает строку. Между ними сервер может назначить поля, которым нельзя доверять внешний ввод. Это объясняет, почему user_id отсутствует в fillable, но всё равно корректно сохраняется seeder. Массовое присваивание и прямое присваивание — разные пути, и разрешение одного поля в схеме не требует разрешать его в HTTP.

Seeder выполняется для local, потому что исходные данные вымышленные и пароль принадлежит учебному профилю. Если позже в базе появился реальный человек с тем же адресом, повтор updateOrCreate мог бы изменить его пароль. Ограничение среды и отдельный адрес уменьшают такой риск именно для нашего способа подготовки. Рабочие аккаунты следует создавать через самостоятельный проверенный процесс, а не переносить seeder из статьи на действующий портал.

Запрос list не получает body в отдельном DTO, поэтому модель содержит все столбцы, хотя Blade показывает только title и status. В серверном HTML это не выдаёт скрытые поля автоматически, но при переходе к JSON ситуация изменится: сериализация модели способна раскрыть больше данных. Поэтому будущий API получит явный Resource. Уже сейчас полезно отличать содержимое серверного объекта от содержания отправленного документа.

Ошибка подключения базы также не является пустой коллекцией. get может завершиться отказом до передачи view. Не ловите любое исключение и не подставляйте пустой массив только ради чистой страницы: это скроет неисправность источника под обычным состоянием отсутствия заявок. Сообщение и журнал должны сохранить различие причин.

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