Laravel: 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
, если вы выполнили приведение к нему).
Например, вместо использования таких полей как:
- publish_time
- verified_date
- password_reset
Было бы полезнее переименовать их в:
- published_at
- verified_at
- password_reset_at
3. Используйте Матричные Последовательности в Фабриках
Вы можете использовать MatrixSequence
в фабриках моделей для создания дополнительных данных для тестов.
User::factory(4)
->state(
new MatrixSequence(
[['first_name' => 'John', 'last_name' => 'Jane']],
[['first_name' => 'Alice', 'last_name' => 'Bob']],
),
)
->create();
Исполнение приведённого выше фрагмента кода создаст четыре записи модели User
со следующими полями:
first_name: John, last_name: Alice
first_name: John, last_name: Bob
first_name: Jane, last_name: Alice
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.
Если эта статья помогла вам, я хотел бы узнать об этом. Кроме того, если у вас есть отзывы по улучшению этой статьи, я то же хотел бы узнать их.