web-dev-qa-db-fra.com

Laravel 4 - Constructeur parent enfant avec injection de dépendance

Je construis un CMS avec Laravel 4 et j'ai un contrôleur d'administration de base pour les pages d'administration qui ressemble à ceci:

class AdminController extends BaseController {

    public function __construct(UserAuthInterface $auth, MessagesInterface $message, ModuleManagerInterface $module)
    {
        $this->auth = $auth;
        $this->user = $this->auth->adminLoggedIn();
        $this->message = $message;
        $this->module = $module;
    }
}

J'utilise le conteneur IOC de Laravel pour injecter les dépendances de classe dans le constructeur. J'ai ensuite diverses classes de contrôleurs qui contrôlent les différents modules composant le CMS et chaque classe étend la classe admin. Par exemple:

class UsersController extends AdminController {

    public function home()
    {
        if (!$this->user)
        {
            return Redirect::route('admin.login');
        }
        $messages = $this->message->getMessages();
        return View::make('users::home', compact('messages'));
    }
}

Maintenant, cela fonctionne parfaitement bien que mon problème, qui est moins un problème et plus d'un problème d'efficacité, se produit lorsque j'ajoute un constructeur à la classe UsersController. Par exemple:

class UsersController extends AdminController {

    public function __construct(UsersManager $user)
    {
        $this->users = $users;
    }

    public function home()
    {
        if (!$this->user)
        {
        return Redirect::route('admin.login');
        }
        $messages = $this->message->getMessages();
        return View::make('users::home', compact('messages'));
    }
}

Puisque la classe enfant a maintenant un constructeur, cela signifie que le constructeur du parent n'est pas appelé et que les éléments dont dépend la classe enfant, tels que this->user, ne sont plus valides, ce qui provoque des erreurs. Je peux appeler la fonction de construction du contrôleur administrateur via parent::__construct(), mais comme je dois lui transmettre les dépendances de classe, je dois définir ces dépendances dans le constructeur enfant, ce qui donne un résultat ressemblant à ceci:

class UsersController extends AdminController {

    public function __construct(UsersManager $user, UserAuthInterface $auth, MessagesInterface $message, ModuleManagerInterface $module)
    {
        parent::__construct($auth, $messages, $module);
        $this->users = $users;
    }

    // Same as before
}

Maintenant, cela fonctionne bien en termes de fonctionnalité; Cependant, il ne me semble pas très efficace d'inclure les dépendances du parent dans chaque classe enfant ayant un constructeur. Il semble aussi assez désordonné. Est-ce que Laravel fournit un moyen de contourner ce problème ou PHP prend-il en charge un moyen d'appeler à la fois les constructeurs parent et enfant sans avoir à appeler parent::__construct() à partir de l'enfant?

Je sais que la question est longue, car ce n’est pas un problème, c’est une question d’efficacité, mais j’apprécie toutes les idées et/ou solutions.

Merci d'avance!

24
ArranJacques

Je sais que c’est une très vieille question, mais je viens de terminer une question similaire sur mon projet actuel et j’ai compris le problème. 

La question fondamentale sous-jacente est la suivante: 

Si j'étends une classe parent qui a un constructeur. Ce constructeur a injecté des dépendances et toutes ses dépendances sont déjà documentées dans le parent lui-même. Pourquoi dois-je inclure à nouveau les dépendances du parent dans ma classe enfant ?

J'ai rencontré le même problème. 

Ma classe parent nécessite 3 dépendances différentes. Ils sont injectés via le constructeur:

<?php namespace CodeShare\Parser;

use CodeShare\Node\NodeRepositoryInterface as Node;
use CodeShare\Template\TemplateRepositoryInterface as Template;
use CodeShare\Placeholder\PlaceholderRepositoryInterface as Placeholder;

abstract class BaseParser {

    protected $node;
    protected $template;
    protected $placeholder;


    public function __construct(Node $node, Template $template, Placeholder $placeholder){
        $this->node           = $node;
        $this->template       = $template;
        $this->placeholder    = $placeholder;
    }

La classe est une classe abstraite, donc je ne peux jamais instancier elle-même. Lorsque j'étends le cours, j'ai toujours besoin d'inclure toutes ces dépendances et leurs références use dans le constructeur de l'enfant:

<?php namespace CodeShare\Parser;

// Using these so that I can pass them into the parent constructor
use CodeShare\Node\NodeRepositoryInterface as Node;
use CodeShare\Template\TemplateRepositoryInterface as Template;
use CodeShare\Placeholder\PlaceholderRepositoryInterface as Placeholder;
use CodeShare\Parser\BaseParser;

// child class dependencies
use CodeShare\Parser\PlaceholderExtractionService as Extractor;
use CodeShare\Parser\TemplateFillerService as TemplateFiller;


class ParserService extends BaseParser implements ParserServiceInterface {

    protected $extractor;
    protected $templateFiller;

    public function __construct(Node $node, Template $template, Placeholder $placeholder, Extractor $extractor, TemplateFiller $templateFiller){
        $this->extractor      = $extractor;
        $this->templateFiller = $templateFiller;
        parent::__construct($node, $template, $placeholder);
    }

L'inclusion des instructions use pour les 3 dépendances parent dans chaque classe semblait être du code en double puisqu'elles sont déjà définies dans le constructeur parent. Mon idée était de supprimer les instructions parent use car elles devront toujours être définies dans la classe enfant qui étend le parent. 

Ce que j'ai compris, c'est que l'inclusion de la variable use pour les dépendances dans la classe parent et l'inclusion des noms de classe dans le constructeur du parent ne sont UNIQUEMENT requises pour l'indication de type dans le parent. 

Si vous supprimez les instructions use du parent et le nom de classe d'indices de type du constructeur de parents, vous obtenez:

<?php namespace CodeShare\Parser;

// use statements removed

abstract class BaseParser {

    protected $node;
    protected $template;
    protected $placeholder;

    // type hinting removed for the node, template, and placeholder classes
    public function __construct($node, $template, $placeholder){
        $this->node           = $node;
        $this->template       = $template;
        $this->placeholder    = $placeholder;
    }

Sans les instructions use et l'indication de type du parent, il ne peut plus garantir le type de classe transmis à son constructeur car il n'a aucun moyen de le savoir. Vous pouvez construire à partir de votre classe enfant avec n'importe quoi et le parent l'acceptera. 

Cela ressemble à une double saisie de code, mais dans votre cas, vous ne construisez pas avec les dépendances définies dans le parent, vous vérifiez que l'enfant envoie les types corrects. 

3
Chris Schmitz

Il y a un moyen .._____ lorsque BaseController résout automatiquement ses dépendances.

use Illuminate\Routing\Controller;
use Illuminate\Foundation\Application;

// Dependencies
use Illuminate\Auth\AuthManager;
use Prologue\Alerts\AlertsMessageBag;

class BaseController extends Controller {

    protected $authManager;
    protected $alerts;

    public function __construct(
        // Required for resolving
        Application $app,

        // Dependencies
        AuthManager $authManager = null,
        AlertsMessageBag $alerts = null
    )
    {
        static $dependencies;

        // Get parameters
        if ($dependencies === null)
        {
            $reflector = new \ReflectionClass(__CLASS__);
            $constructor = $reflector->getConstructor()
            $dependencies = $constructor->getParameters();
        }

        foreach ($dependencies as $dependency)
        {
            // Process only omitted optional parameters
            if (${$dependency->name} === null)
            {
                // Assign variable
                ${$dependency->name} = $app->make($dependency->getClass()->name);
            }
        }


        $this->authManager = $authManager;
        $this->alerts = $alerts;

        // Test it
        dd($authManager);
    }
}

Donc, dans le contrôleur enfant, vous ne transmettez que l'instance d'application:

class MyController extends BaseController {

    public function __construct(
        // Class dependencies resolved in BaseController
        //..

        // Application
        Application $app
    )
    {
        // Logic here
        //..


        // Invoke parent
        parent::__construct($app);
    }
}

Bien sûr, nous pourrions utiliser Facade pour l'application

3
piotr_cz

Il n'y a pas de solution parfaite et il est important de comprendre que ce n'est pas un problème avec Laravel lui-même. 

Pour gérer cela, vous pouvez faire l'une des trois choses suivantes:

  1. Transmettez les dépendances nécessaires au parent (quel était votre problème)

    // Parent
    public function __construct(UserAuthInterface $auth, MessagesInterface $message, ModuleManagerInterface $module)
    {
        $this->auth = $auth;
        $this->user = $this->auth->adminLoggedIn();
        $this->message = $message;
        $this->module = $module;
    }
    
    // Child
    public function __construct(UsersManager $user, UserAuthInterface $auth, MessagesInterface $message, ModuleManagerInterface $module)
    {
        $this->users = $users;
        parent::__construct($auth, $message, $module);
    }
    
  2. Résoudre automatiquement les dépendances dans la construction parente comme indiqué par @piotr_czdans sa réponse

  3. Créez les occurrences dans la construction parent au lieu de les transmettre en tant que paramètres (pour ne pas utiliser l'injection de dépendance):

    // Parent
    public function __construct()
    {
        $this->auth = App::make('UserAuthInterface');
        $this->user = $this->auth->adminLoggedIn();
        $this->message = App::make('MessagesInterface');
        $this->module = App::make('ModuleManagerInterface');
    }
    
    // Child
    public function __construct(UsersManager $user)
    {
        $this->users = $users;
        parent::__construct();
    }
    

Si vous voulez tester vos cours, la troisième solution sera plus difficile à tester. Je ne sais pas si vous pouvez vous moquer des classes en utilisant la deuxième solution, mais vous vous en moquez en utilisant la première solution.

2
Luís Cruz

J'ai rencontré le même problème lors de l'extension de mon contrôleur de base.

J'ai opté pour une approche différente des autres solutions présentées ici. Plutôt que de compter sur l'injection de dépendance, j'utilise app () -> make () dans le constructeur des parents.

class Controller
{
    public function __construct()
    {
        $images = app()->make(Images::class);
    }
}

Cette approche plus simple peut comporter des inconvénients - rendre éventuellement le code moins vérifiable.

0
Adam Moore

Vous devez transmettre les dépendances au constructeur parent pour qu'elles soient disponibles dans l'enfant. Il n'y a aucun moyen d'injecter les dépendances sur la construction parent lorsque vous l'instanciez via l'enfant.

0
bgallagh3r