Laravel 4 - Child constructor call parent constructor with dependency injection

后端 未结 6 1815
死守一世寂寞
死守一世寂寞 2021-02-05 02:53

I\'m building a CMS using Laravel 4 and I have a base admin controller for the admin pages that looks something like this:

class AdminController extends BaseCont         


        
相关标签:
6条回答
  • 2021-02-05 03:21

    I know this is a super old question, but I just finished grinding on a similar question on my current project and came to an understanding with the issue at hand.

    The basic underlying question here is:

    If I'm extending a parent class that has a constructor. That constructor has injected dependancies and all of it's dependencies are already documented in the parent itself. Why do I have to include the parent's dependencies again in my child class?

    I ran into this same issue.

    My parent class requires 3 different dependencies. They're injected via the constructor:

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

    The class is an abstract class so I can never instantiate it on it's own. When I extend the class, I still need to include all of those dependencies and their use references in the child's constructor:

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

    Including the use statements for the 3 parent dependencies in each class seemed like duplicate code since they're already defined in the parent constructor. My thought was to remove the parent use statements as they'll always need to be defined in the child class that extends the parent.

    What I realized is that including the use for the dependencies in the parent class and including the class names in the parent's constructor are ONLY needed for type hinting in the parent.

    If you remove the use statements from the parent and the type hinted class name from the parents constructor, you get:

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

    Without the use statements and type hinting from the parent, it can no longer guarantee the type of class being passed to it's constructor because it has no way of knowing. You could construct from your child class with anything and the parent would accept it.

    It does seem like double entry of code, but really in your paren't you're not constructing with the dependencies laid out in the parent, you're verifying that the child is sending in the correct types.

    0 讨论(0)
  • 2021-02-05 03:22

    I came across the same issue, when extending my base Controller.

    I opted for a different approach than the other solutions shown here. Rather than rely on dependency injection, I'm using app()->make() in the parents constructor.

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

    There may be downsides to this simpler approach - possibly making the code less testable.

    0 讨论(0)
  • 2021-02-05 03:28

    You must pass the dependencies to the parent constructor to have them available in the child. There is no way to inject the dependencies on the parent construct when you instantiate it via the child.

    0 讨论(0)
  • 2021-02-05 03:29

    There's a way. When BaseController autoresolves it's dependecies.

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

    So in child controller you pass only Application instance:

    class MyController extends BaseController {
    
        public function __construct(
            // Class dependencies resolved in BaseController
            //..
    
            // Application
            Application $app
        )
        {
            // Logic here
            //..
    
    
            // Invoke parent
            parent::__construct($app);
        }
    }
    

    Of course, we could use Facade for application

    0 讨论(0)
  • 2021-02-05 03:30

    There isn't a perfect solution and it's important to understand that this isn't an issue with Laravel itself.

    To manage this, you can do one of three things:

    1. Pass the necessary dependencies to the parent (which was your issue)

      // 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. Auto resolve the dependencies in the parent construct as stated by @piotr_cz in his answer

    3. Create the instances in the parent construct instead of passing them as parameters (so you don't use Dependency Injection):

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

    If you want to test your classes, the third solution will be more difficult to test. I'm unsure if you can mock the classes using the second solution, but you mock them using the first solution.

    0 讨论(0)
  • 2021-02-05 03:33

    I've also ended up with this problem and cleared this mess by not calling constructor at child class and use extra needed dependencies inside the function parameters.

    It will work with controllers because you won't need to call these functions manually and you can inject everything there. So common dependencies goes to the parent and less needed will be added to the methods itself.

    0 讨论(0)
提交回复
热议问题