Макросы в Laravel — это то, о чём еще сказано недостаточно в рамках фреймворка. Они реально мощные и полезные. За последние год-два я не создал ни одного проекта, где бы не использовал макросы.
Если описывать макросы кратко, то это способ расширения метода класса, но без использования наследования, а через замыкание. Это означает, что все экземпляры этого класса будут иметь этот метод. В любом классе фреймворка, который использует трейт Macroable, могут быть применены макросы.
В этой статье я хочу показать ряд простых вариантов использования макросов, которые улучшат вашу работу, уменьшат дублирование кода, сделают код более читабельным или просто решат проблемы, которые могут возникнуть при тестировании.
С самого начала это неочевидная вещь. Есть пара мест, куда бы я порекомендовал положить ваши макросы. Первый — это использовать простой файл php и загружать его через Composer. Обычно я создаю новый файл macros.php в папке app. И, затем, редактирую autoload в composer.json: добавляю относительный путь к файлу app/macros.php в свойство files. Теперь нужно запустить composer dump-autoloader, чтобы наш файл загружался и выполнялся, настраивая макросы для всего приложения
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"database/factories"
],
"files": [
"app/macros.php"
]
}
Другое место для размещения макросов — метод boot в сервис-провайдере. Честно говоря, это может привести к некоторому беспорядку, поскольку ваше приложение растет и вам приходится добавлять еще больше макросов. Кстати, есть странный макрос, который не может быть сделан в macros.php — это Route, так как он привязан к экземпляру хранящемуся в сервисном контейнере. Поэтому мы не будем использовать класс Route в этой статье.
Скукота, но именно здесь большинство людей познакомились с макросами в Laravel, поэтому я хотел бы рассказать об этом совсем кратко.
Хороший пример небольшого макроса, который мне приходилось использовать раньше, — это преобразование ключей для массива, что довольно утомительно, если вы написали его как функцию.
Вместо этого мы можем добавить это как макрос. Сначала мы сделаем макрос, чтобы отмаппить все и управлять рекурсивно.
\Illuminate\Support\Collection::macro(
'mapKeysWith',
function ($callable) {
/* @var $this \Illuminate\Support\Collection */
return $this->mapWithKeys(function ($item, $key) use ($callable) {
if (is_array($item)) {
$item = collect($item)
->mapKeysWith($callable)
->toArray();
}
return [$callable($key) => $item];
});
}
);
И еще один, для красивой обёртки
\Illuminate\Support\Collection::macro(
'mapKeysToCamelCase',
function () {
/* @var $this \Illuminate\Support\Collection */
return $this->mapKeysWith('camel_case');
}
);
Готово. Теперь мы можем его использовать, например так:
collect(['test_key' => ['second_layer' => true]])->mapKeysToCamelCase();
// Создает массив ['TestKey' => ['SecondLayer' => true]]
Это, вероятно, одно из самых важных мест, где макросы действительно имеют значение. Например, было бы почти невозможно расширить Query Builder самостоятельно, а с макросами и не нужно.
К примеру, вам нужно напрямую использовать базу данных, и скорей всего вы будете делать это с помощью сырых запросов (raw queries).
$query->whereRaw(
"ST_Distance_Sphere(`table`.`column`, POINT(?, ?)) < ?",
[$long, $lat, $distance],
$boolean
);
Если вы часто делает это в вашем приложении, то код становится повторяемым, не говоря уже о случаях, когда вы пытаетесь соединить несколько where в одном выражении. Один из способов обойти это заключается в использовании скоупов (scopes), но это потребует добавления скоупов в каждую модель, где их нужно использовать. А мы можем просто сделать для этого макрос. В этом примере мы сделаем макрос для фильтрации результатов в MySQL, используя встроенные геопространственные функции сервера.
\Illuminate\Database\Query\Builder::macro(
'whereSpatialDistance',
function ($column, $operator, $point, $distance, $boolean = 'and') {
$this->whereRaw(
"ST_Distance_Sphere(`{$this->from}`.`$column`, POINT(?, ?)) $operator ?",
[$point[0], $point[1], $distance],
$boolean
);
});
Добавим еще один макрос, чтобы мы могли использовать условие orWhere
\Illuminate\Database\Query\Builder::macro(
'orWhereSpatialDistance',
function ($column, $operator, $point, $distance) {
$this->whereSpatialDistance($column, $operator, $point, $distance, 'or');
}
);
Теперь, когда нужно отфильтровать запрос, мы можем вызвать макрос напрямую, как и любой другой оператор where.
$query->whereSpatialDistance('coordinates', [1, 1], 10)
->orWhereSpatialDistance('coordinates', [0, 0], 1);
Написание функциональных тестов явление распространенное и эффективное, но оно может стать довольно однообразным. Что еще хуже, у вас может быть что-то нестандартное, например, API, который всегда возвращает HTTP-статус 200, но что-то вроде такого:
{"error"=>true,"message"=>"bad API call"}
Вам может понадобиться написать тесты, например такие:
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
public function testApi()
{
$this->postJson('/some-route', ['field' => 'on'])
->assertStatus(200)
->assertJsonFragment(['error' => true]);
}
}
Это может быть хорошо для одного теста, но как насчет остальных? Возможно макрос мог бы решить эту проблему, и угадать, что нужно делать.
Во-первых, на этот раз, создадим tests/macros.php и добавим его в composer.json в опцию autoload-dev параметр files. Нам также необходимо будет обновить автозагрузчик при помощи composer dump-autoloader.
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
},
"files": [
"tests/macros.php"
]
},
Затем в файл tests/macros.php добавим следующее:
\Illuminate\Foundation\Testing\TestResponse::macro('assertErrorInResponse', function () {
/** @var $this \Illuminate\Foundation\Testing\TestResponse */
return $this->assertOk()
->assertJsonFragment(['error' => true]);
});
Теперь мы можем использовать его в наших тестах.
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
public function testApi()
{
$this->postJson('/some-route', ['field' => 'on'])
->assertErrorInResponse();
}
}
Это очень простой фрагмент, но он действительно может сделать ваши тесты намного более читабельными, когда вы начнете сокращать повторяющийся код в макросы многократного использования.
Макросы также могут быть полезны, когда вы хотите использовать Фасады и их способность мокать (mock) для ваших юнит-тестов.
Не лучший сценария для мокинга — использование встроенных классов PHP, дергающих файловую системы во время теста.
Например, посмотрите этот код, использующий класс ZipArchive.
$zip = new ZipArchive();
$zip->open($path);
$zip->extractTo($extractTo);
$zip->close();
Я не могу это мокать, это совершенно новый экземпляр класса ZipArchive. В лучшем случае я мог бы создать некую фабрику ZipArchive для создания класса, но это кажется уже излишним. Вместо этого мы сделам макрос (в нашем файле app/macros.php).
\Illuminate\Filesystem\Filesystem::macro(
'extractZip',
function ($path, $extractTo) {
$zip = new ZipArchive();
$zip->open($path);
$zip->extractTo($extractTo);
$zip->close();
}
);
И, когда дело доходит до продакшн-кода, все, что нам нужно сделать, это использовать фасад.
File::extractZip($path, $extractTo);
Но для наших тестов мы просто мокаем метод, используя Facade.
File::shouldReceive('extractZip')
->once()
->with($path, $extractTo);
Это означает, что нам больше не нужно беспокоиться об очистке файловой системы после тестов. Мы аккуратно завернули нашу логику Zip-архива в то, что можно использовать много раз и легко поддающееся мокингу.
Последний из примеров очень простой, но может сильно помочь, когда дело дойдёт до управления вашим кодом.
Например, я часто сталкиваюсь с таким:
$this->validate($request, [
'date' => ['before:' . $model->created_at->toDateTimeString()],
]);
Само по себе это выглядит не очень опрятно, не говоря уже о тех случаях, когда полей и правил будет много.
Вместо этого лучше сделать так:
$this->validate($request, [
'date' => [Rule::before($model->created_at)],
]);
С макросом:
\Illuminate\Validation\Rule::macro(
'before',
function(\Carbon\Carbon $date) {
return 'before:' . $date->toDateTimeString();
}
);
Что делать если у вас так слишком много макросов в одном большом фале, что вам становится трудно в них ориентироваться и управлять ими? Быстрым решением может стать создание большего количества файлов, таких как app/validation_macros.php, но вы ошибаетесь. Пришло время Миксинов (Mixins, Примеси) (не путайте с трейтами, которые иногда рассматриваются как миксины из-за других языков, имеющих их как концепцию).
Миксин — это класс, который определяет множество макросов, которые затем могут быть предоставлены вашему классу для реализации всех их за один раз.
Давайте рассмотрим пример, создав новый класс app/RulesMixin.php.
namespace App;
use Carbon\Carbon;
class RulesMixin
{
public function before()
{
return function(Carbon $date) {
return 'before:' . $date->toDateTimeString();
};
}
public function beforeOrEqual()
{
return function(Carbon $date) {
return 'before_or_equal:' . $date->toDateTimeString();
};
}
public function after()
{
return function(Carbon $date) {
return 'after:' . $date->toDateTimeString();
};
}
public function afterOrEqual()
{
return function(Carbon $date) {
return 'after_or_equal:' . $date->toDateTimeString();
};
}
}
Как вы видите, всё, что нужно сделать миксину, — это реализовать метод без аргументов, который вернет замыкание, являющееся нашим макросом, использующим имя метода в качестве своего имени.
Теперь в нашем app/macros.php мы можем просто настроить класс на использование макроса следующим образом
\Illuminate\Validation\Rule::mixin(new \App\RulesMixin());
Это означает, что класс Rule будет не только по-прежнему иметь метод before, но теперь будет иметь также правила валидации after, beforeOrEqual и afterOrEqual в более доступном формате.
Мне очень нравится использование макросов в Laravel, и я не могу не подчеркнуть, насколько они полезны для расширения функциональности фреймворка, в соответствии с вашими потребностями, не прилагая особых усилия для этого.
Если вы хотите узнать больше классов, использующих трейт Macroable, то вот вам список для Laravel 5.8:
vendor/laravel/framework/src/Illuminate/Auth/RequestGuard.php
vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
vendor/laravel/framework/src/Illuminate/Cache/Repository.php
vendor/laravel/framework/src/Illuminate/Console/Command.php
vendor/laravel/framework/src/Illuminate/Console/Scheduling/Event.php
vendor/laravel/framework/src/Illuminate/Cookie/CookieJar.php
vendor/laravel/framework/src/Illuminate/Database/Grammar.php
vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Relation.php
vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php
vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php
vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php
vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php
vendor/laravel/framework/src/Illuminate/Http/JsonResponse.php
vendor/laravel/framework/src/Illuminate/Http/RedirectResponse.php
vendor/laravel/framework/src/Illuminate/Http/Request.php
vendor/laravel/framework/src/Illuminate/Http/Response.php
vendor/laravel/framework/src/Illuminate/Http/UploadedFile.php
vendor/laravel/framework/src/Illuminate/Mail/Mailer.php
vendor/laravel/framework/src/Illuminate/Routing/PendingResourceRegistration.php
vendor/laravel/framework/src/Illuminate/Routing/Redirector.php
vendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php
vendor/laravel/framework/src/Illuminate/Routing/Route.php
vendor/laravel/framework/src/Illuminate/Routing/Router.php
vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
vendor/laravel/framework/src/Illuminate/Support/Arr.php
vendor/laravel/framework/src/Illuminate/Support/Collection.php
vendor/laravel/framework/src/Illuminate/Support/Optional.php
vendor/laravel/framework/src/Illuminate/Support/Str.php
vendor/laravel/framework/src/Illuminate/Support/Testing/Fakes/NotificationFake.php
vendor/laravel/framework/src/Illuminate/Translation/Translator.php
vendor/laravel/framework/src/Illuminate/Validation/Rule.php
vendor/laravel/framework/src/Illuminate/View/Factory.php
vendor/laravel/framework/src/Illuminate/View/View.php
Если интересуетесь реализацией, то можете просмотреть код на GitHub.
Автор: Peter Fox
Перевод: Demiurge Ash