(Laravel) Dynamic dependency injection for interfa

2020-05-14 18:57发布

问题:

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()

Then I have two different implementations for this interface: Service1 and Service2, which implements the execute method properly.

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.

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 input from a previous step.

It means the user choose a service and based on that value I know if a can use Service1 or 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.

回答1:

Finally after some days researching and thinking alot about the best approach for this, using Laravel I finally solved.

I have to say that this was specially difficult in Laravel 5.2, because in this version the Session middleware 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.

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.

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:

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

Now, in the AppServiceProvider we can resolve the dependency in this way:

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:

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.

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.

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.

He personally created a golden article about this. Please read it because solve this issue completely and in a very right way.

laravel registry pattern



回答2:

The fact that you define that your controller works with ServiceInterface is ok

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



回答3:

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:

/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;
            }
        );
    }
}

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!



回答4:

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.

It has a case statement based on the argument.

It will use App::make(ServiceOne::class) or App::make(ServiceTwo::class).depending on which one is required.

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.