Полезные советы и лучшие практики для Laravel

Лучши практики Ларавел

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

Я работаю с Laravel уже 2 года. Начинал с плохого кода и, постепенно, занимаюсь его улучшением, используя преимущества Laravel. Я покажу вам свои тайные приемы, которые могут вам помочь в программировании на этом фреймворке.

 

Используйте локальные скоупы, когда нужно сделать запрос

У Laravel удобный способ написания запросов к базе данных через Query Builder. Что-то вроде этого:

$orders = Order::where('status', 'delivered')->where('paid', true)->get();

Это очень здорово. Я отказался от SQL и полностью сосредоточился на коде, ставшим доступнее для меня. Но и этот фрагмент кода может быть улучшен, если мы используем локальные скоупы (local scopes). Они позволят нам создать собственные методы Query Builder, которые мы сможем применить цепочкой для получения данных. Например, вместо операторов ->where() мы можем использовать более понятные методы ->delivered() и ->paid().

Для начала добавим в нашей модели Order несколько методов:

class Order extends Model
{
   ...
   public function scopeDelivered($query) {
      return $query->where('status', 'delivered');
   }

   public function scopePaid($query) {
      return $query->where('paid', true);
   }
}

При объявлении скоупов нужно использовать точное именование scope[Something]. Тогда Laravel будет знать, что это скоуп, и использует его в Query Builder. Убедитесь, что включён первый аргумент, который автоматически внедряет Laravel и который является экземпляром построителя запросов.

$orders = Order::delivered()->paid()->get();

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

class Order extends Model
{
   ...
   public function scopeStatus($query, string $status) {
      return $query->where('status', $status);
   }
}

$orders = Order::status('delivered')->paid()->get();

Далее вы узнаете, почему нужно использовать snake_case (змеиный_регистр) для полей базы данных, а вот первая причина: Laravel по умолчанию использует where[Something] для замены предыдущего скоупа. Таким образом, вы можете сделать:

Order::whereStatus('delivered')->paid()->get();

Laravel будет искать поле «Something» (из where[Something]) в змеином_регистре. Если у вас есть поле status в вашей БД, то вы можете использовать предыдущий пример. Если у вас есть shipping_status, то вы можете использовать:

Order::whereShippingStatus('delivered')->paid()->get();

Всё на ваш выбор!

 

При необходимости используйте файлы Запросов

Laravel предоставляет удобный способ проверки форм. Неважно, это запрос POST или GET, фреймворк всё равно всё проверит, если захотите.

Вы можете провести валидацию в вашем контроллере:

public function store(Request $request)
{
    $validatedData = $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ]);

    // Запись в блог валидна...
}

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

Laravel предлагает «симпатичный» способ валидации запросов путем создания класса запроса и использовании его вместо устаревшего класса Request. Вам просто нужно создать свой request:

php artisan make:request StoreBlogPost

Внутри папки app/Http/Requests вы найдете файл запроса:

class StoreBlogPostRequest extends FormRequest
{
   public function authorize()
   {
      return $this->user()->can('create.posts');
   }

   public function rules()
   {
       return [
         'title' => 'required|unique:posts|max:255',
         'body' => 'required',
       ];
   }
}

Теперь, вместо Illuminate\Http\Request в вашем методе вы должны использовать свеже созданный класс:

use App\Http\Requests\StoreBlogPostRequest;

public function store(StoreBlogPostRequest $request)
{
    // Запись в блоге валидна...
}

Метод authorize() должен возвращать логическое значение. Если оно будет false, то получите ошибку 403, поэтому убедитесь, что вы перехватываете её в методе render() в app/Exceptions/Handler.php:

public function render($request, Exception $exception)
{
   if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) {
      //
   }

   return parent::render($request, $exception);
}

Отсутствующий здесь метод в классе запроса — это функция messages(), которая представляет собой массив, содержащий сообщения, которые будут возвращены в случае ошибки валидации:

class StoreBlogPostRequest extends FormRequest
{
   public function authorize()
   {
      return $this->user()->can('create.posts');
   }

   public function rules()
   {
       return [
         'title' => 'required|unique:posts|max:255',
         'body' => 'required',
       ];
   }

   public function messages()
   {
      return [
        'title.required' => 'Требуется Заголовок.',
        'title.unique' => 'Заголовок сообщения уже существует.',
        ...
      ];
   }
}
@if ($errors->any())
   @foreach ($errors->all() as $error)
      {{ $error }}
   @endforeach
@endif

Если вы хотите получить сообщение проверки определенного поля, то вы можете сделать это следующим образом (он вернет ложно-логическую сущность, если поле пройдет валидацию):

<input type="text" name="title" />
@if ($errors->has('title'))
   <label class="error">{{ $errors->first('title') }}</label>
@endif

 

Магические скоупы

Вы можете использовать уже имеющиеся магические скоупы:

Получение результата по убыванию поля created_at:

User::latest()->get();

Получение результата по любому полю по убыванию:

User::latest('last_login_at')->get();

Получение результата в случайном порядке:

User::inRandomOrder()->get();

Запрос работает, только если условие верно:

// Предположим, что пользователь находится на странице новостей 
// и хочет сначала отсортировать их, сначала новые
// mydomain.com/news?sort=new

User::when($request->query('sort'), function ($query, $sort) {
   if ($sort == 'new') {
      return $query->latest();
   }
   
   return $query;
})->get();

Вместо when() вы можете использовать unless — это его противоположность.

 

Используйте Отношения, чтобы избежать больших запросов (или плохо написанных)

Вы когда-нибудь использовали кучу join’ов в запросе для получения информации? Довольно сложно писать эти команды SQL, даже с помощью Query Builder, но Модели уже делают это с Отношениями . Возможно, сразу вы поймете не всё, из-за большого количества информации в документации, но это поможет вам лучше разобраться, как все это работает и как сделать ваше приложение лучше.

 

Использование Задач для трудоемких операций

Laravel Jobs — мощный инструмент для выполнения задач в фоновом режиме.

  • Нужно отправить электронное письмо? Задачи.
  • Нужно передать сообщение? Задачи.
  • Нужно обработать изображение? Задачи.

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

Вы можете ознакомиться с документацию по Задачам здесь.

Для очередей задач мне нравится использовать Laravel Horizon. Его легко настроить, его можно демонизировать через Supervisor, а в конфигурацию назначить сколько процессов нужно для каждой очереди.

 

Придерживайтесь стандартов и методов доступа к базам данных

Laravel учит нас с самого начала, что переменные и методы должны быть $camelCase camelCase(), в то время как поля базы данных должны быть snake_case. Почему? Потому что, это помогает нам создавать лучшие методы доступа (accessors).

Аксессоры — это настраиваемые поля, которые мы можем сделать прямо в нашей модели. Если база данных содержит first_name, last_name и age, то мы можем добавить пользовательское поле с именем name, которое объединяет first_name и last_name. Не волнуйтесь, оно не попадет в БД. Это просто пользовательский атрибут этой конкретной модели. Все методы доступа, такие как скоупы, имеют собственный синтаксис наименования: getSomethingAttribute:

class User extends Model
{
   ...
   public function getNameAttribute(): string
   {
       return $this->first_name.' '.$this->last_name;
   }
}

При использовании $user->name он возвратит объединенную строку.

По умолчанию атрибут name не отображается в dd($user), но мы можем сделать его общедоступным при помощью переменной $appends:

class User extends Model
{
   protected $appends = [
      'name',
   ];
   ...

   public function getNameAttribute(): string
   {
       return $this->first_name.' '.$this->last_name;
   }
}

Теперь, каждый раз когда мы делаем dd($user), то видим, что переменная существует (хотя её нет в базе данных).

Однако будьте осторожны: если у вас уже есть поле name, то все будет немного по-другому: переменная name внутри $appends больше не нужна,
а функция атрибута ждет только один параметр, который и является уже сохраненной переменной (больше не используем $this).

Для того же примера, можно преобразовать переменные в верхний регистр:

class User extends Model
{
   protected $appends = [
      //
   ];
   ...

   public function getFirstNameAttribute($firstName): string
   {
       return ucfirst($firstName);
   }

   public function getLastNameAttribute($lastName): string
   {
      return ucfirst($lastName);
   }
}

Теперь, когда мы используем $user->first_name, то получим строку в верхнем регистре.

Именно по этой причине удобно использовать змеиный_регистр для полей базы данных.

 

Не храните в конфигурациях статические данные, связанные с моделью

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

Вместо этого:

// BettingOdds.php

class BettingOdds extends Model
{
   ...
}

// config/bettingOdds.php

return [
   'sports' => [
      'soccer' => 'sport:1',
      'tennis' => 'sport:2',
      'basketball' => 'sport:3',
      ...
   ],
];

и получая их через

config(’bettingOdds.sports.soccer’);

Я предпочитаю делать так:

// BettingOdds.php

class BettingOdds extends Model
{
   protected static $sports = [
      'soccer' => 'sport:1',
      'tennis' => 'sport:2',
      'basketball' => 'sport:3',
      ...
   ];
}

и получаю их через:

BettingOdds::$sports['soccer'];

Почему? Потому что, проще использовать в дальнейших операциях:

class BettingOdds extends Model
{
   protected static $sports = [
      'soccer' => 'sport:1',
      'tennis' => 'sport:2',
      'basketball' => 'sport:3',
      ...
   ];

   public function scopeSport($query, string $sport)
   {
      if (! isset(self::$sports[$sport])) {
         return $query;
      }
      
      return $query->where('sport_id', self::$sports[$sport]);
   }
}

Теперь мы можем применить скоуп:

BettingOdds::sport('soccer')->get();

 

Использование коллекций вместо необработанного массива

Мы давно привыкли работать с массивами в сыром виде:

$fruits = ['apple', 'pear', 'banana', 'strawberry'];

foreach ($fruits as $fruit) {
   echo 'I have '. $fruit;
}

Теперь мы можем использовать передовые методы, которые помогают нам обрабатывать данные в массивах. Мы можем фильтровать, преобразовывать, перебирать и изменять данные внутри массива:

$fruits = collect($fruits);

$fruits = $fruits->reject(function ($fruit) {
   return $fruit === 'apple';
})->toArray();

['pear', 'banana', 'strawberry']

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

При работе с Query Builder метод ->get() возвращает экземпляр Collection. Но не перепутайте Collection с Query Builder:

В Query Builder мы не получаем никаких данных. У нас множество методов, связанных с запросами: orderBy(),where(), и т.д.

  • После использования ->get(), данные извлекаются, память освобождается и возвращается экземпляр Collection. Некоторые методы запросов становятся недоступны или же их имена отличаются. Проверьте доступные методы  для получения дополнительной информации.
  • Если вы можете фильтровать данные на уровне Query Builderуровне — делайте это! Не полагайтесь на фильтрацию, когда дело доходит до Collection — в некоторых местах вы будете использовать слишком много памяти, а вам это совершенно не нужно. Лимитируйте результаты и используйте индексы на уровне БД.

 

Используйте пакеты и не изобретайте велосипед

Вот некоторые пакеты, которые я использую:

Некоторые пакеты, которые написал я:

  • Befriended (лайки, подписки и баны, как в социальных сетях)
  • Schedule (создание расписания и сверка с днями и часами)
  • Rating (рейтинг моделей)
  • Guardian (система разрешений, простым способом)

 

Автор: Alex Renoki
Перевод: Demiurge Ash