Laravel: 20 полезных советов

Источник: «20 Useful Laravel Tips»
Итак, я решил составить список из 20 моих любимых советов, которые я размещал в своих аккаунтах в соцсетях, чтобы все могли познакомиться с ними. Они не размещены в каком-то определённом порядке, но я надеюсь, что вы найдёте хотя бы один новый совет, который окажется полезным.

Вступление

Я регулярно публикую небольшие сниппеты и советы в Твиттере (@AshAllenDesign), которые вы можете использовать в своих Laravel-приложениях. Но из-за особенностей социальной сети, старый контент может быть перемещён вглубь ленты и не виден через несколько дней после его публикации.

1. Используйте поля типа DATETIME вместо Boolean

В ваших Laravel приложениях, зачастую можно использовать поля типа DATETIME вместо boolean.

Например, если бы у нас была модель Post, вместо поля is_published типа boolean вы могли бы использовать поле published_at типа DATETIME.

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

Возможно вам не нужна точная дата сейчас, но она может быть полезной в будущих функциях (таких, как отчётность).

Например, мы могли бы обновить модель Post, что бы она выглядела следующим образом:

class Post extends Model
{
public function isPublished(): bool
{
return $this->published_at !== null;
}
}

Это означает, что мы можем использовать $post->isPublished() для получения логического значения. И мы могли бы использовать $post->published_at для получения даты и времени публикации записи.

Сам фреймворк Laravel использует этот подход для обратимого удаления (soft delete) моделей и устанавливает поле deleted_at при мягком удалении модели.

2. Именование столбцов типа Date и Time

Может быть, весьма полезным использовать соглашение об именовании action_at для ваших полей DATETIME и TIMESTAMP. Это помогает мгновенно распознать является ли поле, с которым вы работаете, полем даты и времени (и, вероятно, экземпляром класса Carbon, если вы выполнили приведение к нему).

Например, вместо использования таких полей как:

Было бы полезнее переименовать их в:

3. Используйте Матричные Последовательности в Фабриках

Вы можете использовать MatrixSequence в фабриках моделей для создания дополнительных данных для тестов.

User::factory(4)
->state(
new MatrixSequence(
[['first_name' => 'John', 'last_name' => 'Jane']],
[['first_name' => 'Alice', 'last_name' => 'Bob']],
),
)
->create();

Исполнение приведённого выше фрагмента кода создаст четыре записи модели User со следующими полями:

  1. first_name: John, last_name: Alice
  2. first_name: John, last_name: Bob
  3. first_name: Jane, last_name: Alice
  4. first_name: Jane, last_name: Bob

4. Используйте when в PendingRequest

Вы можете использовать метод when в классе PendingRequest при построении запросов с фасадом http.

Предположим, что у вас есть следующий код:

$http = Http::withBasicAuth($username, $password);

if (app()->environment('local')) {
$http->withoutVerifying();
}

Если вы предпочитаете, чтобы логика запроса была объединена, а не разделена с помощью оператора if, вы можете переписать её используя when:

$http = Http::withBasicAuth($username, $password)
->when(app()->environment('local'), function (PendingRequest $request) {
$request->withoutVerifying();
});

При использовании любого из этих подходов нет правильного или неправильного варианта, всё зависит от личных предпочтений в написании кода.

5. Использование директив Blade checked и selected

Когда вы создаёте формы в Blade, вам может понадобиться установить значение checkbox в checked или выбрать из списка select один или несколько элементов option. Для этого вы можете использовать директивы Blade @checked и @selected.

Например, если мы хотим, чтобы checkbox был отмечен, если пользователь активен, мы могли бы использовать следующий подход:

<input type="checkbox"
name="active"
value="active"
@checked($user->active) />

Точно так же мы могли бы использовать тот же подход, если бы хотели выбрать определённый option в элементе select с помощью директивы @selected:

<select name="version">
@foreach ($product->versions as $version)
<option value="" @selected(old('version') == $version)>

</option>
@endforeach
</select>

6. Используйте Mockery::on() в PHPUnit тестах

При написании тестов на Laravel и PHP вам может понадобиться смоделировать класс (такой, как сервис или action). Смоделировав класс, вы можете убедиться, что он был вызван, как ожидалось, с правильными аргументами, но без запуска кода внутри него. Это может быть особенно полезно при тестировании ваших контроллеров.

Вам может захотеться сделать тесты более строгими и убедится, что конкретная модель или объект передаются методу. Для этого вы можете использовать Mockery::on().

В качестве базового пример возьмём следующий контроллер. Метод контроллера выполняет action PublishPost, при этом action принимает модель Post. Затем мы возвращаем в ответ простой JSON.

// app/Http/Controllers/PublishPostController

use App\Actions\PublishPost;
use App\Models\Post;

class PublishPostController extends Controller
{
public function __invoke(Post $post, PublishPost $action): Response
{
$action->execute($post);

return response()->json([
'success' => true,
]);
}
}

В нашем тесте мы можем настроить смоделированный action, до того как мы сделаем HTTP-вызов. В смоделированном методе мы можем указать, что ожидаем, что метод execute будет вызван один раз и ему будет переданная нами модель Post

Если эти критерии соблюдены, ассерты будут пройдены и мы будем знать, что action был вызван так, как мы и ожидали. В противном случае, если критерии не соблюдены, тест не будет выполнен.

// tests/Feature/PublishPostControllerTest

use App\Actions\PublishPost;
use App\Models\Post;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;

class PublishPostControllerTest extends TestCase
{
/** @test */
public function post_can_be_deleted(): void
{
$post = Post::factory()->create();

$this->mock(PublishPost::class, function (MockInterface $mock) use ($post) {
$mock->shouldReceive('execute')
->once()
->withArgs([
Mockery::on(fn (Post $arg): bool => $arg->is($post)),
]);
});

$this->post(route('post.publish', $post))
->assertOk()
->assertJson([
'success' => true,
]);
}
}

Если вам интересно больше узнать о тестировании, то возможно вас заинтересует статья Laravel: Как сделать ваше приложение более тестируемым

7. Используйте getOrPut() в Коллекциях/Collection

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

Это может быть полезно при создании коллекции с данными из нескольких источников, и если вы не хотите дублировать элементы в своих данных.

Например, вместо того, что бы писать:

if (! $collection->has($key)) {
$collection->put($key, $this->builtItem($data));
}

return $collection->get($key);

Вы можете использовать метод getOrPut() следующим образом:

return $collection->getOrPut($key, fn () => $this->buildItem($data));

8. Отладка HTTP Запросов

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

Для дампа данных запроса, вы можете использовать метод dump() следующим образом:

Http::dump()->get($url);

Точно так же вы можете использовать метод dd, для остановки приложения и вывода данных:

Http::dd()->get($url);

9. Репликация Моделей

В Laravel приложении можно дублировать Модель используя метод replicate(). Это упрощает копирование Моделей.

Я использую эту функциональность в своём блоге для создания копии общего шаблона записи в блоге, который я использую для создания новой статьи.

Например, вы можете продублировать модель следующим образом:

$post = Post::find(123);

$copiedPost = $post->replicate();

$copiedPost->save();

Если вы хотите исключить копирование некоторых свойств, то можете передать их имена в виде массива:

$post = Post::find(123);

$copiedPost = $post->replicate([
'author_id',
]);

$copiedPost->save();

Кроме того, метод replicate() создаёт не сохранённую модель, поэтому вы можете связать вместе со своими обычными методами модели. Например, вы можете скопировать модель и добавить copy в конце заголовка, что бы вы могли видеть, что запись была реплицирована.

$post = Post::find(123);

$copiedPost = $post->replicate([
'author_id',
])->fill([
'title' => $post->title.' (copy)',
]);

$copiedPost->save();

Важно помнить, что модели не сохраняются в базе данных после использования метода replicate(). Итак, вам нужно убедиться, что вы сохраняете их используя метод save.

10. Добавление подсказок автозаполнения к командам artisan

Когда вы создаёте свои artisan-команды в своём приложении Laravel, вы можете использовать метод anticipate для предоставления пользователю подсказок автозаполнения.

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

class TestCommand extends Command
{
public function handle()
{
$email = $this->anticipate('Find user by email: ', [
'mail@ashallendesign.co.uk',
'hello@example.com',
]);
}
}

Стоит отметить, что пользователь по-прежнему может ввести ответ, которого нет в списке. Метод anticipate используется только для подсказок, а не для проверки ввода.

11. Использование wasRecentlyCreated в Моделях

Бывают случаи, когда в Laravel приложении нужно проверить, была ли модель извлечена из базы или только что создана, в текущем жизненном цикле запроса — например, при использовании метода firstOrCreate.

Для этого вы можете использовать поле wasRecentlyCreated в модели следующим образом:

$user = User::firstOrCreate(
['email' => request('email')],
['name' => request('name')],
);

if ($user->wasRecentlyCreated) {
// Ваш пользователь был только что создан...
} else {
// Ваш пользователь уже существует и был получен из базы данных...
}

12. Изменение ключа для привязки Модели к Route

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

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

Route::get('blog/{slug}', [BlogController::class, 'show']);

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

use App\Actions\PublishPost;
use App\Models\Post;

class BlogController extends Controller
{
public function show($slug)
{
$post = Post::where('slug', $slug)->firstOrFail();

return view('blog.show', [
'post' => $post,
]);
}
}

Чтобы упростить и очистить этот код, мы могли бы заменить в маршруте параметр slug на post:slug:

Route::get('blog/{post:slug}', [BlogController::class, 'show']);

Теперь, мы можем обновить наш метод контроллера на ожидание модели Post в качестве параметра $post, и Laravel автоматически найдёт модель Post которой принадлежит slug переданный в URL.

Это означает, что нам не нужно вручную находить модель в контроллере, и мы можем позволить Laravel определить её следующим образом:

use App\Actions\PublishPost;
use App\Models\Post;

class BlogController extends Controller
{
public function show(Post $post)
{
return view('blog.show', [
'post' => $post,
]);
}
}

13. Использование Внедрения Зависимости / Dependency Injection

Внедрение зависимости в коде вашего Laravel приложения позволяет разрешать зависимости от контейнера всякий раз, когда вы создаёте новый класс. Это сделает код лучше поддерживаемым и тестируемым.

В этом примере мы не используем Внедрение Зависимости, а просто создадим новый класс:

class MyController extends Controller
{
public function __invoke()
{
$service = new MyService();

$service->handle();
}
}

Теперь мы удалим в первую строку метода и добавим MyService в качестве параметра. Laravel будет при вызове этого метода каждый раз внедрять $service для нас, что бы мы могли его использовать.

class MyController extends Controller
{
public function __invoke(MyService $service)
{
$service->handle();
}
}

Также бывают случаи, когда вы находитесь внутри класса, и оказывается, что без серьёзного рефакторинга вы не сможете внедрить свой класс, передав его в качестве дополнительного параметра метода. В таком случае можно использовать хэлпер resolve() предоставляемый Laravel, например:

class MyClass
{
public function execute()
{
$service = resolve(MyService::class);

$service->handle();
}
}

14. Фильтрация Коллекций по типу класса

Вы можете фильтровать Коллекции laravel по заданному типу класса, используя метод whereInstanceOf. Это может быть полезным если вы создаёте Коллекции из нескольких источников данных (например, полиморфных отношений) и вам нужно отфильтровать их до определённых классов.

Например, мы могли бы иметь следующую Коллекцию и отфильтровать её, что бы она содержала только класс User:

$collection = collect([
new User(),
new User(),
new User(),
new Comment(),
]);

$filtered = $collection->whereInstanceOf(User::class)->all();

Метод whereInstanceOf() также принимает массив классов, если вы хотите фильтровать более чем по одному классу. Например, что бы отфильтровать Коллекцию так, что бы она содержала только классы Post и Comment, вы можете сделать это следующим образом:

$filtered = $collection->whereInstanceOf([Post::class, Comment::class])->all();

15. Определение пользовательской логики временного URL

Вы можете настроить способ создания временных URL-адресов для отдельных хранилищ данных. Это может быть удобно, если у вас есть контроллер позволяющий загружать файлы в хранилища, которые не поддерживают временные URL-адреса.

Для использования этой функции необходимо зарегистрировать логику (обычно в сервис провайдере) с помощью метода buildTemporaryUrlsUsing.

Вот пример, как мы могли бы зарегистрировать некую пользовательскую логику для создания временных URL-адресов для локального хранилища данных (локального диска):

public function boot()
{
Storage::disk('local')->buildTemporaryUrlsUsing(function ($path, $expiration, $options) {
return URL::temporarySignedRoute(
'files.download',
$expiration,
array_merge($options, ['path' => $path]),
);
});
}

После регистрации логики, вы можете её использовать функционал с помощью метода temporaryUrl, например так:

$tempUrl = Storage::disk('local')->temporaryUrl('file.jpg', now()->addMinutes(5));

16. Валидация MAC адресов

В Laravel доступно полезное правило mac_address, которое можно использовать для валидации того, является ли поле mac адресом.

Например, следующая проверка будет пройдена, поскольку значение является валидным MAC-адресом:

Validator::make([
'device_mac' => '00:1A:C2:7B:00:47'
], [
'device_mac' => 'mac_address',
])->passes();

Но следующая проверка завершится ошибкой, поскольку значение не является MAC-адресом:

Validator::make([
'device_mac' => 'invalid-mac-address'
], [
'device_mac' => 'mac_address',
])->passes();

17. Шифрование полей в Базе Данных

Вы можете хранить отдельные поля в зашифрованном формате используя задав в свойстве $cast модели значение encrypted для шифруемого поля. Это полезно, если вы храните личные данные в базе данных, которые нуждаются в дополнительной защите в случае взлома данных.

Например, чтобы зашифровать поле my_encrypted_field в модели User, вам нужно обновить свои модель следующим образом:

class User extends Authenticatable
{
protected $casts = [
'my_encrypted_field' => 'encrypted',
];
}

Вы можете продолжать использовать поле как обычно. Например, что бы обновить значение хранящееся в my_encrypted_field, мы всё ещё можем использовать метод update, как обычно:

$user->update(['my_encrypted_field' => 'hello123']);

Если бы вы сейчас заглянули в базу данных, то не увидели бы значения hello123 в поле my_encrypted_field. Вместо этого вы увидели бы его зашифрованную версию.

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

$result = $user->my_encrypted_field;

// $result is equal to: "hello123"

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

18. Перемещение Логики в Методы

Вместо прямой проверки полей в условных выражениях иногда можно перенести логику в метод.

Это поможет улучшить читаемость кода, а также поможет придерживаться принципа DRY, если вам нужно повторно использовать туже логику (или если логика изменится в будущем).

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

if ($user->approved === Approval::APPROVED) {
// Сделать что-то...
}

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

Итак, мы можем переместить логику в метод (например, isApproved) модели User. Теперь мы можем вызывать этот метод, вместо прямой проверки поля.

Например, теперь наша модель может выглядеть так:

// app/Models/User.php

class User extends Model
{
public function isApproved(): bool
{
return $this->approved === Approval::APPROVED;
}
}

Это означает, что мы можем использовать этот метод в нашем коде следующим образом:

if ($user->isApproved()) {
// Сделать что-то...
}

19. Использование различных параметров режима обслуживания

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

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

php artisan down

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

php artisan down --refresh=30

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

Например, мы могли бы установить значение --secret, как your-secret-here. Это будет означать, что если вы зайдёте на your-app-domain.com/your-secret-here, вам будет предоставлен доступ и вы сможете просматривать остальную часть приложения как обычно. Вы можете включить режим обслуживания используя параметр --secret:

php artisan down --secret="your-secret-here"

Если вы не хотите использовать страницу 503 режима обслуживания по умолчанию предоставляемую Laravel, вы можете задать свою с помощью опции --render. Например, если мы хотим использовать resources/views/errors/maintenance.blade.php:

php artisan down --render="errors/maintenance.blade.php"

20. Использование Readonly-свойств

В PHP 8.1 вы можете использовать Readonly-свойств. Они чрезвычайно полезны для уменьшения размера DTO (data transfer objects) и облегчения чтения без ненужных геттеров.

Давайте посмотрим, как DTO может выглядеть без readonly-свойств:

class StoreUserDTO
{
public function __construct(
private string $name,
private string $email,
private string $password,
) {
//
}

public function getName(): string
{
return $this->name;
}

public function getEmail(): string
{
return $this->email;
}

public function getPassword(): string
{
return $this->password;
}
}

Теперь давайте взглянем на тот же DTO, используя readonly-свойства:

class StoreUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
) {
//
}
}

Я думаю, вы согласитесь, что второй вариант с использованием readonly-свойств выглядит намного чище и проще для понимания с первого взгляда.

Заключение

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

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

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

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

Топ-5 генераторов статических сайтов в 2022

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

PHP 8.0 и 8.1: Объяснение современных возможностей