引言
学习laravel而不了解容器的知识,那谈不上会laravel。本文从一个laravel的初学者角度,一步一步了解容器是在什么样的场景下产生的,以及laravel中是如何使用容器的。在看本文之前,如果有反射和匿名函数的基础,会更容易理解
一、控制反转(Ioc)、依赖注入(DI)
学习laravel的容器,首先需要了解,依赖注入(Dependency Injection)和控制反转(Inversion of Control)这两个概念,以及他们的关系。
依赖注入是控制反转的一种实现方式
先了解一下什么是控制反转。当调用者需要被调用者的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例,但在这里,创建被调用者的工作不再由调用者来完成,而是将被调用者的创建移到调用者的外部,从而反转被调用者的创建,消除了调用者对被调用者创建的控制,因此称为控制反转
上边的文字描述有点抽象,下边看一个场景,帮助我们了解控制反转。假设用户登录的时候,系统会提供记录登录日志的功能,可以选择使用【文件】或者【数据库】的方式来记录日志。实现方案如下:
<?php
interface Log
{
public function write();
}
class FileLog implements Log
{
public function write(){
echo 'file log write...'.PHP_EOL;
}
}
class DatabaseLog implements Log
{
public function write(){
echo 'database log write...'.PHP_EOL;
}
}
class User
{
protected $fileLog;
public function __construct()
{
$this->fileLog = new FileLog();
}
public function login()
{
echo 'login success...'.PHP_EOL;
$this->fileLog->write();
}
}
$user = new User();
$user->login();
上边的写法可以实现通过文件的方式记录日志,【假设现在需要通过数据库的方式记录日志的话】我们就需要修改User类,这样的话,代码就没达到解耦合的目的
要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由 IoC 容器来完成,然后在调用者中注入被调用者(通过构造器/方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。 *依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序*。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程
现在我们按照上边说的【依赖注入】的方式,对User类进行修(在调用者中注入被调用者(通过构造器/方法注入实现)),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。):
class User
{
protected $log;
public function __construct(Log $log)
{
$this->log = $log;
}
public function login()
{
echo 'login success...';
$this->log->write();
}
}
$user = new User(new DatabaseLog());
$user->login();
刚开始接触laravel的时候,就特别好奇很多对象实例通过方法的参数定义就能传进来,而调用的时候也不需要我们手动的去传入,比如说Request,所以这个时候就需要知道larave是怎么实现的。要想了解laravel是怎么实现的,需要先了解php中的反射,因为laravel容器的实现借助了反射,所以先大致介绍一下反射
二、反射
反射的概念其实可以理解成根据类名返回该类的任何信息,比如该类有什么方法,参数,变量等等。反射官方文档:https://www.php.net/manual/zh/book.reflection.php
就拿刚才的User类来举例:
$class = new reflectionClass(User::class);
$constructor = $class->getConstructor();
$dependencies = $constructor->getParameters();
$user = $reflector->newInstance();
$user = $reflector->newInstanceArgs($dependencies = []);
现在创建一个make方法,将User类的名字作为参数传给make方法,在make中通过反射机制拿到User的构造函数,进而得到构造函数的参数对象,然后通过递归的方式创建参数的依赖,最后就是通过newInstanceArgs方法生成User实例:
class User
{
protected $log;
public function __construct(FileLog $log)
{
$this->log = $log;
}
public function login()
{
echo 'login success...';
$this->log->write();
}
}
function make($concrete){
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
if(is_null($constructor)) {
return $reflector->newInstance();
}else {
$dependencies = $constructor->getParameters();
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
}
}
function getDependencies($paramters) {
$dependencies = [];
foreach ($paramters as $paramter) {
$dependencies[] = make($paramter->getClass()->name);
}
return $dependencies;
}
$user = make('User');
$user->login();
如果不熟悉反射,上边这段代码可能有点难理解,但是如果啃明白了,会觉得特别有意思,成就感满满!上边介绍了依赖注入、控制反转、反射,下边进入本文重点,Ioc容器
三、IoC容器和服务提供者
我们上边通过反射的方式,其实还没有达到解耦的目的,假如现在要换别的方式记录日志,还是需要修改User。现在我们就借助容器来实现真正的解耦
先借助一个容器,提前将log、user都绑定到Ioc容器中。然后User的创建就交给容器去做
实现思路:
1、IoC容器维护binding数组记录bind方法传入的键值对如:log=>FileLog, user=>User。也就是说我们提前把我们需要用到的类,都绑定到这个数组中,并给类一个别名
2、在ioc->make('user')的时候,通过反射拿到User的构造函数,拿到构造函数的参数,发现参数是User的构造函数参数log,然后根据log得到FileLog。
3、这时候我们只需要通过反射机制创建 $filelog = new FileLog();
4、通过newInstanceArgs然后再去创建new User($filelog);
大致长下边这样:
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();
这个容器就指Ioc容器,这个User可以理解成服务提供者。上边说到了,如果User的构造函数参数是接口该如何处理,其实就是通过Ioc容器提前绑定好
核心实现代码:
interface log
{
public function write();
}
class FileLog implements Log
{
public function write(){
echo 'file log write...';
}
}
class DatabaseLog implements Log
{
public function write(){
echo 'database log write...';
}
}
class User
{
protected $log;
public function __construct(Log $log)
{
$this->log = $log;
}
public function login()
{
echo 'login success...';
$this->log->write();
}
}
class Ioc
{
public $binding = [];
public function bind($abstract, $concrete)
{
$this->binding[$abstract]['concrete'] = function ($ioc) use ($concrete) {
return $ioc->build($concrete);
};
}
public function make($abstract)
{
$concrete = $this->binding[$abstract]['concrete'];
return $concrete($this);
}
public function build($concrete) {
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
if(is_null($constructor)) {
return $reflector->newInstance();
}else {
$dependencies = $constructor->getParameters();
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
}
}
protected function getDependencies($paramters) {
$dependencies = [];
foreach ($paramters as $paramter) {
$dependencies[] = $this->make($paramter->getClass()->name);
}
return $dependencies;
}
}
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();
现在不需要关心是用什么方式记录日志了,哪怕后期需要修改记录日志的方式,只需要在ioc容器修改绑定其他记录方式日志就行了
那么laravel中的服务容器和服务提供者长啥样呢?
可以在config目录找到app.php中providers,这个数组定义的都是已经写好的服务提供者
$providers = [
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
...
]
...
class CacheServiceProvider{
public function register()
{
$this->app->singleton('cache', function ($app) {
return new CacheManager($app);
});
$this->app->singleton('cache.store', function ($app) {
return $app['cache']->driver();
});
$this->app->singleton('memcached.connector', function () {
return new MemcachedConnector;
});
}
}
具体服务提供者register方法是什么时候执行的,后边会大致说一下Laravel的生命周期
三、Facade外观模式的原理
我们经常会在laravel中通过这样的方式来调用方法:
User::query()->where()
这种写法要比我们刚才需要先通过$ioc->make('user')拿到User的实例,然后再使用$user->login()
那上边那种简单的方式是如何实现的呢?
Facade的工作原理:
1、定义一个服务提供者的外观类,在该类中定义一个容器类的变量,跟ioc容器绑定的key一样,
2、通过静态魔术方法__callStatic可以得到当前想要调用的login
3、使用static::$ioc->make('user');
现在通过这种外观类的方式去修改一下我们上边的那个记录日志的类(即给User类写一个外观类):
class UserFacade
{
protected static $ioc;
public static function setFacadeIoc($ioc)
{
static::$ioc = $ioc;
}
protected static function getFacadeAccessor()
{
return 'user';
}
public static function __callStatic($method, $args)
{
$instance = static::$ioc->make(static::getFacadeAccessor());
return $instance->$method(...$args);
}
}
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
UserFacade::setFacadeIoc($ioc);
UserFacade::login();
可能大家感觉加了这个User的外观类更加麻烦了,需要注入容器,还需要使用魔术方法,其实laravel在运行的时候都将这些工作做好了。我们直接使用UserFacade::login()就可以了。最主要的就是Facade提供了简单易记的语法,从而无需配置长长的类名。像laravel中的Redis、Log等都用的是这种外观模式
四、Laravel的生命周期
要研究laravel的生命周期,肯定是要看入口文件的
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
打开__DIR__.'/../bootstrap/app.php';你会发现这段代码,绑定了Illuminate\Contracts\Http\Kernel::class,这个你可以理解成之前我们所说的$ioc->bind();方法。
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
上边其实还是比较抽象的,在网上看见一张把laravel的生命周期画的非常清楚的图,分享给大家:
我觉得了解了在laravel中容器是个什么之后,对后边更深入的学习laravel是非常有帮助的,希望大家看完之后能真正的有所收获!
评论