2016-04-26 21 views
9

Attualmente sto affrontando un dilemma molto interessante con mia architettura e l'implementazione.iniezione di dipendenza (laravel) dinamico per l'interfaccia, in base all'input dell'utente

devo un'interfaccia chiamata ServiceInterface cui con un metodo chiamato execute()

Poi ho due diverse implementazioni per questa interfaccia: Service1 e Service2, che implementa il metodo eseguito correttamente.

ho un controller chiamato MainController e questo controllore ha un "tipo suggerimento" per la ServiceInterface (dipendenza iniezione), significa che entrambi, Service1 e Service2, può essere chiamato come risoluzione per tale iniezione di dipendenza.

Ora la parte divertente:

non so quale di queste implementazioni da utilizzare (Service1 o Service2) perché ho appena so se posso usare uno o l'altro sulla base di un input dell'utente da un passo precedente.

Significa che l'utente sceglie un servizio e in base a tale valore so se è possibile utilizzare Service1 o Service2.

Attualmente sto risolvendo l'iniezione di dipendenza utilizzando un valore di sessione, quindi a seconda del valore restituisco un'istanza o altro, MA penso davvero che non sia un buon modo per farlo.

Ti prego, fatemi sapere se avete dovuto affrontare qualcosa di simile e, come si fa a risolverlo, o che cosa posso fare per raggiungere questo obiettivo nel modo giusto.

Grazie in anticipo. Per favore fatemi sapere se sono richieste ulteriori informazioni.

+0

Una buona domanda. – simhumileco

risposta

8

finalmente dopo qualche giorno che ricercano e di pensare molto circa l'approccio migliore per questo, utilizzando laravel ho finalmente risolto.

devo dire che questo è stato particolarmente difficile in Laravel 5.2, perché in questa versione la sessione middleware viene eseguito solo nei controllori utilizzati in un percorso, significa che se per qualche motivo ho utilizzato un controller (non legato per un rote) e cercare di ottenere l'accesso alla sessione non sarà possibile.

Quindi, poiché non posso utilizzare la sessione, ho deciso di utilizzare i parametri URL, ecco l'approccio della soluzione, spero che alcuni di voi l'abbiano trovato utile.

Quindi, avete un'interfaccia:

interface Service 
{ 
    public function execute(); 
} 

Poi un paio di implementazioni per l'interfaccia:

Il servizio one:

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

Il servizio due.

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

Ora la parte interessante: ha un controller con una funzione che hanno una dipendenza con l'interfaccia di servizio ma ho bisogno di risolverlo dinamicamente a ServiceOne o ServiceTwo sede in un ingresso uso. Quindi:

Il controller

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

prega di notare che ServiceRequest, convalidato che la richiesta già il parametro che abbiamo bisogno di risolvere la dipendenza (lo chiamano 'service_name')

Ora, nel AppServiceProvider siamo in grado di risolvere la dipendenza in questo modo:

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

Così ora tutto il responsibilty è per la classe Resolver, questo cl ass fondamentalmente utilizzare il parametro passato alla contructor di restituire il fullname (con spazio dei nomi) della classe che sta per essere usato come un'implementazione dell'interfaccia Servizio:

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

Beh, spero davvero che aiuta ad alcuni di voi.

I migliori auguri!

---------- EDIT -----------

ho appena rendo conto, che non è una buona idea di utilizzare direttamente i dati di richiesta, all'interno del container di Laravel, farà davvero dei problemi a lungo termine.

Il modo migliore è registrare direttamente tutte le possibili istanze suportate (serviceone e servicetwo) e quindi risolverne una direttamente da un controller o un middleware, quindi il controller "che decide" quale servizio utilizzare (da tutto il disponibile) in base all'input della richiesta.

Alla fine funziona allo stesso modo, ma ti permetterà di lavorare in modo più naturale.

Devo dire grazie a rizqi. Un utente dal canale delle domande della chat di Laravel.

Ha creato personalmente un oro article su questo. Per favore leggilo perché risolva questo problema completamente e in modo molto corretto.

laravel registry pattern

3

Il fatto che si definisce che il controller funziona con ServiceInterface è ok

Se dovete scegliere l'attuazione concreta del basando servizio su un passaggio precedente (che, come ho capito, accade in un precedente richiesta) memorizzare il valore in sessione o nel database è giusto, dato che non hai alternative: per scegliere l'implementazione devi conoscere il valore dell'input

Il punto importante è "isolare" la risoluzione del calcestruzzo implementazione dal valore di ingresso in un unico luogo: ad esempio, creare un metodo che prende questo valore come parametro e restituisce l'attuazione concreta del servizio da parte del Valore:

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

e nel controllore:

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

In questo esempio ho usato una classe diversa per memorizzare il metodo, ma è possibile inserire il metodo nel controller o utilizzare un semplice fabbrica modello, a seconda di dove il servizio dovrebbe essere risolto nella propria applicazione

+1

Grazie per quello. In effetti volevo solo essere sicuro che se usare la sessione fosse un approccio corretto, dubitavo perché ottenere l'accesso alla sessione non è facile dal container di Laravel (risoluzione delle dipendenze), ma ho appena scoperto che in effetti posso usare una fabbrica approccio per farlo in modo più "naturale" – JuanDMeGon

+0

@JuanDMeGon: prego – Moppo

1

Trovo il modo migliore per affrontare questo sta usando un modello di fabbrica. È possibile creare una classe dire ServiceFactory e ha un unico metodo create() può accettare un argomento che viene utilizzato per scegliere dinamicamente quale classe concreta istanziare.

Ha un'istruzione case basata sull'argomento.

Utilizzerà App::make(ServiceOne::class) o App::make(ServiceTwo::class). A seconda di quale è richiesto.

Si è quindi in grado di iniettare questo nel controller (o un servizio che dipende dalla fabbrica).

È quindi possibile simularlo in un test dell'unità di servizio.

+0

Sì, è il modo migliore per andare. Ho usato lo stesso approccio, seguendo l'articolo condiviso sulla risposta originale: http://rizqi.id/laravel-registry-pattern – JuanDMeGon

1

È un problema interessante. Attualmente sto usando Laravel 5.5 e lo sto rimuginando. Voglio anche che il mio fornitore di servizi restituisca una classe specifica (implementando un'interfaccia) basata sull'input dell'utente. Penso che sia meglio passare manualmente l'input dal controller in modo che sia più facile vedere cosa sta succedendo. Vorrei anche memorizzare i valori possibili dei nomi di classe nella configurazione. Così basano sugli classi di servizio e l'interfaccia che hai definito in precedenza mi è venuta con questo:

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

questo modo possiamo convalidare l'input per garantire stiamo passando un servizio valido, allora il controllore gestisce l'input passa dall'oggetto Request nel ServiceProvider. Penso solo che quando si tratta di mantenere questo codice sarà chiaro cosa sta succedendo invece di usare l'oggetto richiesta direttamente nel ServiceProvider. PS Ricordarsi di registrare CustomServiceProvider!