Laravel源码分析之服务容器和服务提供者 源码解读 和 实现(完整代码)

引文

  • 服务容器是用于管理类的依赖和执行依赖注入的工具。
  • 服务提供者是Laravel应用程序的引导中心,核心服务通过服务提供者进行注册,如服务容器容器绑定,中间件等服务提供器。Laravel项目配置文件config/app.php中有个providers数组,数组中的内容即是应用程序需要加载的服务提供器。
    在这里插入图片描述
  • 接上篇文章Laravel源码分析之控制反转和依赖注入, 在这篇文章中, 已经实现了一个简单的IOC容器,但是还是不能很好的解耦,如图上文代码:
/**
 * 日志接口
 */
interface Log
{
    public function log($msg);
}

/**
 * 文件日志的实现
 */
class FileLog implements Log
{
    /**
     * 将log进行一个简单的输出
     * @param $msg
     * @return Log|void
     */
    public function log($msg)
    {
        echo '文件日志记录: ' . $msg . PHP_EOL;
    }
}


/**
 * 数据库日志的实现
 */
class DbLog implements Log
{
    public function log($msg)
    {
        echo '数据库日志记录:' . $msg . PHP_EOL;
    }
}


class User
{
    private $log;
    /**
     * @param FileLog $log
     */
    public function __construct(FileLog $log)
    {
        $this->log = $log;
    }

    /**
     * 简单的登录操作
     * @param string $username
     */
    public function login($username='ClassmateLin')
    {
        echo '用户:' . $username . '登录成功!' . PHP_EOL;
        $this->log->log('日志: 用户:' . $username . '登录成功!');
    }
}


class Application
{

    function make(string $class_name)
    {

        $reflector = new reflectionClass($class_name); // 拿到反射实例
        $constructor = $reflector->getConstructor(); // 拿到构造函数

        if (is_null($constructor)) { // 如果写构造函数,得到的constructor是null。
            return $reflector->newInstance(); // 进行无参数实例化
        }

        // 拿到构造函数依赖的参数
        $dependencies = $constructor->getParameters();

        // 这时候我们依赖的参数可能也有参数,通过递归的去获取当前类的参数。
        $instance = $this->getDependencies($dependencies);

        // 进行带参数的实例化
        return $reflector->newInstanceArgs($instance);

    }

    /**获取依赖
     * @param $params
     * @return array
     */
    private function getDependencies($params)
    {
        $dependencies = [];
        // array_walk相等于foreach, for 的作用,据说速度是最快的,我也没去验证,只是喜欢闭包。
        array_walk($params, function ($param) use (&$dependencies) {
            $class_name = $param->getClass()->name;  // 获取类名
            $dependencies[] = $this->make($class_name);  // 调用make函数创建实例
        });
        return $dependencies;
    }
}

$app = new Application();
$user = $app->make('User');
$user->login();

虽说已经实现了一个简单的容器,但是并不能很好的解耦,User 依赖的是具体类,而不是依赖于抽象接口。
当我们想将FileLog修改为DbLog时,如果只有一两个类使用了FileLog那么还有,当然日志不可能仅在一两个类中使用,非常繁琐,所以需要实现在配置文件中,通过修改log=database或者log=file来进行配置日志。
此时我们需要借助一个容器来实现绑定,而此时类应该依赖于抽象,不应该再依赖于实体。

  • 接下来,一步步的了解Laravel服务容器的实现,当然进行了简化。

容器代码阅读

源码阅读

  • 在入口文件public/index.php有一行代码是这样写的: $app = require_once __DIR__.'/../bootstrap/app.php';,包含了bootstrap/app.php文件, $app这个变量可以理解为是容器。
  • bootstrap/app.php代码定义如下:
<?php
# 实例化了一个Application类,并将跟目录传进去了。
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

# 注册单例对象
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

// 省略类似代码

# 返回核心应用(容器)
return $app;
  • 接着鼠标移动到Application上按CTRL+B,跳转到Application的构造方法中, 其内容如下:
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath); // 设置路径
        }

        $this->registerBaseBindings();  // 注册基本内容

        $this->registerBaseServiceProviders();  // 注册基本服务提供者
 
        $this->registerCoreContainerAliases(); // 注册核心容器别名
    }

构造方法主要是对项目路径的绑定,注册基本应用,注册基本服务提供者,注册容器核心别名,读者可自行点进去看源码,现在主要来看下最后一个方法做了什么, 代码如下:

    public function registerCoreContainerAliases()
    {
        foreach ([
            'app'                  => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
		// 省略了一部分
        'log'                  => [\Illuminate\Log\Writer::class, \Illuminate\Contracts\Logging\Log::class, \Psr\Log\LoggerInterface::class],
         'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

可以看到foreach中是一个关联数组, 如键log中对应也是一个数组。通过迭代获取每个键和值,然后执行了, this->alias方法。alias方法中只有简单的两行:

    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $abstract; // 键值对换存储
        $this->abstractAliases[$abstract][] = $alias; // 这里相等于把原来的数组复制了一份。
    }

如果源码看的不清楚,可以用以下代码执行打印出来看看:

<?php
namespace IOC;

interface LogInterface{}

class LogWrite{}


class Application
{

    private $aliases = [];
    private $abstractAliases = [];

    public function __construct()
    {
        $this->registerCoreContainerAliases();
    }

    public function registerCoreContainerAliases()
    {
        foreach ([
                     'log' => [LogWrite::class, LogInterface::class]
                 ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $abstract; // 键 值 对调换
        $this->abstractAliases[$abstract][] = $alias; // 将值添加到对应的键的数组中。
    }
    
    public function print()
    {
        print_r($this->aliases);
        print_r($this->abstractAliases);
    }
}

$app = new Application();
$app->print();
  • 接下来看bootstrap/app.php中的代码:
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);
  • singleton中实际上调用了bind方法, 是对bind方法的一层封装,bind方法中参数有三个, 依次是$abstract(属性名), $concrete(闭包函数), $share是否共享,该对象用于标识全局是否只有一个实例。
    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true); // bind一个单例.
    }
  • bind方法源码如下:
    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);
        /*先删除已有的实例, unset($this->instances[$abstract], $this->aliases[$abstract]);
		这里终于见到了上面提到的$this->aliases数组了。
		*/

        if (is_null($concrete)) {  // 如果没有闭包参数
            $concrete = $abstract;  // 则把当前闭包设置为属性,下面绑定前会先判断是否为闭包,否则去获取闭包
        }
        
        if (! $concrete instanceof Closure) {
        	// 如果不是闭包则去获取一个闭包
            $concrete = $this->getClosure($abstract, $concrete);
        }
		// compact() 在当前的符号表中查找该变量名并将它添加到输出的数组中,变量名成为键名而变量的内容成为该键的值
		// 添加到bindings数组中
        $this->bindings[$abstract] = compact('concrete', 'shared');
        
        if ($this->resolved($abstract)) {// 如果这个属性已经实例化了,那么会重新实例化
            $this->rebound($abstract);  // 到这里才进行实例化
        }
    }
  • 主要看看rebound方法:
    protected function rebound($abstract)
    {
        $instance = $this->make($abstract);
        // 获取当前abstract rebound操作需要调用方法
        foreach ($this->getReboundCallbacks($abstract) as $callback) {
            call_user_func($callback, $this, $instance); // 调用函数, 第一个为回调函数,后面为参数。
        }
    }
  • 接着看make方法:
public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

在make中调用了resolve,有两个参数,第一个参数为属性,第二个为参数, resolve代码如下:

    protected function resolve($abstract, $parameters = [])
    {	
    	
    	// 这里出现了前面提到的alias数组,这里正是通过log去拿到了LogWrite:class的操作。
        $abstract = $this->getAlias($abstract);
		
		// 判断参数是否为空,或者有没有上下文绑定
        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
		// 如果已经有该实例了且不需要上下文绑定,那么直接可以返回实例了,也表示单例
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];  
        }

        $this->with[] = $parameters;
		// 拿到其闭包
        $concrete = $this->getConcrete($abstract);
		// isBuildabe方法通过$concrete === $abstract || $concrete instanceof Closure 来判断是否可以实例化;
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete); // 可以实例化则调用build函数
        } else {
            $object = $this->make($concrete); // 否则递归调用make方法。
        }
        
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

 		// 判断是否单例或者不需要上下文
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object; // 直接将实例保存
        }
		// 进行需要回调的函数调用
        $this->fireResolvingCallbacks($abstract, $object);
        
        $this->resolved[$abstract] = true;  // 标识为已经注册过的属性
        array_pop($this->with);
        return $object;  // 最后返回一个实例对象
    }
  • 接下来主要看看build方法干了什么, 其代码:

    public function build($concrete)
    {

        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        $reflector = new ReflectionClass($concrete);
        
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();
        
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        $instances = $this->resolveDependencies(
            $dependencies
        );

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

我上篇文章Laravel源码分析之控制反转和依赖注入中实现的其实就是这部分内容的简化操作,只不过这里是闭包,而我的示例中是类,都是通过反射拿构造函数,通过构造函数拿参数,如果参数也需要参数,那么递归调用,直到没有定义构造函数为止即结束递归。

总结

  • IOC容器记录键值和类或抽象类或闭包对应的数组,来实现容器绑定。
  • 在进行bind的时候并不会创建实例,在build的时候才创建实例。
  • 在实例化的时候通过属性去实例化, 如属性log, 这时候通过log去拿到对应的类。
  • 然后有判断是否为全局对象或者已经实例化可以直接返回,又或者需要进行清除。
  • 后面递归地通过反射拿到构造函数,再通过构造函数拿到其参数,直到最后创建好一个对象返回。

ICO容器的实现

思路

以上篇文章用户和日志的例子来思考:

  • 创建一个数组来进行绑定: [‘log’=> FileLog, ‘user’=> User]
  • 在进行ioc->make(‘user’) 的时候再通过user去拿User
  • 拿到User之后,通过反射来创建实例。

代码

<?php


/**
 * 日志抽象接口
 */
interface Log
{
    public function write();  // 子类需要实现一个write方法
}

/**
 * 文件日志实现
 * @package IOC
 */
class FileLog implements Log
{
    public function write()
    {
        echo '日志驱动: 文件' . PHP_EOL;
    }
}

/**
 * 数据库日志实现
 * @package IOC
 */
class DatabaseLog implements Log
{
    public function write()
    {
        echo '日志驱动: 数据库' . PHP_EOL;
    }
}

/**
 * 用户类
 * @package IOC
 */
class User
{
    protected $log;

    public function __construct(Log $log)
    {
        $this->log = $log;
    }

    /**
     * 用户登录成功并记录日志
     */
    public function login()
    {
        echo '用户登录成功...';
        $this->log->write();
    }
}

/**
 * IOC容器的实现
 * @package ClassmateLin
 */
class Ioc
{
    // 保存绑定的属性
    public $bindings = [];

    /**
     * 属性进行绑定
     * @param $abstract
     * @param $concrete
     */
    public function bind($abstract, $concrete)
    {
        // 绑定的时候还不需要创建对象, 只有在我们调用make的时候才需要创建对象,可以节省内存, 所以绑定一个闭包。
        $this->bindings[$abstract]['concrete'] = function ($ioc) use ($concrete) {
                return $ioc->build($concrete);
        };

    }

    /**
     * 创建对象
     * @param $abstract
     * @return mixed
     */
    public function make($abstract)
    {
        $concrete = $this->bindings[$abstract]['concrete']; // 根据属性获取其闭包
        return $concrete($this);  // 上面定义的闭包函数参数是ioc,也就是需要将类实例本身传递进去, 闭包函数内部调用了build方法。
    }

    // 创建对象
    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); // 通过带参数的反射接口创建一个实例
        }
    }

    /**
     * 获取函数依赖
     * @param $params
     * @return array
     */
    protected function getDependencies($params) {
        $dependencies = [];
        foreach ($params as $param) {
            # $param->getClass()->name可以拿到类名
            $dependencies[] = $this->make($param->getClass()->name);
        }
        return $dependencies;
    }

}

//实例化IoC容器
$ioc = new Ioc();
$ioc->bind('Log','FileLog'); // 绑定日志
$ioc->bind('user','User'); // 绑定用户
$user = $ioc->make('user'); // 创建用户
$user->login();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章