Макросы в 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]]
Запросы Eloquent
Это, вероятно, одно из самых важных мест, где макросы действительно имеют значение. Например, было бы почти невозможно расширить 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
Перевод: Алексей Широков
Наш Телеграм-канал — следите за новостями о Laravel.