Laravel中的服务容器

我们已经了解了服务容器是个什么东西,也知道了依赖、依赖注入、控制反转以及最终的服务容器的概念和它们要解决的问题。今天,我们就来一起学习一下 Laravel 中的服务容器是怎么使用的,大家一起来看看它是不是和我们上回学习到的服务容器是一样的。

使用 Laravel 中的服务容器

在 Laravel 中使用服务容器非常简单,我们首先还是定义那几个测试的类,不过这次我们把它们分开到不同的文件中存储。

// app/ContainerTest/IntelligencePhone.php
namespace App\ContainerTest;


interface IntelligencePhone
{
public function openApp($name);
}

// app/ContainerTest/iPhone12.php
namespace App\ContainerTest;

class iPhone12 implements IntelligencePhone
{
public function openApp($name){
echo __CLASS__ . '打开' . $name, PHP_EOL;
}
}

// app/ContainerTest/Mi11.php
namespace App\ContainerTest;

class Mi11 implements IntelligencePhone
{
public function openApp($name){
echo __CLASS__ . '打开' . $name, PHP_EOL;
}
}

// app/ContainerTest/ZyBlog.php
namespace App\ContainerTest;

class ZyBlog
{
public function ShuaDuanShiPin(IntelligencePhone $phone){
$phone->openApp('douyin');
}
}

接下来,我们就可以去绑定和使用对象了。

Route::get('container/test1', function(){
app()->bind('iphone12', function(){
return new \App\ContainerTest\iPhone12();
});
app()->instance('mi11', new \App\ContainerTest\Mi11());

app()->singleton('zyblog', function(){
return new \App\ContainerTest\ZyBlog();
});

$zyblog = app()->make('zyblog');
$zyblog->ShuaDuanShiPin(app()->make('iphone12')); // App\ContainerTest\iPhone12打开douyin

});

熟悉吗?这不就是我们之前自己写的那个服务容器的使用方式嘛。不过在这里还有更多不同的用法。bind() 就是直接绑定一个容器对象,和我们之前自己定义的那个容器类没什么太大的差别,而 instance() 就是绑定一个实例化对象。接下来的 singleton() 从名字就可以看出,它绑定一个单例对象。

绑定完成之后,我们就可以使用 make() 方法来获得容器中的对象实例。这个就和我们之前自定义的服务容器中的 make() 方法是一样的概念了。

怎么样,通过之前的学习,我们对 Laravel 中服务容器的使用就非常好理解了吧。下一篇文章中我们再看源码,不过 Laravel 中的源码可比我们自己定义的那个要复杂多了。然而,万变不离其宗,思想毕竟都是一致的。Larvel 中的服务容器还有更多的功能,大家可以去官方文档查看,不过有一个东西非常重要,那就是我们接下来要讲的 服务提供者 。

服务提供者

在 Laravel 中,配合服务容器的还有一个神器不得不提,那就是 服务提供者 。从名称我们可以看出,它是来“提供”服务的。官方的解释是 服务提供者是所有 Laravel 应用程序的引导中心。你的应用程序,以及通过服务器引导的 Laravel 核心服务都是通过服务提供者引导。

用大白话来讲的话,其实就是为我们的进行服务注册的,也就是我们的 bind() 操作。再到代码层面来的说的话,就是我们在控制器、路由中,连 app()->bind() 这一步都可以不用了,直接去定义服务提供者,然后框架在启动的时候就会帮我们加载需要的对象。这就类似于我们电脑开机时的系统引导过程,直接将我们所需要的服务注册或者运行起来。

在我们的 app/Providers 文件夹中就已经默认包含了一些 服务提供者 ,大家可以先看看,然后我们自己创建两个服务提供者,仍然用于实现手机刷视频的对象创建。

php artisan make:provider PhoneServiceProvider   
php artisan make:provider ZyBlogServiceProvider

执行完这两条命令之后,我们就会看到在 app/Providers 下面多了两个文件。当然,你自己创建并且继承自 Illuminate\Support\ServiceProvider 也是完全没有问题的。通过命令行生成的文件中,会有两个方法需要我们来实现,分别是 register() 和 boot() ,我们先来看看 register() 方法。

class PhoneServiceProvider extends ServiceProvider
{
public function register()
{
//
$this->app->bind('iphone12', function(){
return new iPhone12();
});
$this->app->bind('mi11', function(){
return new Mi11();
});
}

public function boot()
{
}
}

class ZyBlogServiceProvider extends ServiceProvider
{
public function register()
{
//
$this->app->singleton('zyblog', function(){
return new ZyBlog();
});
}

public function boot()
{
}
}

register() 不用我说大家也能猜到,这货就是注册对象用的嘛。没毛病,因此,我们在这两个服务提供者的 register() 方法中都去进行了对象的注册绑定,使用的是继承的 ServiceProvider 中已经为我们准备好的 $app 对象来直接进行注册。同时,我们在 PhoneServiceProvider 直接一次性地把两个对象都注册好了。这个注册过程是看我们的需求情况的,想要注册多少个对象到服务容器中当然是我们自己说了算的。

接下来该干嘛了呢?让系统调用这两个服务提供者呀,这个就需要去修改 config 目录下的 app.php 文件了。在这个文件中,我们可以看到许多的配置项,我们最主要关心的是 providers 。它代表就是系统在启动时要运行的那些服务提供者。因此,我们在这里添加上我们刚刚自定义的那两个服务提供者。

'providers' => [
// ………………
// ………………
App\Providers\ZyBlogServiceProvider::class,
App\Providers\PhoneServiceProvider::class,
],

然后,在我们的测试路由中,就可以把之前绑定服务相关的代码删掉或者注释掉了。

//    app()->bind('iphone12', function(){
// return new \App\ContainerTest\iPhone12();
// });
// app()->instance('mi11', new \App\ContainerTest\Mi11());
//
// app()->singleton('zyblog', function(){
// return new \App\ContainerTest\ZyBlog();
// });

$zyblog = app()->make('zyblog');
$zyblog->ShuaDuanShiPin(app()->make('iphone12')); // App\ContainerTest\iPhone12打开douyin

是不是很方便,使用了服务提供者,就不需要再手动去进行服务对象的绑定注册了,直接使用就可以了。系统在运行启动的时候会帮我们直接将我们的对象进行绑定注册到系统变量中。接下来,我们要解决一个依赖问题,也就是外部传递的这个参数 $phone 对象让它也放到服务提供者中,这个要怎么做呢?我们来看看服务提供者的 boot() 方法。

boot() 方法是在所有服务提供者被注册之后才会调用,也就是说,我们可以在这个方法里面访问框架中所有已注册的其它服务。比如说,我们在 config/app.php 中,先注册的是 ZyBlogServiceProvider 这个服务提供者,如果我们在 register() 中去调用 iphone12 或者 mi11 的话,那它们肯定是还没有注册的。而 boot() 则是在所有服务提供者的 register() 执行完成之后才调用的,在它的方法体中,我们是可以获取相关的手机对象的。

// app/Providers/ZyBlogServiceProvider.php
public function boot()
{
//
if(!$this->app['zyblog']->getPhone()){
$this->app['zyblog']->setPhone($this->app->make(env('PHONE', 'iphone12')));
}
}

在 boot() 方法中,我们使用 env() 来获取 .env 配置文件中的 PHONE 配置信息。这样我们就给定了一个默认的手机对象信息。当然,我们也可以灵活地在外面使用 setPhone() 来在运行时调用想要使用的 phone 对象。很明显,我们也要改造一下 ZyBlog 类。

class ZyBlog
{
private $phone;

public function setPhone(IntelligencePhone $phone){
$this->phone = $phone;
}

public function getPhone(){
return $this->phone;
}

public function ShuaDuanShiPin(){
$this->phone->openApp('douyin');
}
}

接下来,在默认情况下,路由中我们只需要调用服务提供者并且调用指定的刷视频的方法就可以了。

$zyblog = app()->make('zyblog');
$zyblog->ShuaDuanShiPin(); // App\ContainerTest\Mi11打开douyin

在 .env 中,设置了 PHONE=mi11 ,所以我们在默认情况下直接输出的就是使用 mi11 手机来刷视频了。

由此可以看出,boot() 方法的作用通常是可以帮我们在服务提供者全部加载完成后,进行一些初始化或者解决依赖相关的问题。对于整个服务容器来说,服务提供者是非常重要的一个部分,因为它起着整个框架启动加载核心组件的重要作用。当然,也有很多组件,比如说核心的 app 、事件、日志、路由服务都是直接在源代码中进行了服务注册而没有实现服务提供者的,我们将在下次分析源码时再深入了解。

另外,服务提供者还可以通过 Application 中的 register() 方法来手动注册。

app()->register(\App\Providers\PhoneServiceProvider::class);
app()->register(\App\Providers\ZyBlogServiceProvider::class);

你可以尝试注释掉 config/app.php 中的配置,然后在路由中添加上述代码,也可以直接完成服务提供者的注册。其实,自动的服务提供者的注册加载最终也是调用的这个 register() 方法来完成的。

上下文绑定

最后,对于我们这种有依赖关系的例子来说,Laravel 中还提供了上下文绑定的方式来处理依赖。

// app/ContainerTest/ZyBlog.php
public function __construct(IntelligencePhone $phone)
{
$this->phone = $phone;
}

先给 ZyBlog 类型加上构造函数,让依赖通过构造函数传递过来。然后直接使用上下文绑定语法就可以啦。

app()->when(\App\ContainerTest\ZyBlog::class)
->needs(\App\ContainerTest\IntelligencePhone::class)
->give(\App\ContainerTest\Mi11::class);

$zyblog = app()->make(\App\ContainerTest\ZyBlog::class);
$zyblog->ShuaDuanShiPin(); // App\ContainerTest\Mi11打开douyin

这是什么鬼?!!!

when() 方法是指我们需要的某个对象,needs() 方法指需要一个什么样的注入接口,give() 就是给它一个真正的实例化对象。

然后我们通过最简单的方式直接 make() 一个 ZyBlog 对象,此时,需要的 phone 对象就被注入进去了,是不是感觉有点高大上。

总结

今天我们简单地入门了解了一下在 Laravel 框架中如何使用服务容器以及服务提供者这两个非常核心的组件。作为普通的服务容器来说,它们的使用非常简单方便,但其实在一个商业化的开源框架中,它们的功能绝不仅限如此,比如说服务容器的接口绑定实现、标记、扩展绑定这些,还有服务提供者的延迟提供者我们都没有讲到,但这些内容在官方文档上已经写得很详细了,大家可以自己去了解尝试一下。剩下的,就是下篇文章我们将看一下 Laravel 是如何实现服务容器以及服务提供者的。

参考文档:

深入剖析 Laravel 服务提供者实现原理:http://blog.phpzendo.com/?p=358

​https://learnku.com/docs/laravel/8.x/container/9361​

​https://learnku.com/docs/laravel/8.x/providers/9362​