简述
当你接触一段时间Laravel的Service Container, Service Provider,Contracts和Facade后,也许已经知道它们是什么了,但是对于如何使用,在什么时候使用,以及它们之间的关系是什么,还不是非常清楚。 而关键是如果你反复看文档,你会被它坑死,因为文档有些部分不但没有解释清楚,反而有误导的内容; 现在我们就来一次性把它们搞定;
基本概念
在继续本教程之前,你需要先对以上概念有基本了解,知道它们是什么;
Service Container和 Service Provider
Service Container,也就是IOC容器的使用并不依赖Service Provider,例如:
$app->make('App\Models\Post');
这句话和 new App\Models\Post; 的结果完全一样; 另外你在控制器里使用构造函数,type-hint进行依赖注入,也完全和Service Provider没有半毛钱关系。
总之,你可以完全不使用Service Provider;
Service Provider 和Contracts
如果说IOC容器的使用并不依赖Service Provider,那么为什么我们用composer下载扩展包的时候总是要在config/app.php里绑定一下Service Provider呢,有时候还需要绑定一下Facade;
理解的思路是这样的,Laravel核心类(Services)都是用接口(contracts)+实现来构成的, 如果不理解这个概念,仔细看文档接口那一章。而你在使用的时候,如果要拿到某个接口实现的实例的话,需要用到Service Container,而要用Service Container去解析一个接口,而不是直接解析一个类,这时就要用到Service Provider了,可以说,Service Provider的主要功能,就是来绑定接口的。
下我准备要讲坑爹的事情了,在讲接口绑定前,先了解一些基本的事实:
一些事实
$app->make('App\Models\Post');
你可以这样写,
$app->make('post');
也可以这样写,这里的post是一个别名,这个别名是造成混淆的主要地方; 这个时候你肯定在想,这样写有啥用,我去哪里关联这个别名到App\Models\Post呢?
Service Provider 的 bind方法
对,就是在Service Provider里用bind方法来绑定别名:
$this->app->bind('post', function ($app) { return new App\Models\Post;});
这样绑定后你就可以$app->make('post');这样写了;然而搞个别名到目前为止也没什么卵用。没关系,稍后会讲到,它和Facade有关系;我们先来解释文档坑爹的地方:
文档是这样写这个bind方法的:
$this->app->bind('HelpSpot\API',
function ($app) {
return new HelpSpot\API($app['HttpClient']);}
);
哇擦,您的这第一个参数到底填的啥啊,事实上,第一个参数可以填类的全称,但是如果不是填简称,我这样绑定有任何意义么? 后面再返回一个一样的类实例? 咦?$app['HttpClient']这个是什么?? 其实它是告诉你可以在解析类的时候可以再接着注入一个其他类的实例;文档大哥,拜托你解释一下好不好,能不能举个靠谱点的例子...
如果你到其他的扩展包中去看别人的bind的写法,你会发现千奇百怪的绑定写法,先不管他们,现在我们来看Service Provider对接口的使用方法,最最基本的原理是这样的:
//给一个接口起个别名$this->app->bind('event_pusher', function ($app) { return new App\Contracts\EventPusher;});//指定这个接口应该解析的实例$this->app->bind('App\Contracts\EventPusher', 'App\Services\RedisEventPusher');
通过这两步,我们让这个接口有了别名,也有了解析时对应的实现;
这样,我们就可以:
$app->make('event_pusher');
得到App\Services\RedisEventPusher;
Service Provider 和 Facades
我们来看Facade的写法,比如说Illuminate\Support\Facades\Cache:
class Cache extends Facade{
protected static function getFacadeAccessor() { return 'cache'; }}
这个cache就是上面提到过的别名;
下面我们来看Facade的对应关系图:
Facade Name Facade Class Resolved Class Service Provider Binding Alias
Cache Illuminate\Support\Facades\Cache Illuminate\Cache\Repository cache
所以你调用Cache::get('user_id')的时候,实际上是调用了Illuminate\Support\Facades\Cache 这个类,get并不是这个类的静态方法,事实上,get这个方法在Facade这个类里根本不存在,这正是它设计的本意,当get这个方法不存在的时候,它就会调用Facade基类里的__callStatic魔术方法(需要提前了解这个魔术方法),这个方法中就会把Service Provider中绑定的类(或接口)解析并返回出来,本例中也就是Illuminate\Cache\Repository 这个类,所以get其实是Illuminate\Cache\Repository这个类的方法;
然后我们在再看文档,有的Facade怎么没有别名呢?比如:
Facade Name Facade Class Resolved Class Service Provider Binding Alias
Response Illuminate\Support\Facades\Response
Illuminate\Contracts\Routing\ResponseFactory
是的,你可以直接写类的全称,而不是别名,如果你看这个Illuminate\Support\Facades\Response源码,它是这样写的:
class Response extends Facade{
protected static function getFacadeAccessor() {
return 'Illuminate\Contracts\Routing\ResponseFactory';
}}
可以直接返回该类;
Facade的命名空间到底是什么
我们发现,在使用Cache::get('user_id')的时候,你可以使用use Cache; 也可以使用
use Illuminate\Support\Facades\Cache;
这是为什么呢?
别忘了,你在config/app.php里面Class Aliases 那里绑定过 Facade 别名,也就是:
'Cache' => Illuminate\Support\Facades\Cache::class,
这样绑定过,你就可以直接use Cache来使用Facade了;
补充:
container容器
先来说一下这货是干什么的。现在假设一下你想要做一个开源项目,写一个类实现某种功能。OK,那么再假设,你这个类要做的工作比较复杂,需要
操作数据库
缓存
操作静态文件
操作session
等等……想想都复杂,不过没有关系,以上几个部分显而易见地看出来,它们之间的似乎没什么关联。那么你可以考虑把以上几部分写成独立的类,也就是说针对数据库操作我们写一个DB类封装一些常用操作,针对缓存我们写一个Cache类……
那么好了,当要使用这个类时,可能就会看到这样的代码
<?php
class SomeClass {
public function dbTask() {
$db = new DBClass(['localhost',3306,root,pwd]);
//数据库操作
}
public function cacheTask() {
$cache = new Cache(['localhost',11211]);
//缓存操作
}
//...
}
看起来其实还好,但是想想,如果SomeClass依赖很多外部的类,我们每次使用SomeClass都必须先use进那个类,再通过参数实例化那个对象,这样非常麻烦。那么,这时候就产生了容器的概念。
什么叫容器呢?简单讲,就是把我们的应用可能用到的service(像上述那些类,因为专职某个功能,称它们为service),全部绑定到一个全局的对象里面,这个对象就叫容器。可能就像下面这样
class Container {
protected $service_arr = [];
public function bind($name, $instance) {
$this->servie_arr[$name] = $instance;
}
public function get($name) {
return $this->service_arr[$name];
}
}
那么我们以后要用到某些service的时候,就可以直接通过Container->get('some')的方式来获取实例了比如
<?php
class SomeClass {
priavte $container;
public function construct($con) {
$this->container = $con;
}
public function dbTask() {
$db = $this->container->get('db');
//数据库操作
}
public function cacheTask() {
$cache = $this->container->get('cache');
//缓存操作
}
//...
}
OK,现在你大概明白container到底是干嘛的了吧。再简单一点讲,就是把我们可能需要在代码内部手动实例化的对象,全部绑定到container这个对象上,以后要用就来这儿取。至于它的好处我就不赘述了,可以看看设计模式关于IoC和DI的章节(控制反转和依赖注入)
ServiceProvider
理解了上面的container,你就会有疑问
如果要把所有可能的service绑定,container类是否会变得非常庞大?
是的,如果你在应用的开始调用N次bind方法,依次绑定需要的所有service,这个文件将难以维护,比如
class Container {
protected $service_arr = [];
public function bind($name, $instance) {
$this->servie_arr[$name] = $instance;
}
public function get($name) {
return $this->service_arr[$name];
}
}
$con = new Container();
$db = new DBClass(['locahost',3306,root,pwd]);
$con->bind('db', $db);
$cache = new Cache(['127.0.0.1',11211]);
$con->bind('cache',$cache);
//...
所有的类的实例化都写到一个文件里了,讲会是耦合性升高。 ServiceProvider就是解决这个问题的
每一个需要绑定到container的service,你需要创建一个对应ServiceProvider,这个ServiceProvider中有一个register方法,在这里面进行绑定,而不是像上述那样。比如:
class SomeServiceProvider extends ServiceProvider {
public function register() {
$this->app->bind('some', new Some());
}
}
Laravel的container为了知道需要绑定哪些service,它会去读一个数组,这个数组是config/app.php中的Providers,然后你只需要把你的SomeServiceProvider写进这个数组,就可以绑定上了。
Facade
刚开始看Facade其实还是不太好理解,到底什么是Facade呢,它存在的意义又是什么呢?
我的理解
简化对service的使用,可以理解为语法糖
可以方便的替换
可能你有疑惑,到底替换什么呢?看看下面的代码吧
use Illuminate\Support\Facades\Redis;
class IndexController extends Controller {
public function index() {
$redis = Redis::connect();
//do something
}
}
然而当我们打开Illuminate\Support\Facades\Redis看看
file
并没有connect的静态方法啊?但是可以发现唯一的一个方法返回了一个redis字符串,有什么玄机呢
那么我们再看看RedisServiceProvider
file 我们的RedisServiceProvider在container里绑定了一个redis……所以大概你也猜到了,没错!
Redis::connect() 等价于 $this->app->get('redis')->connect()
get的到底是什么,取决于Facade中getFacadeAccessor方法返回的字符串!
这样有什么好处呢?文档也说了,除了简便使用以外最大的用处是测试,想想看,你把Redis的getFacadeAccessor方法返回值变成'memcached',那么你所有使用Redis::some()是不是就全部切换成