Защищённые классы

Сегодня у меня была любопытная беседа с Дэвидом Мюрреем об идее защищённых моделей от использования в нежелательных контекстах. Он разрабатывал приложение, у которого есть репозитории, работающие с моделями, и мы подумали, что стоит попробовать защитить модели от непосредственного использования.

Ограничения

PHP не поддерживает такого рода трюки. На самом деле это довольно смехотворная идея – предотвратить вызов класса в определённое время. Но это нечто такое, что можно сделать с помощью подручных средств.

Давайте предположим, что конкретные классы могут быть созданы только в контексте других классов. Мы можем создать только модели Doctrine в репозиториях доктрины, модели Eloquent в Eloquent репозиториях и так далее.

Чтобы не было путаницы, уточню: я использую слово «модель» в значении, что модель – это всего лишь обёртка для материала базы данных. Тем самым я описываю реализацию паттерна Шлюз к данным записи. То есть, когда я говорю «модель», я имею ввиду класс обёртки вокруг таблицы базы данных, где каждый экземпляр представляет строку в базе данных.

Оглядываясь назад

В PHP есть определённая функция, которая поможет нам достичь текущей скромной цели. Называется она debug_backtrace(), с которой, вероятно, вы знакомы и наблюдаете каждый раз, когда генерируете ошибку в PHP.

1 _H8ckcVDE-B0AJg-5o4QeQ
Упс!

debug_backtrace() может сказать нам, в каком классе вызваны операторы. Оглянувшись на несколько шагов назад, мы можем узнать, создаются ли новые экземпляры класса в рамках других классов. Давайте для начала попробуем что-нибудь простое:

class DoctrineModel
{
  public function __construct()
  {
    print_r(debug_backtrace(
      DEBUG_BACKTRACE_IGNORE_ARGS, 5
    ));
  }
}

Флаг DEBUG_BACKTRACE_IGNORE_ARGS уменьшает количество получаемых данных, потому что всё, в чём мы действительно заинтересованы, это ключ class. Цифра 5 – это количество шагов, на которые мы хотим вернуть трассировку. В итоге получится что-то похожее на это:

Array
  (
    [0] => Array
      (
        [file] => /path/to/debug.php
        [line] => 37
        [function] => __construct
        [class] => DoctrineModel
        [type] => ->
      )
  )

Мы можем подробно остановиться на этой идее, создав новую модель в другом классе:

class DoctrineModel
{
  public function __construct()
  {
    print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5));
  }
}
 
class DoctrineRepository
{
  public function __construct()
  {
    new DoctrineModel();
  }
}
 
new DoctrineRepository();

Так мы придём к обратной трассировке, напоминающей:

Array
(
  [0] => Array
    (
      [file] => /path/to/debug.php
      [line] => 41
      [function] => __construct
      [class] => DoctrineModel
      [type] => ->
    )
  [1] => Array
    (
      [file] => /path/to/debug.php
      [line] => 46
      [function] => __construct
      [class] => DoctrineRepository
      [type] => ->
    )
)

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

Другой след, другая черта

Повторно используемый код может быть и частью трейта. Я люблю трейты, эти глубоко испорченные создания. Нам нужно что-то простое:

trait ProtectedClassTrait
{
  /**
   * @param string $parent
   * @param int $depth
   */
  protected function protect($parent, $depth = 5)
  {
    $trace = debug_backtrace(
      DEBUG_BACKTRACE_IGNORE_ARGS,
      $depth
    );
  
    $i = count($trace);
  
    while ($i--) {
      if ($trace[$i]["class"] === $parent) {
        return;
      }
    }
  
    throw new LogicException();
  }
}

В классах, которые мы хотим защитить, получится так:

class EloquentModel
{
  use ProtectedClassTrait;
  
  /**
   * @return EloquentModel
   */
  public function __construct()
  {
    $this->protect(EloquentRepository::class);
  }
}

Теперь мы можем создавать экземпляры модели только внутри намеченного класса:

class EloquentRepository
{
  /**
   * @var EloquentModel
   */
  protected $model;
  
  /**
   * @return EloquentRepository
   */
  public function __construct()
  {
    $this->model = new EloquentModel();
  }
}

Бамс, внедрение зависимости!

Знаю, знаю. Это отстой, потому что зависимости должны быть созданы за пределами классов, для которых они нужны.

Решение заключается в использовании фабрики!

class DoctrineRepository
{
  /**
   * @var ModelFactory
   */
  protected $factory;
  
  /**
   * @param ModelFactory $factory
   *
   * @return DoctrineRepository
   */
  public function __construct(ModelFactoryInterface $factory)
  {
    $this->factory = $factory;
  
    $factory->make(DoctrineModelInterface::class);
  }
}

Прям как Java, вау.

class ModelFactory implements ModelFactoryInterface
{
  /**
   * @var array
   */
  protected $models = [
    DoctrineModelInterface::class => DoctrineModel::class,
    EloquentModelInterface::class => EloquentModel::class
  ];
  
  /**
   * @param string $interface
   *
   * @return ModelInterface
   */
  public function make($interface)
  {
    if (isset($this->models[$interface])) {
      $model = $this->models[$interface];
  
      return new $model();
    }
  
    throw new InvalidArgumentException();
  }
}

Вот оно, мои друзья. Если, по-вашему, делать так – не очень хорошая идея, то воспринимайте это как один из методов для достижения какой-нибудь простой цели типа этой.

Я пропустил несколько интерфейсов, но смысл должен быть ясен. В зависимости от фабрики, вы можете чувствовать себя менее ужасно при создании зависимостей внутри класса. По крайней мере, теперь стажёры не смогут играться с моделями…

Примечание

Это авторский перевод статьи «Protected Classes» на русский язык.

Комментарии

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *