Laravel Under The Hood - Магия макросов

Laravel Under The Hood - Магия макросов

5 ноября 2024 г.

Привет 👋

Как часто вы хотели метод, которого нет в коллекциях или строковых помощниках? Вы начинаете связывать методы, только чтобы упереться в стену, когда один из них оказывается отсутствующим. Честно говоря, это понятно; фреймворки, знаете ли, штука универсальная. Я оказывался в такой ситуации несколько раз.

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

Что за макросы? 🍏

Допустим, у нас есть такой JWT:

$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

И нам нужно извлечь заголовки:

str($jwt)
    ->before('.')
    ->fromBase64()
    ->fromJson(); // does not exist 😞

//  BadMethodCallException  Method Illuminate\Support\Stringable::fromJson does not exist.

ThefromJson()не существует 😔 Конечно, можно было бы просто сделать:

json_decode(str($jwt)->before('.')->fromBase64());

Но где же тут веселье? Плюс, это моя статья 🤷

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

Если вы проверитеIlluminate\Support\Stringableкласс, вы увидите, что он используетMacroableчерта.

Давайте продолжим и расширим класс. ВAppServiceProvider, добавьте следующее:

<?php

namespace App\Providers;

use Illuminate\Support\Stringable;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Stringable::macro('fromJson', function (bool $associative = true) {
            return json_decode($this->value, $associative);
        });
    }
}

Теперь давайте перезапустим код:

str($jwt)
    ->before('.')
    ->fromBase64()
    ->fromJson();

// ["alg" => "HS256", "typ" => "JWT"]

Это работает идеально 🎉 Но теперь вы можете задаться вопросом, как это работает? И что именно$this->value? Что, черт возьми, происходит?

Раскрытие магии 🪄

Мы знаем, чтоStringableкласс используетMacroableчерта, которая обеспечиваетmacro()Метод. Давайте подробнее рассмотрим, что он делает:

// src/Illuminate/Macroable/Traits/Macroable.php

/**
 * Register a custom macro.
 *
 * @param  string  $name
 * @param  object|callable  $macro
 *
 * @param-closure-this static  $macro
 *
 * @return void
 */
public static function macro($name, $macro)
{
    static::$macros[$name] = $macro;
}

Это довольно просто: он просто сохраняет обратный вызов в статическомmacrosмассив. Теперь, если мы рассмотрим признак дальше, мы найдем__callметод, который срабатывает каждый раз, когда вызывается несуществующий метод. В нашем случае этоfromJson()Давайте углубимся:

/**
 * Dynamically handle calls to the class.
 *
 * @param  string  $method
 * @param  array  $parameters
 * @return mixed
 *
 * @throws \BadMethodCallException
 */
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}

Сначала он проверяет, зарегистрирован ли макрос, что имеет место в случаеfromJson(), затем он извлекает обратный вызов (или объект) из массива макросов. Теперь, для магического трюка; если макрос является замыканием (как в нашем случае), он вызываетbindTo()что по сути говорит закрытию, что$thisдолжен ссылаться на то, что передается как первый аргумент. В этом случае этоStringableэкземпляр, который, как оказалось, имеет$valueатрибут.

// $this here is the stringable
// $this inside the closure is now referencing the stringable class
$macro->bindTo($this, static::class);

И вот почему мы можем сделать$this->value.

Мы можем сделать лучше: Миксины 🧩

Хочу показать вам еще одну вещь! Когда мы расширяем один и тот же класс несколько раз, поставщик услуг может очень быстро запутаться. Мы можем извлечь все наши пользовательские макросы в класс, называемый Mixin.

Давайте создадимStringableMixin:

<?php

namespace App\Macros;

use Closure;

class StringableMixin
{
    public function fromJson(): Closure
    {
        return function (bool $associative = true) {
            json_decode($this->value, $associative);
        };
    }

    // Add more macros here as needed
}

Сейчас, вAppServiceProvider, мы можем зарегистрировать этот миксин:

use App\Macros\StringableMixin;
use Illuminate\Support\Stringable;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Stringable::mixin(new StringableMixin);
    }
}

И все! Теперь мы можем сделать:

str($jwt)
    ->before('.')
    ->fromBase64()
    ->fromJson();

По сути то же самое, просто немного чище.

Если вам интересно, как это работает,mixin()метод наMacroableчерта используетAPI отражения. Он извлекает все открытые методы из класса Mixin, ожидает, что каждый из них вернет замыкание, а затем регистрирует замыкание как макрос, как мы видели ранее.

Плохая IDE 🥲

Ну, как вы видели, происходит много магии, и IDE не будет знать об определенных макросах. Если вы работаете в команде, другие разработчики также не будут знать об этих макросах, что нехорошо. К счастью, есть инструменты, которые помогут вам в этом. Бесплатный и открытый вариант — этоПомощник Laravel IDEупаковка.

Вы можете установить пакет и сгенерировать_ide_helper.phpфайл, и все будет готово.

И вот это конец..

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

Поэтому, когда вы чувствуете, что чего-то не хватает в вашем приложении, но не в самом фреймворке, используйте макросы 🪄


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE