Decorating classes my way

The need for a class decorator

The name ClassDecorator is a glorified version of façade builder; as mentioned in at least one earlier post of mine I needed a systematic way to build a façade of functions around an adapter class making heavy use of PHP magic methods.
The scope of the initial effort, using the decorator on my Functions adapter class, has been transcended and this class decorator is now one of my main TDD tools.

The code

The class itself is not that much code and implements methods aimed at allowing, typically at test time, to dress a class before mocking it.

<?php

/**
 *  The class will add and remove methods from a target class for testing purposes.
 *  Due to PHPUnit limits methods that are not present in the class cannot be stubbed or mocked and PHPUnit
 *  will throw a 'Mocked method does not exist' error. To avoid this the Functions class get a method façade
 *  using a list or array of methods to do so.
 *
 */
class ClassDecorator
{
    /**
     * The name, complete with namespace, of the class to decorate.
     * Example is 'TAD\AdapterClasses\Functions'
     * @var string
     */
    public $targetClassName = null;
    /**
     * An array that keeps track of the methods added to the class for tear-down purposes
     * @var array
     */
    public $addedMethods = array();
    /**
     * Returns an instance of the class
     * @param string $targetClassName The target class name with full namespacing
     */
    public function __construct($targetClassName)
    {
        if (!is_string($targetClassName)) {

            throw new \InvalidArgumentException('Target class name must be a string!');
        }
        $this->targetClassName = $targetClassName;
    }
    /**
     * Adds many methods to the target class replacing them if already defined
     * @param mixed  $list    Either an array of function names or a comma separated list of function names
     * @param string $code    The code the added methods will execute, defaults to null-returning methods
     * @param boolean $replace Defaults to true and already defined methods will be replaced in the class if defined already, false     to skip already defined methods.
     * @return int The number of methods added
     */
    public function addMethods($list, $code = null, $replace = true)
    {
        if (!is_string($list) and !is_array($list)) {

            throw new \InvalidArgumentException('List must be either an array or a comma separated string');
        }
        if (is_string($list)) {
            $list = explode(',', $list);
        }
        // for each function add it to the class
        $added = 0;

        foreach ($list as $method) {
            // remove leading and trailing whitespaces
            $added+= $this->addMethod($method, $code, $replace);
        }
        return $added;
    }
    /**
     * Adds a method to the target class
     * @param string $method  The method name
     * @param string  $code    The code the added method will execute, defaults to returning null
     * @param boolean $replace If false an already defined method will not be replaced, defaults to true
     * @return  int 1 if the method was added, 0 if no method was added
     */
    public function addMethod($method, $code = null, $replace = true)
    {
        if (!is_string($method)) {

            throw new \InvalidArgumentException('Method name must be a string');
        }
        if (!is_null($code) and !is_string($code)) {

            throw new \InvalidArgumentException('Code must either be null or a string');
        }
        if (is_null($code)) {
            $code = 'return null;';
        }
        // remove any eventual leading and trailing white spaces from the method name
        $method = trim($method);
        // if the method has been added to the class and replacing is active then remove it
        if (method_exists($this->targetClassName, $method)) {
            if ($replace) {
                runkit_method_remove($this->targetClassName, $method);
            }
            else {

                return 0;
            }
        }
        // add a null returning method
        runkit_method_add($this->targetClassName, $method, '', $code);
        $this->addedMethods[] = $method;

        return 1;
    }
    /**
     * Removes a method from the target class
     * @param  string $methodName The name of the method to remove
     * @return int             1 if the method was removed, 0 otherwise
     */
    public function removeMethod($methodName)
    {
        if (method_exists($this->targetClassName, $methodName)) {
            runkit_method_remove($this->targetClassName, $methodName);
            $this->addedMethods = array_diff($this->addedMethods, array(
                $methodName
            ));

            return 1;
        }

        return 0;
    }
    /**
     * Removes many method from the target class
     * @param  mixed $list Either an array of method names to remove or a comma-separated list
     * @return int The number of removed methods
     */
    public function removeMethods($list = null)
    {
        if (is_null($list)) {
            $list = $this->addedMethods;
        }
        if (is_string($list)) {
            $list = explode(',', $list);
        }
        $removed = 0;

        foreach ($list as $methodName) {
            $removed+= $this->removeMethod($methodName);
        }

        return $removed;
    }
    /**
     * Returns an array containing the names of the methods added to the class
     * @return array The names of the methods added to the class
     */
    public function getAddedMethods()
    {

        return $this->addedMethods;
    }
}
?>

I’m not going again at it but, very briefly, PHPUnit will not mock methods that are not declared in the class and this make mocking an adapter class and its defined-at-runtime methods impossible.

Runkit does the heavy-lifting

The methods are removed and added to the class, right now public only, using runkit and will epically fail generating errors if the runkit extension is not loaded.

The way I use it

One way I use the class is to dress up a class I know will define a method at runtime with that method like

// file SomeClassDependingOnAdpaterTest.php

// during testing WordPress global space functions will not be defined!

// create a new instance of the class to dress my own Functions adapter class
$cd = new ClassDecorator('TAD\AdpaterClasses\Functions');

// at runtime the 'add_action' method will be defined by WordPress and the adapter class
// will wrap it using the __call method
$cd->addMethod('add_action');

//the mock will now have the 'add_action' method
$mock = $this->getMock('TAD\AdpaterClasses\Functions');

Can’t I simply use runkit?

Yes, but a class decorator makes adding many (say 300) methods to a class easy as well as one and shortens the call using default values. Furthermore it gives a way to strip down those methods later.

Final words

The whole class revolves around two powerful possibilities

  • runkit can add and remove methods from a class at runtime
  • PHPUnit can change a method arguments and return values at runtime

the two possibilities above give legs to the class decorator.

I appreciate your input