Laravel: Девять типичных ошибок новичков

Источник: «Laravel: 9 Typical Mistakes Juniors Make»
Некоторое время назад я сделал серию на Youtube под названием Code Reviews. Из этой серии и других обзоров я собрал наиболее распространённые ошибки допускаемые в Laravel новичками.

Не все из них действительно серьёзные недостатки, большинство просто не самые эффективные способы написания кода. Тогда остаётся открытым вопрос, зачем использовать такой фреймворк, как Laravel, и не использовать его основные возможности в полной мере?

Итак, в произвольном порядке…

Ошибка 1. Не использовать группы маршрутов

По возможности объединяйте маршруты в группы. Например, есть такие маршруты:

Route::get('dashboard', [HomeController::class, 'index'])->name('dashboard')->middleware(['auth']);
Route::resource('donation', DonationController::class)->middleware(['auth']);
Route::resource('requisition', RequisitionController::class)->middleware(['auth']);

Route::name('admin.')->prefix('admin.')->group(function () {
Route::view('/', 'admin.welcome')->middleware(['auth', 'admincheck']);
Route::resource('donor', DonorController::class)->middleware(['auth', 'admincheck']);
Route::resource('details', OrganisationDetailController::class)->middleware(['auth', 'admincheck']);
});

Все маршруты с middleware auth и три маршрута, также проверяющие является ли пользователь admin. Все эти middleware повторяются каждый раз.

Было бы лучше поместить все маршруты в группу для проверки middleware auth, а затем внутри создать ещё одну группу для маршрутов admin.

Таким образом, когда разработчик откроет файл маршрутов, он сразу узнает, какие маршруты предназначены только для аутентифицированных пользователей.

Route::middleware('auth')->group(function () {
Route::get('dashboard', [HomeController::class, 'index'])->name('dashboard');
Route::resource('donation', DonationController::class);
Route::resource('requisition', RequisitionController::class);

Route::name('admin.')->prefix('admin.')->middleware('admincheck')->group(function () {
Route::view('/', 'admin.welcome');
Route::resource('donor', DonorController::class);
Route::resource('details', OrganisationDetailController::class);
});
});

Ошибка 2. Не использовать привязку модели маршрута

Часто вижу, как начинающие кодеры ищут вручную в контроллере данные, даже если в маршрутах привязка модели указана корректно. Например, в маршрутах у вас есть:

Route::resource('student', StudentController::class);

Так что здесь это даже ресурсный маршрут. Но я вижу, что некоторые новички всё ещё пишут код контроллера следующим образом:

public function show($id)
{
$student = Student::findOrFail($id);

return view('dashboard/student/show', compact(['student']));
}

Вместо этого используйте привязку модели маршрута, и Laravel найдёт модель:

public function show(Student $student)
{
return view('dashboard/student/show', compact(['student']));
}

Ошибка 3. Слишком длинный код Eloquent Create/Update

При сохранении данных в БД я видел, как пишут код, похожий на этот:

public function update(Request $request)
{
$request->validate(['name' => 'required']);
$user = Auth::user();
$user->name = $request->name;
$user->username = $request->username;
$user->mobile = $request->mobile;
// Другие поля...
$user->save();
return redirect()->route('profile.index');
}

Вместо этого, его можно написать короче, по крайней мере, двумя способами.

Во-первых, в этом примере не нужно устанавливать Auth::user() в переменную $user. Первый вариант может быть:

public function update(Request $request)
{
$request->validate(['name' => 'required']);
auth()->user()->update([$request->only([
'name',
'username',
'mobile',
// Другие поля...
]);
return redirect()->route('profile.index');
}

Во втором варианте помещаем проверку в Form Request. Затем в метод update() нужно будет просто передать $request->validated().

public function update(ProfileRequest $request)
{
auth()->user()->update([$request->validated());
return redirect()->route('profile.index');
}

Видите, насколько короче код?

Ошибка 4. Неправильное именование

Часто новички дают имена как хотят, не думая о других разработчиках, которые будут читать их код.

Например, сокращение имён переменных: вместо $data они называют $d. Всегда используйте правильное именование. Например:

Route::get('/', [IndexController::class, 'show'])
->middleware(['dq'])
->name('index');
Route::get('/about', [IndexController::class, 'about'])
->middleware(['dq'])
->name('about');
Route::get('/dq', [IndexController::class, 'dq'])
->middleware(['auth'])
->name('dq');

Что это за middleware и метод в IndexController, называемые dq? Ну, в этом примере, если бы мы зашли в app/Http/Kernel.php, чтобы найти это middleware, то могли обнаружить что-то вроде этого:

class Kernel extends HttpKernel
{
// ...
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'admin' => \App\Http\Middleware\EnsureAdmin::class,
'dq' => \App\Http\Middleware\Disqualified::class,
'inprogress' => \App\Http\Middleware\InProgress::class,
// ...
];
}

Неважно, что внутри middleware, но название файла Disqualified. Так что везде вместо dq его надо называть disqualified. Таким образом, если к проекту присоединятся другие разработчики, им будет понятнее.

Ошибка 5. Слишком большие контроллеры

Довольно часто я вижу, как новички пишут огромные контроллеры со всеми возможными действиями в одном методе:

Всё это может быть в одном методе store(), например:

public function store(Request $request)
{
$this->authorize('user_create');

$userData = $request->validate([
'name' => 'required',
'email' => 'required|unique:users',
'password' => 'required',
]);

$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');
$userData['password'] = bcrypt($request->password);

$user = User::create($userData);
$user->roles()->sync($request->input('roles', []));

Project::create([
'user_id' => $user->id,
'name' => 'Demo project 1',
]);
Category::create([
'user_id' => $user->id,
'name' => 'Demo category 1',
]);
Category::create([
'user_id' => $user->id,
'name' => 'Demo category 2',
]);

MonthlyRepost::where('month', now()->format('Y-m'))->increment('users_count');
$user->sendEmailVerificationNotification();

$admins = User::where('is_admin', 1)->get();
Notification::send($admins, new AdminNewUserNotification($user));

return response()->json([
'result' => 'success',
'data' => $user,
], 200);
}

Это не обязательно неправильно, но в будущем другим разработчикам будет очень трудно быстро прочитать. И то, что трудно читать, становиться трудно изменить и исправить будущие ошибки.

Вместо этого контроллеры должны быть короче и просто брать данные из маршрутов, вызывать некоторые методы и возвращать результат. Вся логика работы с данными должна находится в специально подходящих для этого классах:

Существуют различные подходы к структурированию кода, но всегда следует избегать одного огромного метода, отвечающего за всё.

Ошибка 6. Проблема N+1 запроса Eloquent

На сегодняшняя типичная причина №1 низкой производительности проекта Laravel — структура запросов Eloquent. В частности, проблема Запроса N+1 является наиболее распространённой: выполнение сотен SQL-запросов на одной странице определённо требует много ресурсов сервера.

И относительно легко обнаружить эту проблему на таких простых примерах, как этот:

// Controller not eager loading Users:
$projects = Project::all();

// Blade:
@foreach ($projects as $project)
<li>{{ $project->name }} ({{ $project->user->name }})</li>
@endforeach

Но примеры из реальной жизни становятся более сложными, и количество запросов может быть "спрятано" в аксессорах, пакетных запросах и других непредсказуемых местах.

Кроме того, типичные джуны не тратят достаточно времени на тестирование своего приложения с большим количеством данных. Это работает для них с несколькими записями базы данных, поэтому они не делают лишних усилий, чтобы смоделировать будущие сценарии, где их код на самом деле вызовет проблемы с производительностью.

Ошибка 7. Нарушение шаблона MVC: Логика в Blade

Всякий раз, когда я вижу директиву @php в Blade файле, моё сердце начинает биться быстрее.

Посмотрите этот пример:

@php
$x = 5
@endphp

За исключением очень редких случаев, весь PHP-код для получения данных должен быть выполнен до того, как он будет показан в Blade.

Архитектура MVC была создана по той причине, что разделение задач между Моделью (Model), Представлением (View) и Контроллером (Controller) делает более предсказуемым, где искать определённые фрагменты кода.

И в то время как части М и C могут обсуждаться, следует ли хранить логику в Модели, в Контроллере или в отдельных Сервисах/Action, уровень V Представлений является своего рода священным. Золотое правило: представления не должны содержать логику. Другими словами, представления предназначены только для представления данных, а не для преобразования или вычислений.

Происхождения этого связано с тем, что Представление может использовать фронтенд HTML/CSS разработчики, и они могут вносить изменения без необходимости понимания PHP-кода. Конечно, в реальной жизни такое разделение редко встречается в командах, но это благородная цель паттерна, который пришёл извне Laravel и даже PHP.

Для большинства ошибок в этом списке есть список ссылок, но здесь мне особо нечего добавить. Просто не храните логику в Представлениях, вот и всё.

Ошибка 8. Отношения: Не созданы внешние ключи

Отношения между таблицами создаются на двух уровнях: вам нужно создать связанное поле, а затем внешний ключ. Я вижу, что многие джуны забывают вторую часть.

Вы когда-нибудь видели что-то подобное в миграции?

$table->unsignedBigInteger('user_id');

На первый взгляд всё выглядит нормально. И это действительно работает, баз каких-либо ошибок. Сначала.

Позвольте мне показать, что произойдёт если вы не поместите constrained() или references() в этот файл миграции.

Внешний ключ — это механизм ограничения связанных операций на уровне базы данных. Поэтому, когда вы удаляете родительскую запись, вы можете выбирать, что делать с дочерними: либо удалить тоже, либо запретить удаление родителя в первую очередь.

Итак, если вы создаёте только поле unsignedBigInteger(), без внешнего ключа, вы позволяете своим пользователям удалять родителя без каких-либо последствий. Таким образом, дети остаются в базе данных, а их родитель больше не существует.

Ошибка 9. Не читать документацию

Я хочу закончить этот список слоном в комнате. Уже много лет самые популярные мои статьи и твиты — это информация, буквально взятая из документации, практически слово в слово.

С годами я понял, что люди на самом деле не читают документацию полностью, а только основные части, которые наиболее важны для них.

Много раз разработчики удивляли меня, не зная очевидных функций, которые были в документации.

Джуны учатся в основном на готовых руководствах или курсах, но чтение официальной документации должно быть регулярным занятием.

Дополнительные материалы

Предыдущая Статья

Расширение перечислений PHP 8.1 с помощью атрибутов

Следующая Статья

SQL-инъекции: Исследование базы данных атаками