(Laravel)基于用户输入的接口动态依赖注入

时间:2016-04-26 01:24:06

标签: php laravel dependency-injection interface laravel-5

我目前面临着一个非常有趣的架构和实施困境。

我有一个名为ServiceInterface的接口,它有一个名为execute()

的方法

然后我对这个接口有两个不同的实现:Service1Service2,它们正确地实现了执行方法。

我有一个名为MainController的控制器,这个控制器有一个"类型提示"对于ServiceInterface依赖注入),这意味着可以将Service1Service2都称为该依赖注入的解析。

现在有趣的部分:

我不知道要使用哪些实现(Service1Service2),因为我只知道我是否可以使用其中一个或其他来自用户输入上一步。

这意味着用户选择了一项服务,根据该值,我知道是否可以使用Service1Service2

我目前正在使用会话值解决依赖注入,所以根据值我返回一个实例或其他,但我真的认为这不是一个好方法。

请告诉我,如果你遇到类似的问题,你如何解决,或者我能做些什么才能以正确的方式实现这一目标。

提前致谢。如果需要进一步的信息,请告诉我。

4 个答案:

答案 0 :(得分:14)

经过几天研究和思考很多关于这方面的最佳方法,使用Laravel我终于解决了。

我必须说这在Laravel 5.2中特别困难,因为在这个版本中, Session 中间件只在路由中使用的控制器中执行,这意味着如果对于某些我使用控制器(没有链接死记硬背)并尝试访问会话的原因是不可能的。

所以,因为我不能使用会话我决定使用URL参数,这里有解决方法,我希望你们中的一些人发现它很有用。

所以,你有一个界面:

interface Service
{
    public function execute();
}

然后是接口的几个实现:

服务一:

class ServiceOne implements Service
{
    public function execute()
    {
        .......
    }
}

服务二。

class ServiceTwo implements Service
{
    public function execute()
    {
        .......
    }
}

现在有趣的部分:有一个控制器,其功能与服务接口具有依赖关系,但我需要根据使用输入将其解析为ServiceOne或ServiceTwo。所以:

控制器

class MyController extends Controller
{
    public function index(Service $service, ServiceRequest $request)
    {
        $service->execute();
        .......
    }
}

请注意ServiceRequest,验证请求已经具有解析依赖关系所需的参数(称之为'service_name'

现在,在AppServiceProvider中,我们可以通过以下方式解决依赖关系:

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {

    }

    public function register()
    {
        //This specific dependency is going to be resolved only if
        //the request has the service_name field stablished
        if(Request::has('service_name'))
        {
            //Obtaining the name of the service to be used (class name)
            $className = $this->resolveClassName(Request::get('service_name')));

            $this->app->bind('Including\The\Namespace\For\Service', $className);
        }
    }

    protected function resolveClassName($className)
    {
        $resolver = new Resolver($className);
        $className = $resolver->resolveDependencyName();
        return $className;
    }
}

所以现在所有的责任都是Resolver类,这个类基本上使用传递给contructor的参数来返回将用作Service接口实现的类的fullname(带命名空间): / p>

class Resolver
{
    protected $name;
    public function __construct($className)
    {
        $this->name = $className;
    }

    public function resolveDependencyName()
    {
        //This is just an example, you can use whatever as 'service_one'
        if($this->name === 'service_one')
        {
            return Full\Namespace\For\Class\Implementation\ServiceOne::class;
        }

        if($this->name === 'service_two')
        {
            return Full\Namespace\For\Class\Implementation\ServiceTwo::class;
        }
        //If none, so whrow an exception because the dependency can not be resolved 
        throw new ResolverException;
    }
}

嗯,我真的希望对你们中的一些人有所帮助。

祝福!

----------编辑-----------

我只是意识到,直接使用请求数据并不是一个好主意,在Laravel的容器内部,从长远来看它确实会带来一些麻烦。

最好的方法是直接注册支持的所有可能实例(serviceone和servicetwo),然后直接从控制器或中间件解析其中一个,那么控制器“谁决定”使用什么服务(来自所有可用的)基于请求的输入。

最后它的工作原理相同,但它会让你以更自然的方式工作。

我要感谢rizqi。来自Laravel闲聊的问题频道的用户。

他亲自为此创造了一个金色的article。请阅读它,因为完全以正确的方式解决了这个问题。

laravel registry pattern

答案 1 :(得分:3)

您定义控制器与ServiceInterface一起使用的事实是正确的

如果你必须根据前一步骤选择服务的具体实现(正如我所理解的那样,在先前的请求中发生),将值存储在会话或数据库中也是正确的,因为你没有替代方案:要选择实现,您必须知道输入的值

重要的一点是在一个地方“隔离”具体实现的解决方案与输入值:例如,创建一个方法,将此值作为参数,并从值返回服务的具体实现: / p>

public function getServiceImplementation($input_val)
{
    switch($input_val)
    {
        case 1 : return new Service1();
        case 2 : return new Service2();
    }    
}

并在您的控制器中:

public function controllerMethod()
{
    //create and assign the service implementation
    $this->service = ( new ServiceChooser() )->getServiceImplementation( Session::get('input_val') );
}

在此示例中,我使用了另一个类来存储方法,但您可以将方法放在控制器中或使用简单工厂模式,具体取决于应用程序应在应用程序中解析的位置

答案 2 :(得分:2)

这是一个有趣的问题。我目前正在使用Laravel 5.5并且一直在考虑它。我还希望我的服务提供者根据用户输入返回一个特定的类(实现一个接口)。我认为最好手动传递来自控制器的输入,这样就可以更容易地看到发生了什么。我还会在配置中存储类名的可能值。 因此,基于您在上面定义的服务类和接口,我提出了这个:

/config/services.php

return [
    'classes': [
        'service1' => 'Service1',
        'service2' => 'Service2',
    ]
]

/app/Http/Controllers/MainController.php

public function index(ServiceRequest $request)
{
    $service = app()->makeWith(ServiceInterface::class, ['service'=>$request->get('service)]);
    // ... do something with your service
}

/app/Http/Requests/ServiceRequest.php

public function rules(): array
    $availableServices = array_keys(config('services.classes'));
    return [
        'service' => [
            'required',
            Rule::in($availableServices)
        ]
    ];
}

/app/Providers/CustomServiceProvider.php

class CustomServiceProvider extends ServiceProvider
{
    public function boot() {}

    public function register()
    {
        // Parameters are passed from the controller action
        $this->app->bind(
            ServiceInterface::class,
            function($app, $parameters) {
                $serviceConfigKey = $parameters['service'];
                $className = '\\App\\Services\\' . config('services.classes.' . $serviceConfigKey);
                return new $className;
            }
        );
    }
}

这样我们可以验证输入以确保传递有效服务,然后控制器处理将Request对象的输入传递给ServiceProvider。我只是想到在维护这段代码时,将会清楚发生了什么,而不是直接在ServiceProvider中使用请求对象。 PS请记住注册CustomServiceProvider!

答案 3 :(得分:1)

我发现处理此问题的最佳方法是使用工厂模式。您可以创建一个类ServiceFactory并且它有一个方法create()它可以接受一个参数,该参数用于动态选择要实例化的具体类。

它有一个基于参数的case语句。

它将使用App::make(ServiceOne::class)App::make(ServiceTwo::class)。具体取决于哪一个是必需的。

然后您可以将此注入您的控制器(或依赖于工厂的服务)。

然后,您可以在服务单元测试中模拟它。