thephpleague / container

Small but powerful dependency injection container

Home Page:http://container.thephpleague.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Suggestion: Call method class, injecting constructor and method params

LuanMaik opened this issue · comments

I'm try to use this container with Slim framework, but I'm trying to get the less verbose possible in call the method controller in route definition, but the lib is limited to constructor injection.

My actual code:

$container = new \League\Container\Container();
$container->delegate((new League\Container\ReflectionContainer())->cacheResolutions(false));
// .. define dependencies
Slim\Factory\AppFactory::setContainer($container);

$app = Slim\Factory\AppFactory::create();

$app->get('/api/v1/users', function() {
    // $this = \League\Container\Container
    // Create the controller instance with constructor dependencies
    $controller = $this->get(\App\Controllers\UserController::class);
    // But I need resolve the method params  before call it
    $bar = $this->get(\Foo\Bar::class);
    return $controller->list($bar);
});

My proposal is create a method that execute a class method, resolving all dependencies of constructor and method.

My code with the proposal:

$container = new \League\Container\Container();
$container->delegate((new League\Container\ReflectionContainer())->cacheResolutions(false));
// .. define dependencies
Slim\Factory\AppFactory::setContainer($container);

$app = Slim\Factory\AppFactory::create();

$app->get('/api/v1/users', function() {
    // $this = \League\Container\Container
    // Resolves constructor and method dependencies, then execute it
    return $this->callClassMethod(\App\Controllers\UserController::class, 'list');
});

The new method:

// src/Container.php

class Container implements ContainerInterface
{
     // ...

    /**
     * Execute a class method, injecting all dependencies necessaries in constructor class and method params
     *
     * @param string $className
     * @param string $methodName
     * @param bool $new
     * @return mixed
     * @throws \ReflectionException
     */
    public function callClassMethod(string $className, string $methodName, bool $new = false)
    {
        if (!class_exists($className)) {
           throw new \InvalidArgumentException("The class {$className} not exist.");
        }

        if (!method_exists($className, $methodName)) {
            throw new \InvalidArgumentException("The method '{$methodName}' not exist in class {$className}.");
        }

        $classInstance = $this->get($className, $new);

        $reflectionMethod = new ReflectionMethod($className, $methodName);
        $methodParams = $reflectionMethod->getParameters();
        $resolvedMethodParams = [];
        foreach ($methodParams as $param) {
            if (empty($param->getClass())) {
                throw new \Exception("It's not possible resolve the param '\${$param->getName()}' in '{$className}::{$methodName}'.");
            }
            $resolvedMethodParams[] = $this->get($param->getClass()->getName(), $new);
        }

        return call_user_func_array([$classInstance, $methodName], $resolvedMethodParams);
    }
}

I can use this approach separately, but i think that this feature would be interesting to help with routers libraries implementations like in my case.
The PHP-DI lib, has the call() method, thats seems my proposal, but it resolves the dependencies using 'resolved dependencies cache'.

ReflectionContainer already has a call method on it, but uses reflection exclusively iirc, not really something I'd want to add to the main container though.

It's not really an intended use, but as you've shown above, it's easy enough to put something together if someone wants to do it this way.