Laravel 4 - 子类构造函数通过依赖注入调用父类构造函数

32

我正在使用 Laravel 4 构建 CMS,并且我有一个用于管理页面的基本管理员控制器,看起来类似于这样:

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

我正在使用Laravel的IOC容器将类依赖项注入构造函数中。然后,我有各种控制器类来控制组成CMS的不同模块,并且每个类都扩展自管理员类。例如:

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

现在这个代码完全可以运行,但我的一个小问题(不是问题而是效率问题)出现在我给UsersController类添加构造函数之后。例如:

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

由于子类现在有一个构造函数,这意味着父类的构造函数不会被调用,因此子类依赖的一些东西,例如this->user不再有效,导致错误。我可以通过parent::__construct()调用管理员控制器的构造函数,但是由于我需要向其传递类依赖项,因此我需要在子类构造函数中设置这些依赖项,结果会看起来像这样:

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
}

就功能而言,这样做是可以正常工作的;但是我认为在每个具有构造函数的子类中都必须包含父类的依赖关系并不是很高效。它看起来也很混乱。 Laravel 是否提供了解决此问题的方法,或者 PHP 是否支持一种既能调用父类又能调用子类构造函数而不必从子类调用 parent::__construct() 的方法?

我知道这是一个长问题,实际上不是一个问题,只是我对效率有点强迫症,但我感激任何想法和/或解决方案。

提前致谢!


1
构造函数不会被继承。所以你已经回答了自己的问题。如果你想在子类的构造函数中发生相同的操作,你需要手动调用父类的构造函数。 - maxiscool
1
你说的“不够高效”是什么意思? - user2406944
我想在这个问题上添加一些内容,因为我也想做同样的事情。具体来说,我希望基础控制器可以在每次调用时接收Request对象,而不是在每个控制器中重新执行它。这样我就可以在基础控制器中设置一些东西(比如用户),而不必诉诸门面。在PHP中可能吗?通过反射在Laravel中实现这个功能是否可行?我还不知道足够多关于反射的知识来回答这个问题。谢谢。 - Andrew Brown
6个回答

11
没有完美的解决方案,重要的是要理解这不是 Laravel 本身的问题。
为了管理这个问题,您可以有以下三种做法之一:
  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();
    }
    
如果您想测试您的类,第三个解决方案将更加困难。我不确定您是否可以使用第二个解决方案来模拟这些类,但您可以使用第一个解决方案来模拟它们。

5

我知道这是一个非常老的问题,但我刚刚在我的当前项目中遇到了类似的问题,并对这个问题有所理解。

基本的核心问题是:

如果我正在扩展一个具有构造函数的父类。那个构造函数有注入依赖项,并且所有它的依赖关系已经在父类本身中记录。为什么我必须再次在我的子类中包含父类的依赖项

我也遇到了同样的问题。

我的父类需要三个不同的依赖项。它们通过构造函数注入:

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

这个类是一个抽象类,所以我永远不能单独实例化它。当我扩展这个类时,我仍然需要在子类的构造函数中包含所有这些依赖项及其use引用:

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

在每个类中包含三个父依赖项的use语句似乎是重复的代码,因为它们已经在父构造函数中定义。我的想法是删除父级use语句,因为它们总是需要在扩展父级的子类中定义。
但我意识到,在父类中包括依赖项的use和在父类构造函数中包括类名,仅在父类中用于类型提示。
如果从父级中删除use语句和从父级构造函数中删除类型提示的类名,则会得到:
<?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;
    }

没有从父类中使用 use 语句和类型提示,它不能再保证传递给它构造函数的类的类型,因为它无法知道。你可以使用任何东西从你的子类构造,而父类会接受它。

看起来好像代码重复了一遍,但实际上在你的父类中,你不是按照父类的依赖关系进行构建,而是验证子类是否发送了正确的类型。


3

有一种方式。当BaseController自动解析它的依赖项时。

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

因此,在子控制器中,您只传递应用程序实例:

class MyController extends BaseController {

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

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


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

当然,我们可以使用 Facade 来进行应用程序的处理。

0

我遇到了同样的问题,当我扩展我的基础控制器时。

我选择了与这里显示的其他解决方案不同的方法。而不是依赖注入,我在父构造函数中使用app()->make()。

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

这种简单的方法可能存在缺点 - 可能会使代码难以测试。


0
你必须将依赖项传递给父构造函数,以便它们在子类中可用。如果通过子类实例化父类,则无法在父构造函数上注入依赖项。

1
那是否意味着TS解决方案就是解决之道?因为实际上,我也觉得在子类中传递相同的参数直到达到父类是一种混乱的做法。这样做会导致类之间紧密耦合。 - basagabi

0

我也遇到了这个问题,并通过在子类中不调用构造函数并在函数参数中使用额外所需的依赖项来清除这个混乱。

它适用于控制器,因为您不需要手动调用这些函数,而且您可以在那里注入所有内容。因此,常见的依赖关系归父级所有,而较少需要的依赖关系将添加到方法本身中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接