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

Posted

技术标签:

【中文标题】(Laravel) 接口的动态依赖注入,基于用户输入【英文标题】:(Laravel) Dynamic dependency injection for interface, based on user input 【发布时间】:2016-08-19 14:59:02 【问题描述】:

我目前在架构和实现方面面临一个非常有趣的困境。

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

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

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

现在是有趣的部分:

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

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

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

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

提前致谢。如果需要更多信息,请告诉我。

【问题讨论】:

一个好问题。 【参考方案1】:

您定义控制器与ServiceInterface 一起工作的事实是可以的

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

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

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') );

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

【讨论】:

谢谢。事实上,我只是想确定使用会话是否是一种正确的方法,我怀疑因为从 Laravel 容器中访问会话并不容易(解决依赖关系),但我刚刚发现实际上我可以使用工厂方法以便以更“自然”的方式执行此操作【参考方案2】:

最后,经过几天的研究和思考,使用 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()
    
        .......
    

有趣的部分是我有一个控制器,它的函数依赖于 Service 接口。不过,我需要根据用户输入将其动态解析为 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 类。这个类基本上使用传递给构造函数的参数来返回将用作服务接口实现的类的全名(带命名空间):

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 throw an exception because the dependency can not be resolved 
        throw new ResolverException;
    

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

祝你好运!

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

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

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

最后,它的工作原理是一样的,但它会让你更自然地工作。

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

他亲自为此创建了一个金色的article。请阅读它,因为它以非常正确的方式彻底解决了这个问题。

laravel registry pattern

【讨论】:

【参考方案3】:

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

它有一个基于论点的案例陈述。

它将使用App::make(ServiceOne::class)App::make(ServiceTwo::class)。取决于需要哪一个。

然后您可以将其注入您的控制器(或取决于工厂的服务)。

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

【讨论】:

是的,这是最好的方法。我使用了相同的方法,在原始回复中分享的文章之后:rizqi.id/laravel-registry-pattern【参考方案4】:

这是一个有趣的问题。我目前正在使用 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!

【讨论】:

以上是关于(Laravel) 接口的动态依赖注入,基于用户输入的主要内容,如果未能解决你的问题,请参考以下文章

Laravel 学习笔记:深入理解控制反转(IoC)和依赖注入(DI)

译深入研究 Laravel 的依赖注入容器

基于接口的动态代理

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

PHP 在Swoole中使用双IoC容器实现无污染的依赖注入

运用Unity实现依赖注入[结合简单三层实例]