Laravel服务容器和服务提供商的详解

2023-06-01 00:00:00 容器 详解 提供商

Laravel的服务容器是框架中最重要的部分之一,但是它却很少受到许多开发人员的关注。通过面试大量候选人,我意识到这种无知的背后有两个主要原因。

他们发现依赖注入的想法很难理解。更不用说IoC和IoC容器的想法了。

他们不知道是否要使用该容器。


因此,在本文中,我将带您逐步理解这个神奇的概念。我将从基本概念开始,最后,最后,您应该了解不同部分如何组合在一起。


展望未来,我假设您对面向对象的编程,类,对象,接口和名称空间有充分的了解。

表中的内容:

依赖注入和IoC,

IoC容器,

服务容器和服务提供商,

绑定或不绑定,

建议读物,

总结思想


依赖注入和IoC


过于简单的依赖关系注入定义是将类依赖关系作为参数传递给其方法之一(通常是构造函数)的过程。


看看下面没有依赖注入的代码:


<?php

namespace App;

use App\Models\Post;

use App\Services\TwitterService;

class Publication {

    public function __construct()

    {

        // dependency is instantiated inside the class

        $this->twitterService = new TwitterService();

    }

    public function publish(Post $post)

    {

        $post->publish();

        $this->socialize($post);

    }

    protected function socialize($post)

    {

        $this->twitterService->share($post);

    }

}



该类可以是虚构的博客平台的一部分,负责发布帖子并在社交媒体上分享。


socialize()方法使用TwitterService类的实例,该实例包含一个名为share()的公共方法。


<?php

namespace App\Services;

use App\Models\Post;

class TwitterService {

    public function share(Post $post)

    {

        dd('shared on Twitter!');

    }

}





如您所见,在构造函数中,已经创建了TwitterService类的新实例。无需在类内部执行实例化,您可以将实例作为构造函数从外部注入到构造函数中。


<?php

namespace App;

use App\Models\Post;

use App\Services\TwitterService;

class Publication {

    public function __construct(TwitterService $twitterService)

    {

        $this->twitterService = $twitterService;

    }

    public function publish(Post $post)

    {

        $post->publish();

        $this->socialize($post);

    }

    protected function socialize($post)

    {

        $this->twitterService->share($post);

    }

}



对于此简单的演示,您可以在routes / web.php文件中使用/ route回调。


<?php

// routes/web.php

use App\Publication;

use App\Models\Post;

use App\Services\TwitterService;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $post = new Post();

    // dependency injection

    $publication = new Publication(new TwitterService());

    dd($publication->publish($post));

    // shared on Twitter!

});

这是基本的依赖注入。将依赖项注入应用于类会导致控件反转。以前,依赖类(即发布)控制实例化依赖类(即TwitterService),而后来,该控件已移交给框架。




IoC容器


在上一节中,我向您介绍了依赖注入的概念,并向您展示了它如何使类将实例化控制移交给框架。


IoC容器可以使依赖项注入过程更加高效。这是一个简单的类,能够在需要时保存并提供数据。简化的IoC容器可编写如下:


<?php

namespace App;

class Container {

    // array for keeping the container bindings

    protected $bindings = [];

    // binds new data to the container

    public function bind($key, $value)

    {

        // bind the given value with the given key

        $this->bindings[$key] = $value;

    }

    // returns bound data from the container

    public function make($key)

    {

        if (isset($this->bindings[$key])) {

            // check if the bound data is a callback

            if (is_callable($this->bindings[$key])) {

                // if yes, call the callback and return the value

                return call_user_func($this->bindings[$key]);

            } else {

                // if not, return the value as it is

                return $this->bindings[$key];

            }

        }

    }

}


您可以使用bind()方法将任何数据绑定到此容器:


<?PHP

// routes/web.php

use App\Container;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $container = new Container();

    $container->bind('name', 'Farhan Hasin Chowdhury');

    dd($container->make('name'));

    // Farhan Hasin Chowdhury

});



您可以通过传递一个回调函数将类绑定到此容器,该回调函数将类的实例作为第二个参数返回。


<?php

use App\Container;

use App\Service\TwitterService;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $container = new Container;

    $container->bind(TwitterService::class, function(){

        return new App\Services\TwitterService;

    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269}

});



假设您的TwitterService类需要用于身份验证的API密钥。 在这种情况下,您可以执行以下操作:


<?php

use App\Container;

use App\Service\TwitterService;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(TwitterService::class, function() use ($container){

        return new App\Services\TwitterService($container->make('ApiKey'));

    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269 ▼

    //     #apiKey: "very-secret-api-key"

    // }

});



将数据绑定到容器后,可以在需要时提出要求。 这样,您只需使用一次new关键字。


我并不是说使用new是不好的。 但是,每次使用new时,都必须注意将正确的依赖项传递给类。 但是,对于IoC容器,该容器负责注入依赖项。


IoC容器可以使您的代码更加灵活。 考虑一种情况,您想将TwitterService类与其他类(如LinkedInService类)交换。


该系统的当前实现不是非常适合于此。 要替换TwitterService类,您必须创建一个新类,将其绑定到容器,并替换对先前类的所有引用。


不必一定是这样。 您可以通过使用界面使此过程变得更加容易。 首先创建一个新的SocialMediaServiceInterface。


<?php

namespace App\Interfaces;

use App\Models\Post;

interface SocialMediaServiceInterface {

    public function share(Post $post);

}

现在,使您的TwitterService类实现此接口。


<?php

namespace App\Services;

use App\Models\Post;

use App\Interfaces\SocialMediaServiceInterface;

class TwitterService implements SocialMediaServiceInterface {

    protected $apiKey;

    public function __construct($apiKey)

    {

        $this->apiKey = $apiKey;

    }

    public function share(Post $post)

    {

        dd('shared on Twitter!');

    }

}

不要将具体的类绑定到容器,而是绑定接口。 在回调中,像以前一样返回TwitterService类的实例。


<?php

use App\Container;

use Illuminate\Support\Facades\Route;

use App\Interfaces\SocialMediaServiceInterface;

Route::get('/', function () {

    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(SocialMediaServiceInterface::class, function() use ($container){

        return new App\Services\TwitterService($container->make('ApiKey'));

    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\TwitterService {#269 ▼

    //     #apiKey: "very-secret-api-key"

    // }

});



到目前为止,代码仍像以前一样工作。 当您想使用LinkedIn而不是Twitter时,乐趣就开始了。 有了接口之后,您可以通过两个简单的步骤来完成此操作。


创建一个新的LinkedInService类,该类实现SocialMediaServiceInterface。


<?php

namespace App\Services;

use App\Models\Post;

use App\Interfaces\SocialMediaServiceInterface;

class LinkedInService implements SocialMediaServiceInterface {

    public function share(Post $post)

    {

        dd('shared on LinkedIn!');

    }

}



更新对bind()方法的调用,以返回LinkedInService类的实例。


<?php

use App\Container;

use App\Interfaces\SocialMediaServiceInterface;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $container = new Container;

    $container->bind(SocialMediaServiceInterface::class, function() {

        return new App\Services\LinkedInService();

    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\LinkedInService {#269}

});

现在,您将获得LinkedInService类的实例。 这种方法的优点在于,其他地方的所有代码都保持不变。 您只需要更新bind()方法调用即可。 只要一个类实现了SocialMediaServiceInterface,它就可以作为有效的社交媒体服务绑定到容器。




服务容器和服务提供商


Laravel带有一个更强大的IoC容器,称为服务容器。 您可以使用服务容器按照上一部分的方式重写示例,如下所示:


<?php

use App\Container;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    app()->bind('App\Interfaces\SocialMediaService', function() {

        return new App\Services\LinkedInService();

    });

    ddd(app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}

});

在每个Laravel应用程序中,应用程序实例都是容器。 app()辅助函数返回容器的实例。


就像您的自定义容器一样,Laravel服务容器具有bind()和make()方法,用于绑定服务和检索服务。


还有另一种称为singleton()的方法。 当您将一个类绑定为单例时,该类只能有一个实例。


让我给你看一个例子。 更新您的代码以创建给定类的两个实例。


<?php

use App\Container;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    app()->bind('App\Interfaces\SocialMediaService', function() {

        return new App\Services\LinkedInService();

    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}

    // App\Services\LinkedInService {#269}

});



最后用数字(#262和#269)表示,这两个实例互不相同。 如果将类作为单例绑定,则会看到不同的内容。


<?php

use App\Container;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    app()->singleton('App\Interfaces\SocialMediaService', function() {

        return new App\Services\LinkedInService();

    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}

    // App\Services\LinkedInService {#262}

});

如您所见,现在两个实例的编号相同,表明它们是相同的实例。


现在您已经了解了bind(),singleton()和make()方法,接下来您将要学习的是将这些方法调用放在哪里。 您当然不能将它们放在控制器或模型中。




服务提供商是放置绑定的正确位置。 服务提供者是驻留在app / Providers目录中的类。 这些是框架的基础,负责引导大多数框架服务。


默认情况下,每个新的Laravel项目都带有五个服务提供程序类。 其中,默认情况下,AppServiceProvider类使用两种方法为空。 它们是register()和boot()。


register()方法用于向应用程序注册新服务。 在这里放置bind()和singleton()方法调用。


<?php

namespace App\Providers;

use App\Interfaces\SocialMediaService;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider

{

    /**

     * Register services.

     *

     * @return void

     */

    public function register()

    {

        $this->app->bind(SocialMediaService::class, function() {

            return new \App\Services\LinkedInService;

        });

    }

    /**

     * Bootstrap services.

     *

     * @return void

     */

    public function boot()

    {

        //

    }

}



在提供程序内部,您只需编写$ this-> app而不是调用app()helper函数即可访问容器。 但是您也可以这样做。


boot()方法用于引导注册服务所需的逻辑。 一个很好的例子是BroadcastingServiceProvider类。


<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;

use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider

{

    /**

     * Bootstrap any application services.

     *

     * @return void

     */

    public function boot()

    {

        Broadcast::routes();

        require base_path('routes/channels.php');

    }

}



如您所见,它会调用Broadcast :: routes()方法,并且需要route / channels.php文件,从而使广播路由在该过程中处于活动状态。


对于本示例中的一两个简单绑定,可以使用AppServiceProvider,但是对于需要执行更复杂逻辑的服务,可以使用php artisan make:provider <provider name>命令创建新的服务提供者。




全貌


在前面的部分中,我向您介绍了不同的概念,例如依赖项注入,控件反转,服务容器,服务提供者。到目前为止,您应该对容器是什么,如何将类绑定到容器以及在必要时检索它们有一个扎实的了解。在本节中,我将向您展示所有这些想法是如何协调工作的。


让我们回到您之前使用过的Publication类。我希望您还记得Publication类以前依赖于TwitterService类。但是,既然您已经为项目引入了接口,就让我们更新发布以使用它。


<?php

namespace App;

use App\Models\Post;

use App\Interfaces\SocialMediaServiceInterface;

class Publication {

    protected $socialMediaService;

    public function __construct(SocialMediaServiceInterface $socialMediaService)

    {

        $this->socialMediaService = $socialMediaService;

    }

    public function publish(Post $post)

    {

        $post->publish();

        $this->socialize($post);

    }

    protected function socialize($post)

    {

        $this->socialMediaService->share($post);

    }

}



现在,此出版物不再依赖于单个类,而是将接受实现SocialMediaServiceInterface的任何类。


您还已将SocialMediaServiceInterface绑定到LinkedInService类的实现,因此执行app()-> make(SocialMediaServiceInterface :: class); 应该返回LinkedInService类的实例。


但是,尚未绑定到容器的一个类是Publication类本身。 但是如果执行app()-> make(Publication :: class);怎么办? 没有绑定吗? 让我们尝试一下。


<?php

// routes/web.php

use App\Publication;

use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $publication = app()->make(Publication::class);

    ddd($publication);

    // App\Publication {#273 ▼

    //     #socialMediaService: App\Services\LinkedInService {#272}

    // }      

});





事实证明它可行。 Laravel以某种方式成功实例化了Publication类的实例,而没有将其绑定到容器。


乍一看,这似乎很神奇。 但是实际上,这是所有先前解释的概念协调工作的结果。


当Laravel遇到app()-> make(Publication :: class); 行,它会在容器中寻找相应的条目。 当找不到该类的绑定时,它将查看该类的构造函数。


<?php

namespace App;

use App\Models\Post;

use App\Interfaces\SocialMediaServiceInterface;

class Publication {

    protected $socialMediaService;

    public function __construct(SocialMediaServiceInterface $socialMediaService)

    {

        $this->socialMediaService = $socialMediaService;

    }

    //

}





Laravel意识到Publication类具有类型为SocialMediaServiceInterface的依赖项$ socialMediaService,并在容器中寻找该接口。


<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Interfaces\SocialMediaServiceInterface;

class AppServiceProvider extends ServiceProvider

{

    /**

     * Register any application services.

     *

     * @return void

     */

    public function register()

    {

        $this->app->bind(SocialMediaServiceInterface::class, function() {

            return new \App\Services\LinkedInService;

        });

    }

    //

}

找到所需条目后,Laravel实例化一个新的社交媒体服务,将其提供给Publication类,并成功创建一个新实例。


您应该清楚,Laravel可以自动实例化类型提示的依赖关系,只要它们没有实现任何接口即可。 请牢记这一点,如下更新您的routes / web.php:


<?php

// routes/web.php

use App\Publication;

use Illuminate\Support\Facades\Route;

Route::get('/', function (Publication $publication) {

    ddd($publication);

    // App\Publication {#276 ▼

    //     #socialMediaService: App\Services\LinkedInService {#275}

    // }

});

该项目按预期工作。您会看到,由于容器具有自动解析功能,您几乎不会手动从容器中取出实例。只要您正确地绑定接口并提示依赖项,Laravel就会为您带来繁重的工作。




绑定或不绑定


当您将服务绑定到容器时,本文中要回答的最后一个问题。鉴于容器能够实例化类而无需绑定,因此您甚至需要考虑绑定。

答案现在应该已经很清楚了,但是在这里-您可以在类实现容器时将其绑定到容器。

在上面的示例中,LinkedInService类实现了一个接口,因此您必须将接口绑定到该类的实现。

但是,Publication类不实现任何接口。它唯一的依赖关系是已绑定到容器的SocialMediaServiceInterface的实现。因此,不必绑定此类。




建议读物


服务容器(Laravel文档)


服务提供商(Laravel文档)

服务容器(Symfony文档)

服务容器基础知识(广播)

自动解决依赖关系(广播)

服务提供者是缺失的部分(广播)

控制容器的反转和依赖注入模式(Martin Fowler)

控制反转-好莱坞原则(Alejandro Gervasio)


总结思想


服务容器是一个很难理解的概念,无可否认。您不经常直接使用它的事实,使得它对许多开发人员而言甚至不那么重要。但是了解服务容器是掌握Laravel的最重要步骤之一。

我希望我能够使您更清楚地了解有关服务容器的各种概念。从长远来看,了解幕后发生的事情将对您有帮助。

从现在开始,您应该在类构造函数中的类型提示依赖项上更加自信。



转:https://dev.to/fhsinchy/laravel-service-container-and-service-providers-explained-5a1




相关文章