Основы
  • git
  • HTML
  • SCSS
  • JavaScript
  • REST API
  • Руководство TeamLead'a
  • Vue
    • Vue.js
    • Vuex
    • 🍍Pinia
    • Vue router
    • Nuxt
  • PHP
    • PHP
    • Laravel
    • composer
  • QA Engineers
    • Untitled
Powered by GitBook
On this page
  • Linting
  • Контроллеры
  • Маршруты
  • Request
  • Валидация
  • Миграции
  • Модели
  • Artisan команды
  • Сервисы
  • Коллекции
  • Конфиги
  • Полезные ссылки
  1. PHP

Laravel

Руководство по использованию Laravel.

PreviousPHPNextcomposer

Last updated 2 years ago

Laravel позволяет решить одну задачу несколькими способами. В этом разделе перечислены наши соглашения написания кода на Laravel.

Linting

На всех проектах используется .

В PHPStorm можно настроить автоматическое форматирование PHP кода при сохранении Pint - ом:

  1. Отключите встроенное форматирование кода для PHP

  2. Создайте новый File Watcher для Laravel Pint

  3. Используйте следующие настройки

  • 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)

Создавайте новый контроллер, если вам нужны другие методы.

Используйте 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) {
       // 
    }
}

Валидация

Старайтесь не использовать | как разделитель для правил валидации.

// Хорошо
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)

Документируйте модели

После добавления нового поля миграцией или нового метода в модели добавляйте 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')
        );
}

Полезные ссылки

Хорошее видео о рефакторинге контроллеров от бывшего мейнтейнера Laravel и создателя Tailwind (язык ENG):

Старайтесь выносить валидацию в класс.

На каждом проекте стоит .

Для классов сервисов старайтесь создавать новые . И делать привязку интерфейса к реализации в них.

Cruddy by design
FormRequest
laravel-ide-helper
Сервис Провайдеры
Spatie Laravel & PHP Style Guide
IxDF Laravel conventions
Laravel clean code tactics
Laravel Pint
Page cover image