Interfaces For Traits

Traits are little more than compiler-assisted copy and paste. I love them! They have their quirks, and if you (as with everything) use them judiciously then they can be super helpful.

I want to explore one of their quirks, and a possible improvement that can be made to them at a language level. I’ll show how we can solve the problem at runtime, but it would be great to get language support for it.

The Problem

Traits are an excellent means of extracting common behaviour. When we extract a method to a trait, it can use it in many different classes without code duplication.

Unfortunately extracted methods can (and often do) depend on other methods. Methods you can’t [read: shouldn’t] extract. Methods that belong in a different trait. This leads to the hope that classes provide all the methods our traits need.

Consider the the following example:

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

This trait would be so easy to reuse. It calls out to methods which consumer classes provide, but the functionality is generic. It’s the implementation of a simple formula.

The trouble is that it requires the consumer to provide getMarkup() and getWholesalePrice(). With no language support for this, we’re left hoping that consumers provide this functionality.

A Runtime Solution

Luckily, PHP has great reflection tools. These help us to inspect interfaces, classes and traits to gain an understanding of their composition. Imagine we decided that traits should be able to inform consumers of their dependencies:

trait GetSalesPrice
{
/**
* @return array
*/
public function interfacesForGetSalesPrice()
{
return [
HasWholesalePrice::class, HasMarkup::class
];
}
  /**
* @return float
*/
public function getSalesPrice()
{
return $this->getWholesalePrice() + $this->getMarkup();
}
}

We can define a common trait (oh the irony!) that checks for this kind of method. If the interfaces returned are not implemented by the class then we can throw an exception:

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}"
);
}
}
}

This is the simplest usage of Reflection I have seen. The secureTraits() method accepts a class instance and creates a reflection of it. From this we can get a list of interfaces the class implements and traits it uses.

From the list of traits we can figure out what methods to call. They just need to resemble interfacesForTraitName(). We can compare these to the list of interfaces the class implements. If we find a mismatch, we throw an exception.

We can use this with little boilerplate:

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();

We could make secureTraits() do nothing in production, reducing the overhead of this solution.

If you found this helpful and/or found an error; let me know with a comment, or on Twitter.