Phockito::build_test_double PHP Method

build_test_double() protected static method

Passed a class as a string to create the mock as, and the class as a string to mock, create the mocking class php and eval it into the current running environment
protected static build_test_double ( boolean $partial, string $mockedClass ) : string
$partial boolean - Should test double be a partial or a full mock
$mockedClass string - The name of the class (or interface) to create a mock of
return string The name of the mocker class
    protected static function build_test_double($partial, $mockedClass)
    {
        // Bail if we were passed a classname that doesn't exist
        if (!class_exists($mockedClass) && !interface_exists($mockedClass)) {
            user_error("Can't mock non-existent class {$mockedClass}", E_USER_ERROR);
        }
        // How to get a reference to the Phockito class itself
        $phockito = self::_has_namespaces() ? '\\Phockito' : 'Phockito';
        // Reflect on the mocked class
        $reflect = new ReflectionClass($mockedClass);
        if ($reflect->isFinal()) {
            user_error("Can't mock final class {$mockedClass}", E_USER_ERROR);
        }
        // Build up an array of php fragments that make the mocking class definition
        $php = array();
        // Get the namespace & the shortname of the mocked class
        if (self::_has_namespaces()) {
            $mockedNamespace = $reflect->getNamespaceName();
            $mockedShortName = $reflect->getShortName();
        } else {
            $mockedNamespace = '';
            $mockedShortName = $mockedClass;
        }
        // Build the short name of the mocker class based on the mocked classes shortname
        $mockerShortName = self::MOCK_PREFIX . $mockedShortName . ($partial ? '_Spy' : '_Mock');
        // And build the full class name of the mocker by prepending the namespace if appropriate
        $mockerClass = (self::_has_namespaces() ? $mockedNamespace . '\\' : '') . $mockerShortName;
        // If we've already built this test double, just return it
        if (class_exists($mockerClass, false)) {
            return $mockerClass;
        }
        // If the mocked class is in a namespace, the test double goes in the same namespace
        $namespaceDeclaration = $mockedNamespace ? "namespace {$mockedNamespace};" : '';
        // The only difference between mocking a class or an interface is how the mocking class extends from the mocked
        $extends = $reflect->isInterface() ? 'implements' : 'extends';
        $marker = $reflect->isInterface() ? ", {$phockito}_MockMarker" : "implements {$phockito}_MockMarker";
        // When injecting the class as a string, need to escape the "\" character.
        $mockedClassString = "'" . str_replace('\\', '\\\\', $mockedClass) . "'";
        // Add opening class stanza
        $php[] = <<<EOT
{$namespaceDeclaration}
class {$mockerShortName} {$extends} {$mockedShortName} {$marker} {
  public \$__phockito_class;
  public \$__phockito_instanceid;

  function __construct() {
    \$this->__phockito_class = {$mockedClassString};
    \$this->__phockito_instanceid = {$mockedClassString}.':'.(++{$phockito}::\$_instanceid_counter);
  }
EOT;
        // And record the defaults at the same time
        self::$_defaults[$mockedClass] = array();
        // And whether it's an interface
        self::$_is_interface[$mockedClass] = $reflect->isInterface();
        // Track if the mocked class defines either of the __call and/or __toString magic methods
        $has__call = $has__toString = false;
        // Step through every method declared on the object
        foreach ($reflect->getMethods() as $method) {
            // Skip private methods. They shouldn't ever be called anyway
            if ($method->isPrivate()) {
                continue;
            }
            // Either skip or throw error on final methods.
            if ($method->isFinal()) {
                if (self::$ignore_finals) {
                    continue;
                } else {
                    user_error("Class {$mockedClass} has final method {$method->name}, which we can\\'t mock", E_USER_WARNING);
                }
            }
            // Get the modifiers for the function as a string (static, public, etc) - ignore abstract though, all mock methods are concrete
            $modifiers = implode(' ', Reflection::getModifierNames($method->getModifiers() & ~ReflectionMethod::IS_ABSTRACT));
            // See if the method is return byRef
            $byRef = $method->returnsReference() ? "&" : "";
            // PHP fragment that is the arguments definition for this method
            $defparams = array();
            $callparams = array();
            // Array of defaults (sparse numeric)
            self::$_defaults[$mockedClass][$method->name] = array();
            foreach ($method->getParameters() as $i => $parameter) {
                // Turn the method arguments into a php fragment that calls a function with them
                $callparams[] = '$' . $parameter->getName();
                // Get the type hint of the parameter
                if ($parameter->isArray()) {
                    $type = 'array ';
                } else {
                    if ($parameterClass = $parameter->getClass()) {
                        $type = '\\' . $parameterClass->getName() . ' ';
                    } else {
                        $type = '';
                    }
                }
                try {
                    $defaultValue = $parameter->getDefaultValue();
                } catch (ReflectionException $e) {
                    $defaultValue = null;
                }
                // Turn the method arguments into a php fragment the defines a function with them, including possibly the by-reference "&" and any default
                $defparams[] = $type . ($parameter->isPassedByReference() ? '&' : '') . '$' . $parameter->getName() . ($parameter->isOptional() ? '=' . var_export($defaultValue, true) : '');
                // Finally cache the default value for matching against later
                if ($parameter->isOptional()) {
                    self::$_defaults[$mockedClass][$method->name][$i] = $defaultValue;
                }
            }
            // Turn that array into a comma seperated list
            $defparams = implode(', ', $defparams);
            $callparams = implode(', ', $callparams);
            // What to do if there's no stubbed response
            if ($partial && !$method->isAbstract()) {
                $failover = "call_user_func_array(array({$mockedClassString}, '{$method->name}'), \$args)";
            } else {
                $failover = "null";
            }
            // Constructor is handled specially. For spies, we do call the parent's constructor. For mocks we ignore
            if ($method->name == '__construct') {
                if ($partial) {
                    $php[] = <<<EOT
  function __phockito_parent_construct( {$defparams} ){
    parent::__construct( {$callparams} );
  }
EOT;
                }
            } elseif ($method->name == '__call') {
                $has__call = true;
            } elseif ($method->name == '__toString') {
                $has__toString = true;
            } else {
                $php[] = <<<EOT
  {$modifiers} function {$byRef} {$method->name}( {$defparams} ){
    \$args = func_get_args();

    \$backtrace = debug_backtrace();
    \$instance = \$backtrace[0]['type'] == '::' ? ('::'.{$mockedClassString}) : \$this->__phockito_instanceid;

    \$response = {$phockito}::__called({$mockedClassString}, \$instance, '{$method->name}', \$args);
  
    \$result = \$response ? {$phockito}::__perform_response(\$response, \$args) : ({$failover});
    return \$result;
  }
EOT;
            }
        }
        // Always add a __call method to catch any calls to undefined functions
        $failover = $partial && $has__call ? "parent::__call(\$name, \$args)" : "null";
        $php[] = <<<EOT
  function __call(\$name, \$args) {
    \$response = {$phockito}::__called({$mockedClassString}, \$this->__phockito_instanceid, \$name, \$args);

    if (\$response) return {$phockito}::__perform_response(\$response, \$args);
    else return {$failover};
  }
EOT;
        // Always add a __toString method
        if ($partial) {
            if ($has__toString) {
                $failover = "parent::__toString()";
            } else {
                $failover = "user_error('Object of class '.{$mockedClassString}.' could not be converted to string', E_USER_ERROR)";
            }
        } else {
            $failover = "''";
        }
        $php[] = <<<EOT
  function __toString() {
    \$args = array();
    \$response = {$phockito}::__called({$mockedClassString}, \$this->__phockito_instanceid, "__toString", \$args);

    if (\$response) return {$phockito}::__perform_response(\$response, \$args);
    else return {$failover};
  }
EOT;
        // Close off the class definition and eval it to create the class as an extant entity.
        $php[] = '}';
        // Debug: uncomment to spit out the code we're about to compile to stdout
        // echo "\n" . implode("\n\n", $php) . "\n";
        eval(implode("\n\n", $php));
        return $mockerClass;
    }