(Laravel) 界面的动态依赖注入,基于用户输入
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/36853791/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
(Laravel) Dynamic dependency injection for interface, based on user input
提问by JuanDMeGon
I am currently facing a very interesting dilemma with my architecture and implementation.
我目前在我的架构和实现方面面临着一个非常有趣的困境。
I have an interface called ServiceInterface
which have a method called execute()
我有一个调用的接口ServiceInterface
,它有一个调用的方法execute()
Then I have two different implementations for this interface: Service1
and Service2
, which implements the execute method properly.
然后我对这个接口有两种不同的实现:Service1
and Service2
,它正确地实现了 execute 方法。
I have a controller called MainController
and this controller has a "type-hint" for the ServiceInterface
(dependency injection), it means that both, Service1
and Service2
, can be called as resolution for that dependency injection.
我有一个名为的控制器MainController
,这个控制器有一个ServiceInterface
(依赖注入)的“类型提示” ,这意味着,Service1
和Service2
,都可以被称为该依赖注入的解决方案。
Now the fun part:
现在是有趣的部分:
I do not know which of those implementations to use (Service1
or Service2
) because I just know if I can use one or other based on a user inputfrom a previous step.
我不知道要使用哪些实现(Service1
或Service2
),因为我只知道我是否可以根据上一步的用户输入使用一个或其他实现。
It means the user choose a service and based on that value I know if a can use Service1
or Service2
.
这意味着用户选择一项服务,并根据该值我知道是否可以使用Service1
或Service2
。
I am currently solving the dependency injection using a session value, so depending of the value I return an instance or other, BUT I really think that it is not a good way to do it.
我目前正在使用会话值解决依赖项注入,因此根据值我返回一个实例或其他,但我真的认为这不是一个好方法。
Please, let me know if you faced something similar and, how do you solve it, or what can I do to achieve this in the right way.
如果您遇到类似的问题,请告诉我,您如何解决它,或者我能做些什么来以正确的方式实现这一目标。
Thanks in advance. Please let me know if further information is required.
提前致谢。如果需要更多信息,请告诉我。
采纳答案by JuanDMeGon
Finally after some days researching and thinking alot about the best approach for this, using Laravel I finally solved.
最后经过几天的研究和思考最好的方法,使用 Laravel 我终于解决了。
I have to say that this was specially difficult in Laravel 5.2
, because in this version the Sessionmiddleware only is executed in the controllers used in a route, it means that if for some reason I used a controller (not linked for a rote) and try to get access to the session it is not going to be possible.
我不得不说,这是特别困难的Laravel 5.2
,因为在这个版本的会话中间件仅在路由使用的控制器上执行,这意味着,如果由于某种原因,我用了一个控制器(未连接的死记硬背),并尝试访问会话是不可能的。
So, because I can not use the session I decided to use URL parameters, here you have the solution approach, I hope some of you found it useful.
所以,因为我不能使用会话我决定使用 URL 参数,这里你有解决方法,我希望你们中的一些人发现它有用。
so, you have an interface:
所以,你有一个界面:
interface Service
{
public function execute();
}
Then a couple of implementations for the interface:
然后是接口的几个实现:
The service one:
服务一:
class ServiceOne implements Service
{
public function execute()
{
.......
}
}
The service two.
服务二。
class ServiceTwo implements Service
{
public function execute()
{
.......
}
}
Now the interesting part: have a controller with a function that have a dependency with the Service interface BUT I need to resolve it dinamically to ServiceOne or ServiceTwo based in a use input. So:
现在有趣的部分是:有一个控制器,该控制器的功能与 Service 接口具有依赖关系,但我需要根据使用输入将其动态解析为 ServiceOne 或 ServiceTwo。所以:
The controller
控制器
class MyController extends Controller
{
public function index(Service $service, ServiceRequest $request)
{
$service->execute();
.......
}
}
Please note that ServiceRequest, validated that the request already have the parameter that we need to resolve the dependency (call it 'service_name'
)
请注意 ServiceRequest,验证请求已经具有我们需要解析依赖项的参数(调用它'service_name'
)
Now, in the AppServiceProvider we can resolve the dependency in this way:
现在,在 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;
}
}
So now all the responsibilty is for the Resolver class, this class basically use the parameter passed to the contructor to return the fullname (with namespace) of the class that is going to be used as a implementation of the Service interface:
所以现在所有的责任都在 Resolver 类上,这个类基本上使用传递给构造函数的参数来返回将用作 Service 接口实现的类的全名(带命名空间):
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;
}
}
Well, I really hope it help to some of you.
好吧,我真的希望它对你们中的一些人有所帮助。
Best wishes!
最好的祝愿!
---------- EDIT -----------
- - - - - 编辑 - - - - - -
I just realize, that it is not a good idea to use directly the request data, inside the container of Laravel, it really is going to make some trouble in the long term.
我才意识到,直接使用请求数据不是一个好主意,在Laravel的容器内部,从长远来看确实会造成一些麻烦。
The best way is to directly register all the possible instances suported (serviceone and servicetwo) and then resolve one of them directly from a controller or a middleware, so then is the controller "who decides" what service to use (from all the available) based on the input from the request.
最好的方法是直接注册所有可能支持的实例(serviceone 和 servicetwo),然后直接从控制器或中间件解析其中一个,然后由控制器“决定”使用什么服务(从所有可用的)基于请求的输入。
At the end it works at the same, but it is going to allow you to work in a more natural way.
最后它的工作原理是一样的,但它会让你以更自然的方式工作。
I have to say thanks to rizqi. A user from the questions channel of the slack chat of Laravel.
不得不说感谢rizqi。Laravel slack chat 提问频道的一位用户。
He personally created a golden articleabout this. Please read it because solve this issue completely and in a very right way.
回答by Moppo
The fact that you define that your controller works with ServiceInterface
is ok
您定义您的控制器的工作原理与事实ServiceInterface
是确定
If you have to choose the concrete implementation of the service basing on a previous step (that, as i've understood, happens in a previous request) storing the value in session or in database is right too, as you have no alternative: to choose the implementation you have to know the value of the input
如果您必须根据上一步(据我所知,发生在先前的请求中)选择服务的具体实现,那么将值存储在会话或数据库中也是正确的,因为您别无选择:选择实现你必须知道输入的值
The important point is to 'isolate' the resolution of the concrete implementation from the input value in one place: for example create a method that takes this value as a parameter and returns the concrete implementation of the service from the value:
重要的一点是在一个地方将具体实现的解析与输入值“隔离”:例如,创建一个方法,将该值作为参数并从该值中返回服务的具体实现:
public function getServiceImplementation($input_val)
{
switch($input_val)
{
case 1 : return new Service1();
case 2 : return new Service2();
}
}
and in your controller:
并在您的控制器中:
public function controllerMethod()
{
//create and assign the service implementation
$this->service = ( new ServiceChooser() )->getServiceImplementation( Session::get('input_val') );
}
In this example i've used a different class to store the method, but you can place the method in the controller or use a Simple Factory pattern, depending on where the service should be resolved in your application
在此示例中,我使用了不同的类来存储方法,但您可以将该方法放在控制器中或使用简单工厂模式,具体取决于应在应用程序中解析服务的位置
回答by omarjebari
It's an interesting problem. I'm currently using Laravel 5.5 and have been mulling it over. I also want my service provider to return a specific class (implementing an interface) based upon user input. I think it's better to manually pass the input from the controller so it's easier to see what's going on. I would also store the possible values of the class names in the config. So based upon the Service classes and interface you've defined above i came up with this:
这是一个有趣的问题。我目前正在使用 Laravel 5.5 并且一直在考虑它。我还希望我的服务提供者根据用户输入返回一个特定的类(实现一个接口)。我认为最好手动传递来自控制器的输入,这样更容易看到发生了什么。我还将在配置中存储类名的可能值。所以基于你在上面定义的服务类和接口,我想出了这个:
/config/services.php
/config/services.php
return [
'classes': [
'service1' => 'Service1',
'service2' => 'Service2',
]
]
/app/Http/Controllers/MainController.php
/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
/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
/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;
}
);
}
}
This way we can validate the input to ensure we are passing a valid service, then the controller handles passing the input from the Request object into the ServiceProvider. I just think when it comes to maintaining this code it will be clear what is going on as opposed to using the request object directly in the ServiceProvider. PS Remember to register the CustomServiceProvider!
通过这种方式,我们可以验证输入以确保我们传递的是有效的服务,然后控制器处理将输入从 Request 对象传递到 ServiceProvider。我只是认为在维护此代码时,与直接在 ServiceProvider 中使用请求对象相比,发生了什么会很清楚。PS记得注册CustomServiceProvider!
回答by wired00
I find the best way to deal with this is using a factory pattern. You can create a class say ServiceFactory
and it has a single method create()
it can accept an argument which is used to dynamically choose which concrete class to instantiate.
我发现处理这个问题的最好方法是使用工厂模式。您可以创建一个类 sayServiceFactory
并且它有一个方法create()
可以接受一个参数,该参数用于动态选择要实例化的具体类。
It has a case statement based on the argument.
它有一个基于参数的 case 语句。
It will use App::make(ServiceOne::class)
or App::make(ServiceTwo::class)
.depending on which one is required.
它将使用App::make(ServiceOne::class)
或 .App::make(ServiceTwo::class)
取决于需要哪个。
You are then able to inject this into your controller (or service which depends on the factory).
然后您可以将其注入您的控制器(或取决于工厂的服务)。
You can then mock it in a service unit test.
然后,您可以在服务单元测试中模拟它。