Миграция таблицы заявок
Оглавление серии «Laravel 13 и PHP: портал «Заявка»»
Массив в маршруте удобен для представления, но обращение сотрудника должно переживать новый запрос и изменение программы. Зафиксируем таблицу tickets миграцией. Результат урока — воспроизводимое описание схемы: название, текст, статус, владелец и служебное время. Миграция ещё не является выполненным SQL в нашей среде; она задаёт то изменение базы, которое читатель применит самостоятельно к собственной учебной установке.
Портал продолжает использовать users из официального каркаса. Таблица tickets ссылается на пользователя, поэтому миграция users должна предшествовать новой миграции. Архив является overlay на описанный каркас, а не пустой заменой всех его файлов. Не удаляйте базовые migrations ради того, чтобы оставить только показанный в статье короткий файл.
Структура одной заявки
Создадим полный database/migrations/2026_10_11_000100_create_tickets_table.php. Имя файла задаёт порядок применения после стандартной базы. В статусе по умолчанию хранится open. Для SQLite учебной среды строковый статус проверяют операции приложения; сама эта декларация не создаёт ограничение CHECK на два значения.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->restrictOnDelete();
$table->string('title', 120);
$table->text('body');
$table->string('status', 16)->default('open');
$table->timestamps();
$table->index(['user_id', 'status', 'id']);
});
}
public function down(): void { Schema::dropIfExists('tickets'); }
};
Документация миграций Laravel описывает Schema, Blueprint и обратное действие down. В нашем проекте эти механизмы выражают предметный договор: заявка всегда относится к существующему пользователю, её заголовок ограничен, а удаление владельца не должно незаметно уничтожить историю обращений. Поэтому chosen restrictOnDelete запрещает такую потерю через внешний ключ.
id является технической личностью записи. Заголовки могут повторяться и меняться, поэтому не подходят в качестве внешнего ключа будущего комментария. Время created_at позволит показать порядок возникновения, а updated_at — факт изменения текущей строки. Эти поля не заменяют историю переходов статуса: если нужно знать, кто закрыл заявку, это следует сохранить отдельной записью, а не восстанавливать по последнему timestamp.
Внешний ключ и удаление
user_id с constrained ссылается на users.id по принятому соглашению. Если таблица владельцев названа иначе, имя требуется указать явно. Соглашение сокращает код, но не избавляет автора от знания реальной структуры. Ошибка в порядке миграций или в названии связанной таблицы проявится на уровне схемы, а не при отрисовке Blade. Проверять её следует по определению отношения и существующим файлам миграций.
Ограничение restrictOnDelete выбрано сознательно. Альтернатива cascadeOnDelete могла бы удалить все заявки вместе с пользователем, а nullable с nullOnDelete позволила бы истории потерять прямого владельца. Для нашего кабинета ни одна из этих альтернатив пока не соответствует результату серии. Если продукт потребует удалять персональные данные, понадобится отдельное правило архивирования и обезличивания; нельзя считать внешний ключ полной политикой хранения.
Сама таблица не проверяет, что текущий HTTP-пользователь равен user_id. Внешний ключ защищает целостность ссылки, а авторизация защищает право операции. Путаница этих механизмов опасна: запись с существующим чужим user_id корректна для базы и одновременно может нарушать права кабинета. Позднее владелец будет назначаться из сессии на сервере, а не приниматься из поля формы.
Индекс под будущий список
Составной индекс начинается с user_id, потому что личный список сначала ограничивает владельца. Следующие status и id соответствуют будущему фильтру и порядку. Это описанный выбор под конкретный запрос, а не измеренное ускорение. В этой работе база и планы выполнения не запускались, поэтому нельзя приписывать индексу определённые миллисекунды или процент улучшения.
Индекс также имеет стоимость записи и хранения. Добавлять отдельный индекс на каждое поле только потому, что оно существует, не нужно. title в текущем результате не используется для серверного полнотекстового поиска. Если такой поиск появится, обычный составной индекс не станет его автоматической реализацией. Сначала уточняют запрос и базу, затем выбирают подходящую структуру, проверяя план на собственных данных.
Текст body отделён от title: заголовок нужен для списка, описание — для подробной карточки. Это позволяет не выдавать весь текст в будущий короткий API-ответ и не заставляет пользователя помещать длинное объяснение в имя заявки. Размер столбца title в разных базах не обязательно равен пользовательскому правилу проверки формы. Поэтому Form Request отдельно ограничит длину входа, не полагаясь только на возможное поведение SQL.
Изменения схемы после первого выпуска
Если таблица ещё не применена в собственной пустой базе, файл можно редактировать в рамках разработки. Но после выпуска изменение уже применённой миграции не заставит существующую установку выполнить её заново. Для нового поля требуется новая миграция. Это правило важно для дальнейшего attachment_path и квитанции: серия добавит новые файлы изменений, сохраняя историю предыдущей схемы.
down здесь удаляет таблицу и её данные. Не представляйте rollback как универсально безопасную отмену рабочей операции. Для учебной пустой базы это обратная структура; для заполненного портала потребуется резервная копия и план восстановления. Отдельная миграция может быть обратимой по форме и всё равно необратимой для пользовательского содержимого. Последняя глава различит выпуск, откат кода и восстановление данных.
Полезный неправильный случай — хранить статус как русскую подпись «Открыта», а затем сравнивать его с open в Blade. Интерфейс покажет неверную ветку. Машинное значение и отображаемая подпись должны быть согласованы, но не смешаны. Выбранные open и closed останутся такими во всех операциях, JSON и истории серии. Перевод страницы сможет измениться отдельно от записи.
Ожидаемая база следующего шага
После самостоятельного применения миграции таблица tickets должна иметь внешний ключ users, поля title, body, status и timestamps. Она ещё может быть пустой. Никаких двух заявок из массива прошлого урока миграция не переносит автоматически, потому что структура и начальные данные — разные действия. Seeder следующего состояния создаст вымышленные записи для локального пользователя.
Перед продолжением сопоставьте каждое поле с вопросом кабинета: кто владелец, как выбрать карточку, что показать в списке, где хранить описание, какой статус допустим. Если поле не отвечает нужному вопросу, не добавляйте его «на будущее» без договора. Следующий урок создаст модель Eloquent и чтение подготовленных строк, сохранив различие между схемой базы, объектом приложения и HTML-представлением.
Для самостоятельного воспроизведения результата «Зафиксировать схему заявки и связь с пользователем» выберите lesson-03 в архиве исходников. Статус выполнения этого состояния: not_run.