在laravel外墙上使用依赖注入

时间:2016-01-26 10:10:39

标签: php laravel dependency-injection laravel-facade

我已经阅读了许多来源,暗示laravel facade最终是为了方便而存在,而这些类应该是injected以允许松散耦合。甚至Taylor Otwell has a post解释了如何做到这一点。我似乎不是唯一一个wonder this

use Redirect;

class Example class
{
    public function example()
    {
         return Redirect::route("route.name");
    }
}

会变成

use Illuminate\Routing\Redirector as Redirect;

class Example class
{
    protected $redirect;

    public function __constructor(Redirect $redirect)
    {
        $this->redirect = $redirect
    }

    public function example()
    {
         return $this->redirect->route("route.name");
    }
}

这很好,除了我开始发现一些构造函数和方法开始采用四个+参数。

由于Laravel IoC 似乎只注入类构造函数和某些方法(控制器),即使我有相当精益的函数和类,我发现类的构造函数正在被打包出来然后将所需的类注入所需的方法中。

现在我发现如果我继续这种方法,我将需要自己的IoC容器,如果我使用像laravel这样的框架,感觉就像重新发明轮子一样?

例如,我使用服务来控制业务/视图逻辑而不是处理它们的控制器 - 它们只是路由视图。因此,控制器将首先获取其对应的service,然后是其网址中的parameter。一个服务功能还需要检查表单中的值,因此我需要RequestValidator。就像那样,我有四个参数。

// MyServiceInterface is binded using the laravel container
use Interfaces\MyServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Validation\Factory as Validator;

...

public function exampleController(MyServiceInterface $my_service, Request $request, Validator $validator, $user_id) 
{ 
    // Call some method in the service to do complex validation
    $validation = $my_service->doValidation($request, $validator);

    // Also return the view information
    $viewinfo = $my_service->getViewInfo($user_id);

    if ($validation === 'ok') {
        return view("some_view", ['view_info'=>$viewinfo]);
    } else {
        return view("another_view", ['view_info'=>$viewinfo]);
    }
}

这是一个例子。实际上,我的许多构造函数已经注入了多个类(模型,服务,参数,外观)。我已经开始将构造函数注入(适用时)“卸载”到方法注入,并让调用这些方法的类使用它们的构造函数来代替注入依赖项。

我被告知,方法或类构造函数的四个以上参数作为经验法则是不好的做法/代码味道。但是,如果你选择注入laravel外墙的路径,我看不出你怎么能真正避免这种情况。

我有这个想法错了吗?我的课程/职能不够精益吗?我错过了laravels容器的要点还是我真的需要考虑创建自己的IoC容器? Some其他answers似乎暗示laravel容器能够消除我的问题?

尽管如此,似乎没有就此问题达成明确的共识......

6 个答案:

答案 0 :(得分:22)

这是构造函数注入的好处之一 - 当你的类做得太多时,它变得很明显,因为构造函数参数变得太大了。

要做的第一件事就是拆分那些责任太多的控制器。

假设你有一个页面控制器:

Class PageController
{

    public function __construct(
        Request $request,
        ClientRepositoryInterface $clientrepo,
        StaffRepositortInterface $staffRepo
        )
    {

     $this->clientRepository = $clientRepo;
     //etc etc

    }

    public function aboutAction()
    {
        $teamMembers = $this->staffRepository->getAll();
        //render view
    }

    public function allClientsAction()
    {
        $clients = $this->clientRepository->getAll();
        //render view
    }

    public function addClientAction(Request $request, Validator $validator)
    {
        $this->clientRepository->createFromArray($request->all() $validator);
        //do stuff
    }
}

这是分成两个控制器ClientControllerAboutController的主要候选者。

一旦你完成了这个,如果你还有太多的*依赖关系,那么它是时候寻找我称之为间接依赖的东西了(因为我不能想到它们的正确名称!) - 不直接使用的依赖关系依赖类,但转而传递给另一个依赖。

这方面的一个例子是addClientAction - 它需要一个请求和一个验证器,只是将它们传递给clientRepostory

我们可以通过创建一个专门用于从请求创建客户端的新类来重新考虑因素,从而减少我们的依赖关系,并简化控制器和存储库:

//think of a better name!
Class ClientCreator 
{
    public function __construct(Request $request, validator $validator){}

    public function getClient(){}
    public function isValid(){}
    public function getErrors(){}
}

我们的方法现在变为:

public function addClientAction(ClientCreator $creator)
{ 
     if($creator->isValid()){
         $this->clientRepository->add($creator->getClient());
     }else{
         //handle errors
     }
}

关于什么数量的依赖关系太多,没有严格的规则。 好消息是如果你使用松散耦合构建你的应用程序,重新分解相对简单。

我更愿意看到一个构造函数有6个或7个依赖项,而不是无参数构造函数和一堆隐藏在整个方法中的静态调用

答案 1 :(得分:2)

外观的一个问题是,在进行自动化单元测试时,必须编写额外的代码来支持它们。

至于解决方案:

<强> 1。手动解决依赖关系

解决依赖关系的一种方法,如果你不希望通过它来解决。构造函数或方法注入,是直接调用app():

/* @var $email_services App\Contracts\EmailServicesContract
$email_services = app('App\Contracts\EmailServicesContract');

<强> 2。重构

有时当我发现自己将过多的服务或依赖关系传递给某个班级时,我可能违反了单一责任原则。在这些情况下,可能需要重新设计,将服务或依赖关系分解为更小的类。我会使用另一个服务来包装一组相关的类来作为外观提供服务。从本质上讲,它将是服务/逻辑类的层次结构。

示例:我有一项服务可以生成推荐产品并通过电子邮件发送给用户。我调用服务WeeklyRecommendationServices,它接受​​2个其他服务作为依赖 - 一个Recommendation服务,这是一个用于生成建议的黑盒子(它有自己的依赖关系 - 也许是一个repo for产品,帮助者或两个)和EmailService可能有Mailchimp作为依赖项)。某些较低级别的依赖项(如重定向,验证程序等)将位于这些子服务中,而不是作为入口点的服务。

第3。使用Laravel全局函数

一些Facade在Laravel 5中可用作函数调用。例如,您可以使用redirect()->back()代替Redirect::back(),以及view('some_blade)代替View::make('some_blade') 。我相信dispatch和其他一些常用的外墙也是如此。

(已编辑添加)4。使用特征 当我今天在排队工作时,我也观察到注入依赖关系的另一种方法是使用特征。例如,Laravel中的 DispathcesJobs 特征具有以下几行:

   protected function dispatch($job)
    {
        return app('Illuminate\Contracts\Bus\Dispatcher')->dispatch($job);
    }

任何使用该特征的类都可以访问受保护的方法,并可以访问依赖项。它比在构造函数或方法签名中具有许多依赖性更整洁,比全局变量更清楚(关于涉及的依赖性)并且比手动DI容器调用更容易定制。缺点是每次调用函数时都必须从DI容器中检索依赖项,

答案 2 :(得分:1)

构成Laravel中路由机制一部分的类方法(中间件,控制器等)also have their type-hints used to inject dependencies - 它们并不都需要在构造函数中注入。即使我不熟悉任何四个参数限制经验法则,这可能有助于保持构造函数的纤薄; PSR-2 allows for the method definition to be stretched over multiple lines大概是因为要求超过四个参数的情况并不少见。

在您的示例中,您可以在构造函数中注入RequestValidator服务作为折衷方案,因为它们经常被多个方法使用。

至于建立共识 - 对于应用程序而言,Laravel必须更加自以为是,以便采用一刀切的方法。一个更简单的电话是,我认为外墙将在未来的版本中采用渡渡鸟的方式。

答案 3 :(得分:1)

你的想法和关注是正确的,我也有。 Facades有一些好处(我通常不使用它们),但如果你确实使用我会建议只在控制器中使用它们,因为控制器至少只是我的入口和出口点。

对于您给出的示例,我将展示我通常如何处理它:

// MyServiceInterface is binded using the laravel container
use Interfaces\MyServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Validation\Factory as Validator;

...
class ExampleController {

    protected $request;

    public function __constructor(Request $request) {
        // Do this if all/most your methods need the Request
        $this->request = $request;
    }

    public function exampleController(MyServiceInterface $my_service, Validator $validator, $user_id) 
    { 
        // I do my validation inside the service I use,
        // the controller for me is just a funnel for sending the data
        // and returning response

        //now I call the service, that handle the "business"
        //he makes validation and fails if data is not valid
        //or continues to return the result

        try {
            $viewinfo = $my_service->getViewInfo($user_id);
            return view("some_view", ['view_info'=>$viewinfo]);
        } catch (ValidationException $ex) {
            return view("another_view", ['view_info'=>$viewinfo]);
        }
    }
}



class MyService implements MyServiceInterface {

    protected $validator;

    public function __constructor(Validator $validator) {
        $this->validator = $validator;
    }

    public function getViewInfo($user_id, $data) 
    { 

        $this->validator->validate($data, $rules);
        if  ($this->validator->fails()) {
            //this is not the exact syntax, but the idea is to throw an exception
            //with the errors inside
            throw new ValidationException($this->validator);
        }

        echo "doing stuff here with $data";
        return "magic";
    }
}

请记住将代码分解为每个人处理自己责任的小块。 当你正确地破坏你的代码时,在大多数情况下你将没有那么多的构造函数参数,代码将很容易测试和模拟。

最后一个注释,如果您正在构建一个小型应用程序,甚至是一个巨大的应用程序中的页面,例如&#34;联系页面&#34;和&#34;联系页面提交&#34;,您可以使用外墙完成控制器中的所有操作,它只取决于项目的复杂性。

答案 4 :(得分:1)

我喜欢laravel,因为它的美丽建筑。从我的方法,我不会把所有的外观注入控制器方法只为什么?仅在控制器错误的实践中注入重定向外观,因为它可能需要在其他实践中。并且主要是大多数使用的东西应该被宣布为所有人,而对于那些使用一些或只有它的最佳实践通过方法注入它们的人,因为当你在顶部声明它会妨碍你的记忆优化以及你的速度码。希望这会有所帮助

答案 5 :(得分:0)

与我的同事交谈之后,不是一个答案,而是一些值得思考的问题;

  1. 如果laravel的内部结构在版本之间发生了变化(显然过去发生过),注入已解决的Facade类路径会破坏升级中的所有内容 - 同时主要使用默认的外观和辅助方法(如果不完全)避免这个问题。

  2. 尽管解耦代码通常是一件好事,但注入这些已解析的Facade类路径的开销会使类混乱 - 对于接管项目的开发人员来说,花费更多时间来尝试遵循可以更好地使用的代码。修复错误或测试。新开发人员必须记住哪些注入类是开发人员,哪些是laravels。不熟悉laravel的开发人员必须花时间查找API。最终,引入错误或丢失关键功能的可能性会增加。

  3. 由于外墙已经可以测试,开发速度变慢,可测试性没有得到真正改善。快速发展是首先使用laravel的重点。时间永远是一种约束。

  4. 其他大多数项目都使用了laravel外墙。大多数使用laravel经验的人使用外墙。创建一个不遵循先前项目现有趋势的项目通常会减慢速度。未来缺乏经验(或懒惰!)的开发人员可能会忽略门面注入,项目最终可能会采用混合格式。 (甚至代码审阅者都是人类)