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

К концу инструкции у вас будет работающий CRUD для сущности «Статья»: страница со списком записей, форма создания, форма редактирования и удаление. Всё на чистом Laravel 11 без Livewire, Filament и прочих надстроек — только маршруты, модель, миграция, контроллер и Blade-шаблоны. На всё уходит около получаса, если Laravel уже установлен.
CRUD — это четыре операции над данными: Create, Read, Update, Delete. В вебе они ложатся на HTTP-методы и маршруты, и Laravel умеет разложить их за вас одной командой.
Что нужно иметь до начала
Проверьте окружение заранее, чтобы не упереться в отсутствие расширения на пятом шаге:
- PHP 8.2 или новее — Laravel 11 не запустится на 8.1. Проверка:
php -v. - Composer — менеджер зависимостей PHP. Проверка:
composer --version. - Установленный проект Laravel 11. Если проекта нет:
composer create-project laravel/laravel crud-demo, затемcd crud-demo. - База данных. Проще всего SQLite: в новых проектах Laravel 11 она включена по умолчанию. Убедитесь, что в файле
.envстрокаDB_CONNECTION=sqlite, а файлdatabase/database.sqliteсуществует (если нет — создайте пустой файл с таким именем).
Запустите сервер разработки командой php artisan serve — по адресу http://127.0.0.1:8000 должна открыться стартовая страница Laravel. Если открылась, окружение готово.
Шаг 1. Создаём модель, миграцию и контроллер одной командой
Laravel умеет сгенерировать сразу три файла. Выполните:
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.
Шаг 2. Описываем поля в миграции
Откройте свежую миграцию в database/migrations/ и приведите метод up к такому виду:
public function up(): void
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title'); // заголовок
$table->text('body'); // текст статьи
$table->boolean('published')->default(false); // опубликована ли
$table->timestamps(); // created_at и updated_at
});
}Примените миграцию:
php artisan migrateПроверка: в консоли — строка вида DONE напротив миграции articles. В базе появилась таблица articles.
Шаг 3. Разрешаем массовое заполнение полей
Laravel по умолчанию защищает модели от массового присваивания. Чтобы create() и update() принимали данные из формы, укажите разрешённые поля в app/Models/Article.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
// поля, которые можно заполнять массово
protected $fillable = ['title', 'body', 'published'];
}Если пропустить этот шаг, при сохранении вы получите ошибку MassAssignmentException.
Шаг 4. Регистрируем маршруты
Откройте routes/web.php и добавьте одну строку — она создаёт все семь ресурсных маршрутов:
use App\Http\Controllers\ArticleController;
Route::resource('articles', ArticleController::class);Проверка: выполните php artisan route:list. В списке должны появиться маршруты articles.index, articles.create, articles.store, articles.edit, articles.update, articles.destroy, articles.show.
Шаг 5. Заполняем контроллер
Откройте app/Http/Controllers/ArticleController.php. Ресурсный контроллер уже содержит пустые методы — впишите в них логику:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
// Read: список всех статей
public function index()
{
$articles = Article::latest()->get();
return view('articles.index', compact('articles'));
}
// форма создания
public function create()
{
return view('articles.create');
}
// Create: сохранение новой статьи
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'published' => 'boolean',
]);
// чекбокс приходит только когда отмечен
$data['published'] = $request->boolean('published');
Article::create($data);
return redirect()->route('articles.index')
->with('status', 'Статья создана');
}
// форма редактирования
public function edit(Article $article)
{
return view('articles.edit', compact('article'));
}
// Update: сохранение изменений
public function update(Request $request, Article $article)
{
$data = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'published' => 'boolean',
]);
$data['published'] = $request->boolean('published');
$article->update($data);
return redirect()->route('articles.index')
->with('status', 'Статья обновлена');
}
// Delete: удаление
public function destroy(Article $article)
{
$article->delete();
return redirect()->route('articles.index')
->with('status', 'Статья удалена');
}
}Метод show в этой инструкции не используем, оставьте его пустым или удалите — на работу CRUD это не влияет.
Приём сArticle $articleв аргументах называется route model binding: Laravel по числовому{article}из URL сам находит запись в базе и подставляет объект. Если записи нет — отдаёт 404 без единой строки вашего кода.
Шаг 6. Создаём Blade-шаблоны
Создайте папку resources/views/articles и три файла.
index.blade.php — список
<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><title>Статьи</title></head>
<body>
@if (session('status'))
<p>{{ session('status') }}</p>
@endif
<h1>Статьи</h1>
<a href="{{ route('articles.create') }}">Добавить</a>
<ul>
@forelse ($articles as $article)
<li>
{{ $article->title }}
@if ($article->published) (опубликована) @endif
<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>
@empty
<li>Пока нет ни одной статьи</li>
@endforelse
</ul>
</body>
</html>create.blade.php — форма создания
<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><title>Новая статья</title></head>
<body>
<h1>Новая статья</h1>
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<form action="{{ route('articles.store') }}" method="POST">
@csrf
<p><input type="text" name="title" value="{{ old('title') }}" placeholder="Заголовок"></p>
<p><textarea name="body" placeholder="Текст">{{ old('body') }}</textarea></p>
<p><label><input type="checkbox" name="published" value="1"> Опубликовать</label></p>
<button type="submit">Сохранить</button>
</form>
</body>
</html>edit.blade.php — форма редактирования
<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><title>Редактирование</title></head>
<body>
<h1>Редактирование</h1>
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<form action="{{ route('articles.update', $article) }}" method="POST">
@csrf
@method('PUT')
<p><input type="text" name="title" value="{{ old('title', $article->title) }}"></p>
<p><textarea name="body">{{ old('body', $article->body) }}</textarea></p>
<p><label><input type="checkbox" name="published" value="1" @checked(old('published', $article->published))> Опубликовать</label></p>
<button type="submit">Сохранить</button>
</form>
</body>
</html>Проверка: откройте http://127.0.0.1:8000/articles. Увидите заголовок «Статьи» и ссылку «Добавить». Создайте запись — она появится в списке. Отредактируйте и удалите — список обновится, сверху покажется зелёное сообщение из session('status').
Что где живёт: маршруты и методы
Чтобы не путаться, какой маршрут за что отвечает:
| Действие | HTTP-метод | URL | Метод контроллера |
|---|---|---|---|
| Список | GET | /articles | index |
| Форма создания | GET | /articles/create | create |
| Сохранить новую | POST | /articles | store |
| Форма правки | GET | /articles/{id}/edit | edit |
| Сохранить правки | PUT/PATCH | /articles/{id} | update |
| Удалить | DELETE | /articles/{id} | destroy |
HTML-формы умеют только GET и POST, поэтому для PUT и DELETE в форме стоит @method('PUT') и @method('DELETE') — Laravel читает это скрытое поле и подменяет метод.
Если не получилось
- 419 | Page Expired при отправке формы. В форме нет
@csrf. Добавьте директиву сразу после открывающего тега<form>. - Add [title] to fillable property to allow mass assignment. Не заполнен
$fillableв модели — вернитесь к шагу 3. - SQLSTATE… no such table: articles. Миграция не применена. Выполните
php artisan migrate. Если база SQLite — проверьте, что файлdatabase/database.sqliteреально существует. - View [articles.index] not found. Файлы шаблонов лежат не в той папке. Путь должен быть точно
resources/views/articles/index.blade.php. - Удаление или правка не срабатывают, а сервер отдаёт 405 Method Not Allowed. В форме забыт
@method('DELETE')или@method('PUT')— HTML отправил обычный POST.
Что делать дальше
Рабочий CRUD есть, дальше стоит убрать грубые углы:
- Вынести повторяющуюся HTML-обвязку в общий макет через
@extendsи секции, чтобы не дублировать<head>в каждом шаблоне. - Перенести правила валидации из контроллера в отдельный класс:
php artisan make:request StoreArticleRequest. Тогдаstoreиupdateстанут короче. - Добавить пагинацию: замените
Article::latest()->get()наArticle::latest()->paginate(15)и выведите{{ $articles->links() }}. - Прикрыть раздел авторизацией через middleware
auth, если управлять статьями должны только вошедшие пользователи.
Связь статей с пользователями, загрузка изображений и мягкое удаление — отдельные темы, здесь мы собрали минимальный работающий каркас.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Laravel 11 — Controllers (Resource Controllers), Laravel 11 — Eloquent: Mass Assignment
Частые вопросы
У меня Laravel 10, а не 11 — инструкция подойдёт?
Можно ли обойтись без -mcr и создать всё вручную?
Форма создаётся, но при сохранении всё равно ошибка 419 — что не так?
Почему у меня чекбокс published всегда сохраняется как false?
Стоит ли выкладывать такой CRUD в продакшен как есть?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.