PHP Reflection: real-life use cases

0

Reflection is one of these language features that most developer may have never used and may never use in their lifetime. And that’s fine! Reflection is there to handle some very specialized cases. It’s not there for fun. But I am sure that many of you may wonder how often this feature is being used and which are these specialized cases that ask for reflection. I will try to present some real-life examples taking small samples from applications, tools and repositories that put reflection techniques into action.

Serialization

  1. Laravel framework has a trait named “SerializationModels”. This trair is supposed to serialize Eloquent Models by serializing only the model identifier. During the un-serialization the identifier will be to used to retrieve the full model from database.
trait SerializesModels
{
    use SerializesAndRestoresModelIdentifiers;

    /**
     * Prepare the instance for serialization.
     *
     * @return array
     */
    public function __sleep()
    {
        $properties = (new ReflectionClass($this))->getProperties();

        foreach ($properties as $property) {
            $property->setValue($this, $this->getSerializedPropertyValue(
                $this->getPropertyValue($property)
            ));
        }

        return array_map(function ($p) {
            return $p->getName();
        }, $properties);
    }

    /**
     * Restore the model after serialization.
     *
     * @return void
     */
    public function __wakeup()
    {
        foreach ((new ReflectionClass($this))->getProperties() as $property) {
            $property->setValue($this, $this->getRestoredPropertyValue(
                $this->getPropertyValue($property)
            ));
        }
    }
    ...
}

2. The zend-json component of ZendFramework has a JSON encoder named “Encoder” that uses reflection to identify the structure of a PHP construct. The way it works is quite clear.

/**
 * Encode PHP constructs to JSON.
 */
class Encoder
{
    /**
     * Encode the public methods of the ReflectionClass in the class2 format
     *
     * @param ReflectionClass $class
     * @return string Encoded method fragment.
     */
    private static function encodeMethods(ReflectionClass $class)
    {
        $result  = 'methods:{';
        $started = false;

        foreach ($class->getMethods() as $method) {
            if (! $method->isPublic() || ! $method->isUserDefined()) {
                continue;
            }

            if ($started) {
                $result .= ',';
            }
            $started = true;

            $result .= sprintf('%s:function(', $method->getName());

            if ('__construct' === $method->getName()) {
                $result .= '){}';
                continue;
            }

            $argsStarted = false;
            $argNames    = "var argNames=[";

            foreach ($method->getParameters() as $param) {
                if ($argsStarted) {
                    $result .= ',';
                }

                $result .= $param->getName();

                if ($argsStarted) {
                    $argNames .= ',';
                }

                $argNames .= sprintf('"%s"', $param->getName());
                $argsStarted = true;
            }
            $argNames .= "];";

            $result .= "){"
                . $argNames
                . 'var result = ZAjaxEngine.invokeRemoteMethod('
                . "this, '"
                . $method->getName()
                . "',argNames,arguments);"
                . 'return(result);}';
        }

        return $result . "}";

    }
    ...
}

Annotation extraction

3.  The Laravel framework has a class named “AnnotationClassLoader” which can read @Route annotations from a class’ methods.

/**
 * AnnotationClassLoader loads routing information from a PHP class and its methods.
 *
 * ...
 *
 * The @Route annotation can be set on the class (for global parameters),
 * and on each method.
 *
 * The @Route annotation main value is the route path. The annotation also
 * recognizes several parameters: requirements, options, defaults, schemes,
 * methods, host, and name. The name parameter is mandatory.
 *
 * ...
 * 
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class AnnotationClassLoader implements LoaderInterface
{
...
    /**
     * Loads from annotations from a class.
     *
     * ...
     */
    public function load($class, $type = null)
    {
        ...

        $class = new \ReflectionClass($class);

        ...

        foreach ($class->getMethods() as $method) {
            $this->defaultRouteIndex = 0;
            foreach ($this->reader->getMethodAnnotations($method) as $annot) {
                if ($annot instanceof $this->routeAnnotationClass) {
                    $this->addRoute($collection, $annot, $globals, $class, $method);
                }
            }
        }

        ...
    }
...
}

Dumping

4. Symfony VarDumper Component , contains a method that retrieves the parameters from a function/method:

private static function getParameters($function, $class)
{
    ...

    try {
        $r = null !== $class ? new \ReflectionMethod($class, $function) : new \ReflectionFunction($function);
    } catch (\ReflectionException $e) {
        return array(null, null);
    }

    $variadic = '...';
    $params = array();

    foreach ($r->getParameters() as $param) {
        $paramVarName = '$'.$param->name;
        if ($param->isPassedByReference()) {
            $paramVarName = '&'.$paramVarName;
        }
        if ($param->isVariadic()) {
            $variadic .= $paramVarName;
        } else {
            $params[] = $paramVarName;
        }
    }

    return self::$parameters[$paramVarName] = array($variadic, $params);
}

Mocking objects for testing

5. In PHPUnit, the PHPUnit\Framework\MockObject\Generator  class is responsible for generating the a mock object. In this class, there is a methods  generateMock()  that extracts information about the definition of those class methods that can be mocked.

private function generateMock(...)
{
    ...
    $class = new ReflectionClass($mockClassName['fullClassName']);

    if ($class->isFinal()) {
        throw new RuntimeException(
            ...
        );
    }
    ...
    foreach ($methods as $methodName) {
        try {
            $method = $class->getMethod($methodName);

            if ($this->canMockMethod($method)) {
                ...
            }
            ...
        }

...

}

Checking whether a method can be mocked is delegated to another method of the Genetator class:

private function canMockMethod(ReflectionMethod $method)
{
    return !(
        $method->isConstructor() ||
        $method->isFinal() ||
        $this->isReturnTypeFinal($method) ||
        $method->isPrivate() ||
        $this->isMethodNameBlacklisted($method->getName())
    );
}

Automatic Dependency Injection

6.  Many dependency injection contains or frameworks use reflection to automatically instantiate and inject any dependencies (autowiring) when creating specific objects (usually when instantiating controllers).  For example, the Illuminate\Container\Container  (Laravel framework) class contains the build() method which includes the high-level logic of autowiring used by the container:

    /**
     * Instantiate a concrete instance of the given type.
     *
     * @param  string  $concrete
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
        // If the concrete type is actually a Closure, we will just execute it and
        // hand back the results of the functions, which allows functions to be
        // used as resolvers for more fine-tuned resolution of these objects.
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        $reflector = new ReflectionClass($concrete);

        // If the type is not instantiable, the developer is attempting to resolve
        // an abstract type such as an Interface of Abstract Class and there is
        // no binding registered for the abstractions so we need to bail out.
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();

        // If there are no constructors, that means there are no dependencies then
        // we can just resolve the instances of the objects right away, without
        // resolving any other types or dependencies out of these containers.
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        // Once we have all the constructor's parameters we can create each of the
        // dependency instances and then use the reflection instances to make a
        // new instance of this class, injecting the created dependencies in.
        $instances = $this->resolveDependencies(
            $dependencies
        );

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }