Question

How can I check if an object will be successfully instantiated with the given argument, without actually creating the instance?

Actually I'm only checking (didn't tested this code, but should work fine...) the number of required parameters, ignoring types:

// Filter definition and arguments as per configuration
$filter = $container->getDefinition($serviceId);
$args   = $activeFilters[$filterName];

// Check number of required arguments vs arguments in config
$constructor = $reflector->getConstructor();

$numRequired  = $constructor->getNumberOfRequiredParameters();
$numSpecified = is_array($args) ? count($args) : 1;

if($numRequired < $numSpecified) {
    throw new InvalidFilterDefinitionException(
        $serviceId,
        $numRequired,
        $numSpecified
    );
}

EDIT: $constructor can be null...

Was it helpful?

Solution

The short answer is that you simply cannot determine if a set of arguments will allow error-free instantiation of a constructor. As commenters have mentioned above, there's no way to know for sure if a class can be instantiated with a given argument list because there are runtime considerations that cannot be known without actually attempting instantiation.

However, there is value in trying to instantiate a class from a list of constructor arguments. The most obvious use-case for this sort of operation is a configurable Dependency Injection Container (DIC). Unfortunately, this is a much more complicated operation than the OP suggests.

We need to determine for each argument in a supplied definition array whether or not it matches specified type-hints from the constructor method signature (if the method signature actually has type-hints). Also, we need to resolve how to treat default argument values. Additionally, for our code to be of any real use we need to allow the specification of "definitions" ahead of time for instantiating a class. A sophisticated treatment of the problem will also involve a pool of reflection objects (caching) to minimize the performance impact of repeatedly reflecting things.

Another hurdle is the fact that there's no way to access the type-hint of a reflected method parameter without calling its ReflectionParameter::getClass method and subsequently instantiating a reflection class from the returned class name (if null is returned the param has no type-hint). This is where caching generated reflections becomes particularly important for any real-world use-case.

The code below is a severely stripped-down version of my own string-based recursive dependency injection container. It's a mixture of pseudo-code and real-code (if you were hoping for free code to copy/paste you're out of luck). You'll see that the code below matches the associative array keys of "definition" arrays to the parameter names in the constructor signature.

The real code can be found over at the relevant github project page.

class Provider {

    private $definitions;

    public function define($class, array $definition) {
        $class = strtolower($class);
        $this->definitions[$class] = $definition;
    }

    public function make($class, array $definition = null) {
        $class = strtolower($class);

        if (is_null($definition) && isset($this->definitions[$class])) {
            $definition = $this->definitions[$class];
        }

        $reflClass = new ReflectionClass($class);
        $instanceArgs = $this->buildNewInstanceArgs($reflClass);


        return $reflClass->newInstanceArgs($instanceArgs);
    }

    private function buildNewInstanceArgs(
        ReflectionClass $reflClass,
        array $definition
    ) {
        $instanceArgs = array();


        $reflCtor = $reflClass->getConstructor();

        // IF no constructor exists we're done and should just
        // return a new instance of $class:
        // return $this->make($reflClass->name);
        // otherwise ...

        $reflCtorParams = $reflCtor->getParameters();

        foreach ($reflCtorParams as $ctorParam) {
            if (isset($definition[$ctorParam->name])) {
                $instanceArgs[] = $this->make($definition[$ctorParam->name]);
                continue;
            }

            $typeHint = $this->getParameterTypeHint($ctorParam);

            if ($typeHint && $this->isInstantiable($typeHint)) {
                // The typehint is instantiable, go ahead and make a new
                // instance of it
                $instanceArgs[] = $this->make($typeHint);
            } elseif ($typeHint) {
                // The typehint is abstract or an interface. We can't
                // proceed because we already know we don't have a 
                // definition telling us which class to instantiate
                throw Exception;
            } elseif ($ctorParam->isDefaultValueAvailable()) {
                // No typehint, try to use the default parameter value
                $instanceArgs[] = $ctorParam->getDefaultValue();
            } else {
                // If all else fails, try passing in a NULL or something
                $instanceArgs[] = NULL;
            }
        }

        return $instanceArgs;
    }

    private function getParameterTypeHint(ReflectionParameter $param) {
        // ... see the note about retrieving parameter typehints
        // in the exposition ...
    }
    private function isInstantiable($class) {
        // determine if the class typehint is abstract/interface
        // RTM on reflection for how to do this
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top