Квест → Как хакнуть форму
Прошли: 77
Трейты - это не только копипаст на уровне компилятора, есть свои нюансы, знание и использование которых может вам очень пригодиться.
Исследуем один из таких нюансов и по возможности его усовершенствование на уровне языка. Посмотрим, как мы можем решить одну проблему во время выполнения, но было бы здорово иметь эту возможность в самом 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();
Собственно, вот и всё.