Квест → Как хакнуть форму
Прошли: 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();
Собственно, вот и всё.