Laravel
Руководство по использованию Laravel.
Laravel позволяет решить одну задачу несколькими способами. В этом разделе перечислены наши соглашения написания кода на Laravel.
Linting
На всех проектах используется Laravel Pint.
В PHPStorm можно настроить автоматическое форматирование PHP кода при сохранении Pint - ом:
Отключите встроенное форматирование кода для PHP
Создайте новый File Watcher для Laravel Pint
Используйте следующие настройки
Program: $ProjectFileDir$/vendor/bin/pint
Arguments: $FileRelativePath$
Output paths to refresh: $FileRelativePath$
Working directory: $ProjectFileDir$
Контроллеры
Название ресурсных контроллеров должны быть в единственном числе
// Хорошо
final class ArticleController
// Плохо
final class ArticlesController
Названия методов
Старайтесь не выходить за дефолтные CRUD названия методов (index, create, store, show, edit, update и destroy)
Создавайте новый контроллер, если вам нужны другие методы.
Хорошее видео о рефакторинге контроллеров от бывшего мейнтейнера Laravel и создателя Tailwind (язык ENG): Cruddy by design
Используйте method injection для Request класса и остальных зависимостей
// Хорошо
public function update(Request $request, User $user)
{
$this->validate($request, ['email' => ['email']);
$name = $request->input('name');
}
// Плохо
public function update(User $user)
{
$this->validate(request(), ['email' => ['email']);
$name = request('name');
}
Сначала зависимости из маршрутов, а затем остальные
// Routes/web.php
Route::post(‘/users/{user}’, [UserController, ‘update’])
// Хорошо
public function update(User $user, Request $request, UserSevice $service) {}
// Плохо
public function update(Request $request, User $user, UserSevice $service) {}
Маршруты
Используйте новый синтаксис
// Хорошо
Route::get('about', [AboutPageController::class, 'index']);
// Плохо
Route::get('about', 'AboutPageController@index');
Адрес маршрута не должен начинаться с /, если только адрес не будет пустой строкой
// Хорошо
Route::get('/', 'HomeController@index');
Route::get('open-source', 'OpenSourceController@index');
// Плохо
Route::get('', 'HomeController@index');
Route::get('/open-source', 'OpenSourceController@index');
Параметры должны быть в camelCase
Route::get(users/{userId}', [UserController::class, 'show']);
Маршруты должны быть именованными
Route::get('/', [HomeController::class, 'index'])->name('home.index');
Маршруты должны начинаться с HTTP метода
// Хорошо
Route::get('/', [HomeController::class, 'index'])->name('home.index');
// Плохо
Route::name('home.index')->get('/', [HomeController::class, 'index']);
Используйте синтаксис массива для Route::middleware()
// Хорошо
Route::get('about',[AboutPageController::class,'index'])->middleware(['cache:1day']);
Route::get('about', [AboutPageController::class, 'index'])->middleware(['cache:1day', 'CORS']);
// Плохо
Route::get('about', [AboutPageController::class, 'index'])->middleware('cache:1day', 'CORS');
Route::get('about', [AboutPageController::class, 'index'])->middleware('cache:1day');
Request
Используйте методы класса Request вместо магических
// Хорошо
public function store(Request $request)
{
$email = $request->get('email');
if ($request->hasFile('avatar') {
//
}
if ($request->filled('contacts') {
//
}
}
// Плохо
public function store(Request $request)
{
$email = $request->email;
if ($request->avatar) {
//
}
if ($request->contacts) {
//
}
}
Валидация
Старайтесь выносить валидацию в FormRequest класс.
Старайтесь не использовать | как разделитель для правил валидации.
// Хорошо
public function rules(): array
{
return [
'email' => ['required', 'email'],
];
}
// Плохо
public function rules(): array
{
return [
'email' => 'required|email',
];
}
Почему? Синтаксис массива упростит добавление кастомных правил.
Названия кастомных правил должны быть в snake_case стиле
Validator::extend('is_null', fn ($attribute, $value, $parameters, $validator) => $value === null)
Старайтесь избегать mass assignment
// User.php
protected $fillable = ['name', 'email', 'password', 'role'];
// Хорошо
public function store(UserStoreRequest $request)
{
$user = User::create($request->validated());
}
// Плохо
public function store(UserStoreRequest $request)
{
$user = User::create($request->all());
}
Почему? С фронта к форме можно редактированием html кода добавить поле role с значением admin и оно попадет в бд.
Миграции
Всегда пишите down() метод миграции для возможности отката.
Модели
Всегда начинайте запрос со слова query().
Это помогает IDE понять откуда взят этот метод.
// Хорошо
User::query()->firstWhere('id', 42);
// Плохо
User::firstWhere('id', 42)
Документируйте модели
На каждом проекте стоит laravel-ide-helper.
После добавления нового поля миграцией или нового метода в модели добавляйте PHPDoc блок командой.
php artisan ide-helper:models
Выносите захардкоженные значения модели в константы модели
// Хорошо
// Article.php
const STATUS_PUBLISHED = 'PUBLISHED'
const STATUS_DRAFTED = 'DRAFTED'
const STATUSES = [
self::STATUS_PUBLISHED,
self::STATUS_DRAFTED
]
// ArticleStoreRequest.php
'status' => ['required', 'in:'. implode(',', Article::STATUSES)]
// ArticleController.php
Article::query()->where('status', Article::STATUS_PUBLISHED);
Не используйте старый синтаксис атрибутов
// Хорошо
// User.php
protected function firstName(): Attribute
{
return Attribute::make(
get: fn ($value) => ucfirst($value),
);
}
// Плохо
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
Artisan команды
Имена команд должны быть в kebab-case стиле
# Хорошо
php artisan delete-old-records
# Плохо
php artisan deleteOldRecords
# Плохо
php artisan delete_old_records
Сервисы
Старайтесь выносить логику, которая может быть переиспользована в разных местах приложения в классы сервисы.
// Хорошо
// App\Http\Services\StorageImage
public function store(file $image, string, $path = null, $folder = 'images'): string
{
...
Storage::put($path, $file->__toString());
...
}
// App\Http\Controllers\UserController
public function update(User $user, Request $request, Image $imageService)
{
$pathToFile = $imageService->store(
image: $request->file('image'),
path: $user->image
);
}
public function store(Request $request, Image $service)
{
$pathToFile = $imageService->store($request->file('image'));
}
// Плохо
// App\Http\Controllers\UserController
public function update(User $user, Request $request, Image $imageService)
{
...
$path = 'user/avatars/user.jpg';
Storage::put($path, $requset->file('image')->__toString());
}
public function store(Request $request, Image $service)
{
...
$path = 'user/avatars/user.jpg';
Storage::put($path, $requset->file('image')->__toString());
}
Необходимо зависеть от абстракции, а не от реализации
Старайтесь ответить на вопрос: "Может ли текущая реализация быть заменена в будущем?" заранее. Если - да, то необходимо реализовать интерфейс.
// Хорошо
// App\Http\Services\Dns;
// Интерфейс должен содержать методы, которые используются вне сервиса
// (В контроллерах, джобах и т.д)
interface Dns
{
public function getDomain(): string;
public function deleteDomainById(mixed $id): void;
}
// App\Http\Services\CloudflareDns;
class CloudflareDns implements Dns
{
// Вспомогательные методы сервиса лучше делать приватными или защищенными
protected function sendRequestToCloudflare()
{
// Реализация вспомогательного метода
}
public function getDomain(): string
{
// Реализация
}
public function deleteDomainById(mixed $id): void
{
// Реализация
}
}
// App\Http\Controllers\ModalController;
// В типе сервиса указывается интерфейс
public fuction store(Request $request, Dns $dnsService)
{
...
$domain = $dnsService->getDomain();
...
}
Какая от этого польза?
В контексте примера выше. Если в дальнейшем будет необходимо мигрировать с сервиса cloudflare на любой аналог. Нужно будет всего лишь написать новую реализацию интерфейса DNS для аналога, без необходимости изменять код контроллера.
Binding интерфейса к реализации.
Laravel позволяет внедрять зависимости без использования конструктора.
Для классов сервисов старайтесь создавать новые Сервис Провайдеры. И делать привязку интерфейса к реализации в них.
// Хорошо
// App\Providers\DnsServiceProvider
class DnsServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->bind(Dns::class, CloudflareDns::class);
$this->app->when(ModalController::class)
->needs(Dns::class)
->give(CloudflareDns::class);
}
}
Какая от этого польза?
Например, необходимо при локальной разработке не отправлять запрос в Cloudlfare. Тогда можно реализовать класс заглушку и подменять их в зависимости от среды разработки.
// Хорошо
// App\Providers\DnsServiceProvider
class DnsServiceProvider extends ServiceProvider
{
$this->app->when(ModalController::class)
->needs(Dns::class)
->give(
App::environment('local')
? TestingDns::class,
: CloudflareDns::class
);
}
Коллекции
// Из массива пользователей нужно получить
// массив не пустых почт
// Без коллекций
function getUserEmails($users)
{
$emails = [];
for ($i = 0; $i < count($users); $i++) {
$user = $users[$i];
if ($user->email !== null) {
$emails[] = $user->email;
}
}
return $emails;
}
// С использованием коллекций
function getUserEmails($users)
{
return Collection::make($users)
->filter(fn ($user) => $user->email !== null)
->map(fn ($user) => $user->email)
->toArray();
}
Оба решения хороши и мы никак не ограничиваем вас в использовании.
Но с помощью коллекций иногда можно получить более элегантное решение, не так ли?
Не злоупотребляйте коллекциями если это вредит читабельности кода!
Конфиги
Не забывайте дублировать новые переменные в файле .env в файл .env.example, скрывая чувствительные данные
// Хорошо
// .env
CLOUDFLARE_ENDPOINT=https://api.cloudflare.com/client/v4/
CLOUDFLARE_TOKEN=SUPER_SECRET_AUTH_TOKEN
CLOUDFLARE_ZONE_ID=SUPER_SECRET_ZONE_TOKEN
// .env.example
CLOUDFLARE_ENDPOINT=https://api.cloudflare.com/client/v4/
CLOUDFLARE_TOKEN=
CLOUDFLARE_ZONE_ID=
Выносите чувствительные данные в конфиги и .env файлы
// Плохо
// App/Service/CloudflareDns.php
protected function store()
{
return Http::withToken('SUPER_SECRET_AUTH_TOKEN')
->POST(
'https://api.cloudflare.com/client/v4/',
'SUPER_SECRET_ZONE_TOKEN'
);
}
// Отлично
// .env
CLOUDFLARE_ENDPOINT=https://api.cloudflare.com/client/v4/
CLOUDFLARE_TOKEN=SUPER_SECRET_AUTH_TOKEN
CLOUDFLARE_ZONE_ID=SUPER_SECRET_ZONE_TOKEN
// Configs/Cloudflare.php
return [
'endpoint' => env('CLOUDFLARE_ENDPOINT'),
'token' => env('CLOUDFLARE_TOKEN'),
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
];
// App/Service/CloudflareDns.php
protected function store()
{
return Http::withToken(
config('pwabuilder.cloudflare.token')
)->POST(
config('pwabuilder.cloudflare.endpoint'),
config('pwabuilder.cloudflare.zone_id')
);
}
Полезные ссылки
Last updated