CRUD за полчаса в Laravel 11: ошибки новичков и как их обойти
Пошаговый разбор: как собрать полноценный CRUD статей на Laravel 11 за полчаса и не наступить на грабли, из-за которых на пятом шаге всё падает с ошибкой 500 или 419.

За тридцать минут вы получите работающий раздел управления статьями: список, создание, редактирование и удаление записей — то самое CRUD (Create, Read, Update, Delete). Всё на Laravel 11 с базой SQLite, чтобы не настраивать MySQL. В конце будут страницы, которые открываются в браузере, форма с валидацией и данные, которые реально сохраняются в базу. Заодно разберём четыре места, где новички теряют время: забытая миграция, пустой $fillable, отсутствие CSRF-токена в форме и неправильный порядок маршрутов.
Что нужно иметь до начала
Проверьте это до первой команды, иначе застрянете в середине.
- PHP 8.2 или новее — Laravel 11 не запустится на 8.1. Проверка:
php -v. - Composer установлен глобально. Проверка:
composer --version. - Расширение PHP для SQLite включено. Обычно оно есть по умолчанию; если нет — в
php.iniдолжна быть раскомментирована строкаextension=pdo_sqlite. - Node.js не обязателен: для CRUD с обычными Blade-шаблонами фронтенд-сборка не нужна.
Шаг 1. Создаём проект
Одной командой ставим свежий проект и переходим в папку:
composer create-project laravel/laravel crud-demo
cd crud-demo
Проверка: команда php artisan --version покажет что-то вроде Laravel Framework 11.x. Если версия начинается с 10 или ниже — статья тоже подойдёт, но пути к файлам могут отличаться.
Шаг 2. Настраиваем базу SQLite
В файле .env найдите блок с настройками базы и приведите его к такому виду (лишние строки MySQL удалите или закомментируйте):
DB_CONNECTION=sqlite
Создайте пустой файл базы:
touch database/database.sqlite
На Windows без touch создайте пустой файл database.sqlite в папке database вручную. Проверка: файл существует и весит 0 байт.
Шаг 3. Генерируем модель, миграцию и контроллер разом
Artisan умеет создать всё за одну команду. Флаги важны:
php artisan make:model Article -mcr
Расшифровка флагов: -m создаёт миграцию, -c — контроллер, -r делает контроллер ресурсным (сразу с методами index, create, store, show, edit, update, destroy). Проверка: в консоли появятся строки о созданных файлах app/Models/Article.php, database/migrations/..._create_articles_table.php и app/Http/Controllers/ArticleController.php.
Шаг 4. Описываем таблицу в миграции
Откройте файл миграции в database/migrations/ (имя оканчивается на _create_articles_table.php) и добавьте два поля внутрь метода up:
public function up(): void
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title'); // заголовок
$table->text('body'); // текст статьи
$table->timestamps();
});
}
Запустите миграцию:
php artisan migrate
Проверка: в консоли появится строка вида DONE напротив имени миграции. Если спросит про создание базы — отвечайте yes.
Забытьphp artisan migrate— ошибка номер один. Код работает, маршруты есть, а при открытии страницы летитSQLSTATE... no such table: articles. Таблицы просто нет, потому что миграцию описали, но не выполнили.
Шаг 5. Разрешаем массовое заполнение в модели
Откройте app/Models/Article.php и добавьте свойство $fillable. Без него метод Article::create() выбросит исключение MassAssignmentException:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
// поля, которые можно заполнять массово через create() и fill()
protected $fillable = ['title', 'body'];
}
Проверка: никакой видимой — но без этой строки шаг сохранения упадёт.
Шаг 6. Регистрируем маршруты
В файле routes/web.php одна строка создаёт сразу семь маршрутов для всех операций CRUD:
use App\Http\Controllers\ArticleController;
Route::resource('articles', ArticleController::class);
Проверка: команда php artisan route:list покажет список из семи строк с URI вроде articles, articles/create, articles/{article}.
Где здесь ловушка порядка
Если вы позже добавите свой маршрут вручную, ставьте статические пути выше параметрических. Строка Route::get('articles/export', ...) должна идти до Route::resource, иначе Laravel примет export за значение {article} и уйдёт не в тот метод.
Шаг 7. Заполняем контроллер
Откройте app/Http/Controllers/ArticleController.php. Ресурсный контроллер уже содержит пустые методы — впишите в них логику:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
// список всех статей
public function index()
{
$articles = Article::latest()->get();
return view('articles.index', compact('articles'));
}
// форма создания
public function create()
{
return view('articles.create');
}
// сохранение новой статьи
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
Article::create($data);
return redirect()->route('articles.index');
}
// форма редактирования
public function edit(Article $article)
{
return view('articles.edit', compact('article'));
}
// обновление
public function update(Request $request, Article $article)
{
$data = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
$article->update($data);
return redirect()->route('articles.index');
}
// удаление
public function destroy(Article $article)
{
$article->delete();
return redirect()->route('articles.index');
}
}
Аргумент Article $article в методах — это route model binding: Laravel сам находит запись по id из URL и передаёт её объектом. Показ отдельной статьи (show) для простоты опустили — при желании добавите по аналогии.
Шаг 8. Три Blade-шаблона
Создайте папку resources/views/articles и три файла в ней.
index.blade.php — список
<h1>Статьи</h1>
<a href="{{ route('articles.create') }}">Новая статья</a>
<ul>
@foreach ($articles as $article)
<li>
{{ $article->title }}
<a href="{{ route('articles.edit', $article) }}">Изменить</a>
<form action="{{ route('articles.destroy', $article) }}" method="POST" style="display:inline">
@csrf
@method('DELETE')
<button type="submit">Удалить</button>
</form>
</li>
@endforeach
</ul>
create.blade.php — форма создания
<h1>Новая статья</h1>
<form action="{{ route('articles.store') }}" method="POST">
@csrf
<input type="text" name="title" placeholder="Заголовок">
<textarea name="body" placeholder="Текст"></textarea>
<button type="submit">Сохранить</button>
</form>
edit.blade.php — форма редактирования
<h1>Редактировать</h1>
<form action="{{ route('articles.update', $article) }}" method="POST">
@csrf
@method('PUT')
<input type="text" name="title" value="{{ $article->title }}">
<textarea name="body">{{ $article->body }}</textarea>
<button type="submit">Обновить</button>
</form>
Проверка: запустите php artisan serve и откройте http://127.0.0.1:8000/articles. Появится заголовок «Статьи» и ссылка «Новая статья». Создайте запись — она добавится в список.
Директива@csrfв каждой форме и@method('PUT')/@method('DELETE')для обновления и удаления — не украшение. HTML-формы умеют только GET и POST, а Laravel ждёт PUT и DELETE. Без скрытого поля метода запрос уйдёт как POST и не попадёт в нужный маршрут.
Если не получилось
Пять ошибок, которые видят почти все новички.
SQLSTATE[HY000]: no such table: articles— не выполнена миграция. Запуститеphp artisan migrate. Если таблица частично создалась и ломается —php artisan migrate:freshпересоздаст всё с нуля (данные сотрутся).Add [title] to fillable property to allow mass assignment— пустой или отсутствующий$fillableв модели. Вернитесь к шагу 5.- Ошибка 419 Page Expired при отправке формы — забыли
@csrf. Добавьте директиву внутрь каждого<form>. - Кнопка «Удалить» открывает статью вместо удаления — удаление сделали ссылкой
<a>, а нужна форма с методом DELETE. Ссылки всегда шлют GET. Route [articles.index] not defined— не зарегистрированRoute::resourceлибо опечатка в имени. Сверьтесь черезphp artisan route:list.
Что делать дальше
Рабочий CRUD за полчаса — это скелет. Дальше по одному шагу за раз:
- Вынесите правила валидации в отдельный класс через
php artisan make:request StoreArticleRequest— контроллер станет чище. - Добавьте показ ошибок валидации в шаблонах через переменную
$errorsи старые значения через хелперold(), иначе после ошибки форма очистится. - Настройте пагинацию: замените
Article::latest()->get()на->paginate(15), чтобы список не тянул тысячи строк. - Авторизация и разграничение прав (кто может удалять) — отдельная тема, здесь мы её не трогали.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Laravel 11 — Controllers (Resource Controllers), Laravel 11 — Eloquent: Mass Assignment
Частые вопросы
А если у меня Laravel 10, а не 11?
Обязательно ли использовать SQLite?
Почему форма удаления падает с ошибкой 419?
Не сохраняются данные, хотя ошибок нет — что смотреть?
Можно ли сгенерировать готовые шаблоны, а не писать вручную?
Как удалить всё и начать заново, если запутался в миграциях?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.