roydejong/Enlighten

View on GitHub
lib/Context.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Enlighten;

/**
 * Represents the context for a routing process.
 * This is used to provide a target function with the appropriate data.
 */
class Context
{
    /**
     * Contains a mapping of types and instances.
     * These instances will be provided to
     *
     * @var array
     */
    protected $instances;

    /**
     * Contains a mapping of class names that are weakly related.
     *
     * For example, if an \InvalidArgumentException is added via registerInstance(), this mapping will contain an item
     * that maps \Exception - its parent class - to \InvalidArgumentException.
     *
     * This mapping is used when we don't have an exact (strong) match in $instances.
     *
     * @var array
     */
    protected $weakLinks;

    /**
     * Contains a registry of primitive types, indexed by name.
     *
     * @var array
     */
    protected $variablesByName;

    /**
     * Initializes a blank routing context.
     */
    public function __construct()
    {
        $this->instances = [];
        $this->variablesByName = [];

        $this->registerInstance($this);
    }

    /**
     * Registers an object instance to the context.
     * If an instance with the same type is already registered, this will override it.
     *
     * @param object $object
     */
    public function registerInstance($object)
    {
        if (!is_object($object)) {
            throw new \InvalidArgumentException('registerInstance(): Cannot pass primitive types, can only register an object instance; use registerVariable() instead');
        }

        $reflectionObject = new \ReflectionObject($object);
        $className = $reflectionObject->getName();

        // Register strong link in $instance
        $this->instances[$className] = $object;

        // Recursively determine and register weak links
        $determineParent = function (\ReflectionClass $class) use ($className, &$determineParent) {
            $parentClass = $class->getParentClass();

            if (!empty($parentClass)) {
                $this->weakLinks[$parentClass->getName()] = $className;
                $determineParent($parentClass);
            }
        };

        $determineParent($reflectionObject);
    }

    /**
     * Registers a primitive variable type by its name to the context.
     * If a variable with the same name is already registered, this will override it.
     *
     * @param string $name
     * @param mixed $value
     */
    public function registerVariable($name, $value)
    {
        if (is_object($value)) {
            throw new \InvalidArgumentException('registerVariable(): Cannot pass objects, can only register primitive types; use registerInstance() instead');
        }

        $this->variablesByName[$name] = $value;
    }

    /**
     * Gets an array of all registered object instances in this Context.
     *
     * @return array
     */
    public function getRegisteredInstances()
    {
        return array_values($this->instances);
    }

    /**
     * Gets an array of all registered variables in this Context, indexed by name.
     *
     * @return array
     */
    public function getRegisteredVariables()
    {
        return $this->variablesByName;
    }

    /**
     * Given a callable function, attempts to determine the reflection parameters.
     *
     * @param callable $callable
     * @return \ReflectionParameter[]
     */
    private function getReflectionParameters(callable $callable)
    {
        $reflectionParams = [];

        if (is_array($callable)) {
            // Callable array
            $reflector = new \ReflectionMethod($callable[0], $callable[1]);
            $reflectionParams = $reflector->getParameters();
        } else if (is_string($callable)) {
            // Callable function string
            $reflector = new \ReflectionFunction($callable);
            $reflectionParams = $reflector->getParameters();
        } else if (is_a($callable, 'Closure') || is_callable($callable, '__invoke')) {
            /** @var \Closure $callable */
            $reflector = new \ReflectionObject($callable);
            $reflectionParams = $reflector->getMethod('__invoke')->getParameters();
        }

        return $reflectionParams;
    }

    /**
     * Given a callable function and the current context, attempt to determine the appropriate list of parameters to
     * pass to the function when it is called.
     *
     * @param callable $callable
     * @return array A list of parameter values to be passed to the function, in the appropriate order.
     */
    public function determineParamValues(callable $callable)
    {
        $reflectionParams = $this->getReflectionParameters($callable);

        $paramList = [];

        foreach ($reflectionParams as $reflectionParam) {
            $paramList[] = $this->determineParamValue($reflectionParam);
        }

        return $paramList;
    }

    /**
     * Given a class and the current context, attempt to determine the appropriate list of parameters to pass to the
     * constructor when it is called.
     *
     * @param $className
     * @return array
     */
    public function determineParamValuesForConstructor($className)
    {
        $reflection = new \ReflectionClass($className);
        $instance = $reflection->newInstanceWithoutConstructor();
        $callable = [$instance, '__construct'];

        if (!is_callable($callable)) {
            // This class does not have a constructor function
            return [];
        }

        return $this->determineParamValues($callable);
    }

    /**
     * Based on the current context, attempts to determine an appropriate value for a given parameter.
     *
     * @param \ReflectionParameter The function parameter to analyze and determine a value for.
     * @return mixed
     */
    private function determineParamValue(\ReflectionParameter $parameter)
    {
        $class = $parameter->getClass();

        if (!empty($class)) {
            // If this is a object we may be able to map it to something in our context
            $className = $class->getName();

            // Determine strong type-based link
            if (isset($this->instances[$className])) {
                return $this->instances[$className];
            }

            // Determine weak type-based link
            if (isset($this->weakLinks[$className])) {
                $lowerClassName = $this->weakLinks[$className];
                return $this->instances[$lowerClassName];
            }
        } else {
            // If this is a primitive type, attempt to determine it by its name
            $varName = $parameter->getName();

            if (isset($this->variablesByName[$varName])) {
                return $this->variablesByName[$varName];
            }
        }

        // We were unable to determine a suitable value based on this context. Pass back its default value if possible.
        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }

        // As a final fallback we will primitive pass NULL and let the function deal with it.
        return null;
    }
}