Laravel: Фасады

31.10.2020 в 15:44
3852
0

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

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

Однако не все то золото, что блестит: есть в Laravel и другие, на первый взгляд, удобные, но дурацкие решения. Одно из них — Фасады. Да, все вы слышали, как фреймворк за это ругают, но если вы не понимаете, почему они — фасады — зло и как они работают, лучше вам для начала подтянуть матчасть и не выглядеть посмешищем. Первое мы сделаем сейчас, а второе останется вам в качестве домашнего задания.

Откройте злополучный файл и следите за руками. Чтобы создать собственный фасад, вам необходимо отнаследоваться от Illuminate\Support\Facades\Facade и реализовать статический метод getFacadeAccessor, из которого вам нужно вернуть или полное имя класса/интерфейса, или любое другое имя, по которому контейнер внедрения зависимостей сможет его определить и создать.

use Illuminate\Support\Facades\Facade;

final class Shit extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return ShitCode::class;
    }
}

...

final class ShitCode
{
    public function doMagic()
    {
       return 'Magic';
    }
}

Все, фасад готов, более ничего делать не нужно. А как Laravel узнает о методах сервиса? А никак, он использует магический метод __callStatic(), который вызывается php автоматически для не статических методов, вызванных статически.

Вы можете написать Shit::doMagiс() и тогда вызов метода упадет в этот метод фасада:

public static function __callStatic($method, $args)
{
     $instance = static::getFacadeRoot();

     if (! $instance) {
         throw new RuntimeException('A facade root has not been set.');
     }

     return $instance->$method(...$args);
}

Метод getFacadeRoot пытается создать для нас инстанс сервиса, имя которого мы вернули в методе getFacadeAccessor:

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

Если перевести на псевдоязык:

public static function getFacadeRoot()
{
    return static::создатьИнстансКорняФасада('ShitCode');
}

Метод resolveFacadeInstance делает следующее:

protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            return static::$resolvedInstance[$name] = static::$app[$name];
        }
    }

Если метод getFacadeAccessor вернул объект (да-да, и это несмотря на то, что в phpdoc'е заявлено, что возвращать можно только строку), просто возвращаем его, так как это простой объект, не требующий создание с помощью контейнера. Если такой корень фасада уже был ранее создан, то он сохранился в $resolvedInstance и его можно просто вернуть (кэширование). Если есть static::$app (а это контейнер), то достаем из него следующим образом static::$app[$name] и сохраняем в $resolvedInstance, откуда достанем в следующий раз. static::$app[$name] - такая конструкция работает, потому что контейнер реализует php-шный интерфейс ArrayAccess, подробнее на этой строке кода.

Еще у вас есть возможность зарегистрировать callback в сервис-провайдере, который Laravel дернет, когда разрезолвит корень фасада, передав в качестве аргумента оригинальный сервис. Например:

/**
 * @method static string doMagic()
 */
final class Shit extends Facade
{
    protected static function getFacadeAccessor()
    {
        return ShitCode::class;
    }
}

final class ShitCode
{
    public $greeting = '';

    public function doMagic()
    {
        return $this->greeting . 'Magic';
    }

    public function addGreeting(string $greeting)
    {
        $this->greeting = $greeting;
    }
}

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        Shit::resolved(function (ShitCode $code) {
            $code->addGreeting('Hello ');
        });
    }
}

...

Shit::doMagic(); // Hello Magic

Будьте осторожны с использованием этой возможности, так как в этом случае getFacadeAccessor обязан возвращать строку, а не объект, иначе фреймворк упадет с ошибкой Illegal offset type in isset or empty из-за этой строки. На ней вызывается метод контейнера resolved, куда передается инстанс корня фасада, который в нашем случае является объектом. Метод resolved проверяет, не является ли наш абстракт (корень фасада в данном случае) алиасом, а чтобы это узнать, контейнер ищет в массиве $aliases такое имя:

public function isAlias($name)
{
   return isset($this->aliases[$name]);
}

А поскольку именем тут у нас является объект и в обычном массиве ключами массива не могут быть объекты, происходит ошибка.

Тут можно было бы оправдать фреймворк, так как зачем вам фасад для простых объектов, но раз все же он позволяет использовать объекты (is_object($name) помните?), то нехорошо падать на такой глупой ошибке.

В самой первой части обзора про цикл жизни фреймворка мы говорили про бутстрапперы, а это бутстраппер для фасадов:

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Support\Facades\Facade;

class RegisterFacades
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        Facade::clearResolvedInstances();

        Facade::setFacadeApplication($app);

        AliasLoader::getInstance(array_merge(
            $app->make('config')->get('app.aliases', []),
            $app->make(PackageManifest::class)->aliases()
        ))->register();
    }
}

Все остальные методы Facade нужны для облегчения тестирования, и это первый минус фасадов: как и любая статика, фасады тяжело поддаются тестированию. И речь не про тестирование фасадов, а кода, где они используются. Вам придется создавать моки, чтобы фасады не ходили в реальную базу или не пушили реальные события, что тоже не является хорошим решением, так как зачастую тесты могут не показать, что объект, используемый фасадом, перестал работать, как надо, и будут продолжать проходить. Это нас не устраивает, разумеется.

Второй минус — магия. Вам придется постоянно заполнять эти скучные phpdoc в угоду автокомплита, что в некотором смысле нарушает open-closed принцип из популярный аббревиатуры SOLID. Еще один важный недостаток — это то, что фасады являются сервис-локаторами, что считается антипаттерном, так как усложняет их настройку и скрывает от потребителя реальные зависимости объекта, что может приводить к сайд-эффектам и другим неприятным последствиям. По-хорошему, все зависимости, используемых вами объектов, должны быть представлены в сигнатуре конструктора и методов объекта. Переизбыток использования фасадов может привести к такой картине:

public function controllerAction()
{
     Session::push('url', Request::url());

     if (!Cache::has('articles')) {
         Cache::add('articles', Article::orderBy('created_at', 'desc')->paginate(10), '...');
     }

     return Response::view('', Cache::get('articles'));
}

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

loader
31.10.2020 в 15:44
3852
0
Комментарии
Новый комментарий

Логические задачи с собеседований