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$
Контроллеры
Название ресурсных контроллеров должны быть в единственном числе
Copy // Хорошо
final class ArticleController
// Плохо
final class ArticlesController
Названия методов
Старайтесь не выходить за дефолтные CRUD названия методов (index, create, store, show, edit, update и destroy)
Создавайте новый контроллер, если вам нужны другие методы.
Хорошее видео о рефакторинге контроллеров от бывшего мейнтейнера Laravel и создателя Tailwind (язык ENG): Cruddy by design
Используйте method injection для Request класса и остальных зависимостей
Copy // Хорошо
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' ) ;
}
Сначала зависимости из маршрутов, а затем остальные
Copy // 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) {}
Маршруты
Используйте новый синтаксис
Copy // Хорошо
Route :: get ( 'about' , [ AboutPageController ::class , 'index' ] ) ;
// Плохо
Route :: get ( 'about' , 'AboutPageController@index' ) ;
Адрес маршрута не должен начинаться с /, если только адрес не будет пустой строкой
Copy // Хорошо
Route :: get ( '/' , 'HomeController@index' ) ;
Route :: get ( 'open-source' , 'OpenSourceController@index' ) ;
// Плохо
Route :: get ( '' , 'HomeController@index' ) ;
Route :: get ( '/open-source' , 'OpenSourceController@index' ) ;
Параметры должны быть в camelCase
Copy Route :: get ( users / {userId} ', [UserController::class, ' show ']);
Маршруты должны быть именованными
Copy Route :: get ( '/' , [ HomeController ::class , 'index' ] ) -> name ( 'home.index' ) ;
Маршруты должны начинаться с HTTP метода
Copy // Хорошо
Route :: get ( '/' , [ HomeController ::class , 'index' ] ) -> name ( 'home.index' ) ;
// Плохо
Route :: name ( 'home.index' ) -> get ( '/' , [ HomeController ::class , 'index' ] ) ;
Используйте синтаксис массива для Route::middleware()
Copy // Хорошо
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 вместо магических
Copy // Хорошо
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) {
//
}
}
Валидация
Старайтесь не использовать | как разделитель для правил валидации.
Copy // Хорошо
public function rules () : array
{
return [
'email' => [ 'required' , 'email' ] ,
];
}
// Плохо
public function rules () : array
{
return [
'email' => 'required|email' ,
];
}
Почему? Синтаксис массива упростит добавление кастомных правил.
Названия кастомных правил должны быть в snake_case стиле
Copy Validator :: extend ( 'is_null' , fn ($attribute , $value , $parameters , $validator) => $value === null )
Старайтесь избегать mass assignment
Copy // 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 понять откуда взят этот метод.
Copy // Хорошо
User :: query () -> firstWhere ( 'id' , 42 ) ;
// Плохо
User :: firstWhere ( 'id' , 42 )
Документируйте модели
На каждом проекте стоит laravel-ide-helper .
После добавления нового поля миграцией или нового метода в модели добавляйте PHPDoc блок командой.
Copy php artisan ide - helper:models
Выносите захардкоженные значения модели в константы модели
Copy // Хорошо
// 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 ) ;
Не используйте старый синтаксис атрибутов
Copy // Хорошо
// User.php
protected function firstName () : Attribute
{
return Attribute :: make (
get : fn ($value) => ucfirst ( $value ),
) ;
}
// Плохо
public function getFirstNameAttribute ($value)
{
return ucfirst ( $value ) ;
}
Artisan команды
Имена команд должны быть в kebab-case стиле
Copy # Хорошо
php artisan delete-old-records
# Плохо
php artisan deleteOldRecords
# Плохо
php artisan delete_old_records
Сервисы
Старайтесь выносить логику, которая может быть переиспользована в разных местах приложения в классы сервисы.
Copy // Хорошо
// 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 ()) ;
}
Необходимо зависеть от абстракции, а не от реализации
Старайтесь ответить на вопрос: "Может ли текущая реализация быть заменена в будущем ?" заранее.
Если - да, то необходимо реализовать интерфейс.
Copy // Хорошо
// 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 позволяет внедрять зависимости без использования конструктора.
Для классов сервисов старайтесь создавать новые Сервис Провайдеры . И делать привязку интерфейса к реализации в них.
Copy // Хорошо
// 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 . Тогда можно реализовать класс заглушку и подменять их в зависимости от среды разработки.
Copy // Хорошо
// App\Providers\DnsServiceProvider
class DnsServiceProvider extends ServiceProvider
{
$this -> app -> when ( ModalController ::class )
-> needs ( Dns ::class )
-> give (
App :: environment ( 'local' )
? TestingDns ::class ,
: CloudflareDns ::class
) ;
}
Коллекции
Copy // Из массива пользователей нужно получить
// массив не пустых почт
// Без коллекций
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, скрывая чувствительные данные
Copy // Хорошо
// .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 файлы
Copy // Плохо
// 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' )
) ;
}
Полезные ссылки