PHP Profi

Интерфейсы для трейтов Перевод

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

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

Проблема:

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

trait GetSalesPrice
{
  /**
   * @return float
   */
  public function getSalesPrice()
  {
    return $this->getWholesalePrice() + $this->getMarkup();
  }
}

Казалось бы, такой трейт легко использовать, но есть проблема - этот метод использует getMarkup() и getWholeSale(), которых может и не быть в классе-потребителе (класс-потребитель - тот, в котором используется код трейта).


Решение:

К счастью, у php много разных возможностей, позволяющих проверить интерфейсы, классы и трейты на соответсвтие их композиции. Представьте, что мы решили, что трейты должны информировать нас о своих зависимостях там, где мы их будем использовать:
 

trait GetSalesPrice
{
  /**
   * @return array
   */
  public function interfacesForGetSalesPrice()
  {
    return [
      HasWholesalePrice::class, HasMarkup::class
    ];
  }

  /**
   * @return float
   */
  public function getSalesPrice()
  {
    return $this->getWholesalePrice() + $this->getMarkup();
  }
}




Можно определить общий трейт, который делает проверку для нужных нам методов. Если интерфейсы не реализованы классом, то кинем исключение:
 

trait SecureTraits
{
  /**
   * @param mixed $container
   *
   * @return void
   */
  public function secureTraits($container)
  {
    $class = new ReflectionClass($container);
 
    $traits     = $class->getTraitNames();
    $interfaces = $class->getInterfaceNames();
 
    foreach ($traits as $trait) {
      $requiredInterfaces = $this->getInterfacesForTrait(
        $container, $trait
      );
 
      foreach ($requiredInterfaces as $interface) {
        $this->throwForMissingInterfaces(
          $interfaces, $interface, $trait
        );
      }
    }
  }
 
  /**
   * @param mixed  $container
   * @param string $trait
   *
   * @return array
   */
  protected function getInterfacesForTrait($container, $trait)
  {
    $method = "interfacesFor{$trait}";
  
    $requiredInterfaces = [];
  
    if (method_exists($container, $method)) {
      $requiredInterfaces = $container->$method();
    }
 
    return $requiredInterfaces;
  }
 
  /**
   * @param array  $interfaces
   * @param string $interface
   * @param string $trait
   */
  protected function throwForMissingInterfaces(
    $interfaces, $interface, $trait
  )
  {
    if (!in_array($interface, $interfaces)) {
      throw new LogicException(
        "{$interface} must be implemented for {$trait}"
      );
    }
  }
}



Это простейший способ использования Reflection (имеется в виду класс Reflection), который когда-либо видел автор :) Метод secureTraits() принимает экземпляр класса и создает для него reflection, откуда мы можем получить список реализованных интерфейсов класса  и трейты, которые он использует. Из списка трейтов мы можем узнать, какие методы вызываются , они просто должны быть такими же (быть похожими на) interfacesForTraitName() . Сравниваем их со списком реализованных в классе интерфейсов и при несоответствии кидаем исключение. Можно использовать такой шаблончик:
 

class ShoePolish implements HasWholesalePrice, HasMarkup
{
  use SecureTraits;
  use GetSalesPrice;
 
  /**
   * @return ShoePolish
   */
  public function __construct()
  {
    $this->secureTraits($this);
  }
 
  /**
   * @return float
   */
  public function getWholesalePrice()
  {
    return 14.99;
  }
  
  /**
   * @return float
   */
  public function getMarkup()
  {
    return $this->getWholesalePrice() * 0.2;
  }
}
 
$shoePolish = new ShoePolish(); 
 
print $shoePolish->getSalesPrice();

Собственно, вот и всё.

2014-11-23 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий