Laravel核心概念剖析

文章内容纯属个人对理解,如有不对的地方还望大佬多多指教。文章内容基于Laravel5.5,加黑字体的是关键词可以帮助加深理解。

建议结合本社区【服务容器】和【服务提供者】章节看加深理解。

核心概念

Laravel的核心概念是服务容器服务提供者,他们是框架的基础。框架提供的所有能力都由服务提供者引导绑定到容器中。容器主要定义了一些绑定解析服务的方式,这些方式也可以理解为容器对外提供的接口。服务提供者则引导了容器如何去解析和实例化该服务。下面详细剖析服务容器和服务提供者的概念:

1、服务容器

服务容器是一个用于管理类依赖以及实现依赖注入的强有力工具,它主要有laravel/framework/src/Illuminate/Container/Container.php类提供服务,文中具体代码可以在该类中找到。

1.1 绑定服务接口

容器类用来管理绑定关系的属性主要有两个bindingsinstance,其中instance属性的绑定关系是单例的。下面看绑定服务的几种方式:

// 1、通过bind绑定
$this->app->bind('redis.connection', function ($app) {
    return $app['redis']->connection();
});

// 2、绑定单例
$this->app->singleton('redis', function ($app) {
    $config = $app->make('config')->get('database.redis');
    return new RedisManager(Arr::pull($config, 'client', 'predis'), $config);
});

// 3、直接绑定实例(单例)
$this->app->instance('test', new Test);

再看这三种绑定方式的具体实现:

1.1.1bind函数

bind在将绑定关系放入到了bindings属性,此处只是绑定抽象和实现的关系,具体实现不会被实例化。

// 解释:将抽象$abstract绑定到具体的实现$concrete。$shared定义该实现是否为单例
public function bind($abstract, $concrete = null, $shared = false)
{
    // 删除alias和instance的绑定,将抽象$abstract绑定到$bindings
    $this->dropStaleInstances($abstract);
    // 如果没有给出具体类型,则设置具体类型为该抽象类型
    if (is_null($concrete)) {
      $concrete = $abstract;
    }

    // 如果具体的实现$concrete不是匿名函数、则把它包装为匿名函数
    if (! $concrete instanceof Closure) {
      $concrete = $this->getClosure($abstract, $concrete);
    }

   // 将抽象的实现绑定到bindings属性
    $this->bindings[$abstract] = compact('concrete', 'shared');

   // 如果该抽象已在容器中解析,则触发该抽象绑定的回调。 
    if ($this->resolved($abstract)) {
      $this->rebound($abstract);
    }
}
1.1.2singleton函数

可见singleton函数调用的是bind,第三个参数shared参数为true,指定该服务为单例。

// 绑定单例
public function singleton($abstract, $concrete = null)
{
  $this->bind($abstract, $concrete, true);
}
1.1.3instance函数

instance函数绑定的是实例,也可以是其他基础类型,解析时作为单例处理。

// 绑定实例 
public function instance($abstract, $instance)
{ 
    // 删除abstractAliases的绑定 
    $this->removeAbstractAlias($abstract); 

    // 检查是否已绑定到$bindings、$instances或$aliases
    $isBound = $this->bound($abstract); 
    unset($this->aliases[$abstract]); 

    $this->instances[$abstract] = $instance; 

    // 如果该抽象已绑定过(属于重复绑定),则触发该抽象绑定的回调。
    if ($isBound) {
        $this->rebound($abstract); 
    } 
    return $instance;
}

关于绑定大概就这些,此处先不考虑服务延迟加载,上下文绑定等等…先抛开复杂的处理看下绑定的本质,下面看下对解析的处理。

2.2 解析服务接口

解析对外提供的函数有app()->make(),app()->makeWith(),resolve(),app()app()->get()这几个,其中app()->get()会提前判断要解析的服务是否存在,不存在话抛出EntryNotFoundException异常,其他接口则直接抛出ReflectionException异常。最终的解析逻辑都一样,主要依赖容器中的resolvebuild函数,下面具体看下这两个函数的实现:

2.2.1 resolve 函数
protected function resolve($abstract, $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 对单例服务的处理,不需要处理上下文的情况下,如果存在直接返回,不会重新new一个新的实例
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    // 尝试从bindings中获取具体实现
    $concrete = $this->getConcrete($abstract);

    // 如果抽象$abstract和实现$concrete完全相等 或者 $concrete是匿名函数,则用build去解析,否则将具体实现$concrete再走一遍make,最终也是走本接口,可以看作是一个递归调用
    if ($this->isBuildable($concrete, $abstract)) {

       // 这快处理了类的实例化,依赖检查等,下面具体看
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // 对这个服务做一些扩展处理
    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;
}

resolve它主要做了对单例的处理,上下文检查,解析事件的触发和服务的扩展性处理。

2.2.2 build函数

此处是依赖处理和实例化对象的关键。

public function build($concrete)
{
    // 如果具体实现是个匿名函数,直接调用匿名函数,由其返回实现即可。
    if ($concrete instanceof Closure) {
        // getLastParameterOverrid的用意是?
        return $concrete($this, $this->getLastParameterOverride());
    }
     // 获取类的反射实例
    $reflector = new ReflectionClass($concrete);

    // 不可以实例化时抛出异常
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;
    // 获取类的构造函数
    $constructor = $reflector->getConstructor();

    // 如果没有构造函数,直接new出实例即可
    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);
}

到这里,服务绑定和服务解析就差不多了。这些东西有什么用呢?下面再看服务提供者中怎么使用它们。

2、服务提供者

所有的服务提供者都继承于Illuminate\Support\ServiceProvider抽象类,该抽象类对子类暴露了容器变量$app用来操作容器。在服务提供者中包会含一个 register 和一个 boot 函数。在 register 方法中, 你可以调用容器对外提供的绑定接口注册服务。boot函数会在所有服务注册完之后依次调用。下面看一个具体的服务提供者例子

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

 /**
 * 继承自ServiceProvider类
 */
class RiakServiceProvider extends ServiceProvider
{
    /** 
    * 是否延时加载该服务提供者,此处意味着在框架启动阶段不会调用register函数进行服务绑定,只会将该服务放入延迟加载列表,这些数据会放在bootstrap/cache/services.php文件中,该文件包含了所有服务提供者提供的所有服务,框架每次启动时都会检查是否需要重新生成。
    * @var bool 
    */ 
    protected $defer  =  true;

    // 在服务容器里注册(框架启动阶段调用)
    public function register()
    {
        $this->app->singleton(Connection::class, function ($app) {
            return new Connection(config('riak'));
        });

        $this->app->bind('redis', function ($app) {
            return new Redis;
        });

        $this->app->instance('test', new Test);
    }

    // 所有的服务提供者加载完后,会依次调用boot函数
    public function boot()
    {
        ... code
    }

    /**
     * 获取提供器提供的服务。 
    * 此处用于生成bootstrap/cache/services.php文件时获取该提供者提供的服务列表
    * @return array 
    */
    public function provides()
    {
        return [Connection::class, 'redis', 'test'];
    }
}

添加完服务提供者之后,需要将该提供者在config/app.php 配置文件中注册,才会生效。然后你就可以在框架的任何地方使用了。

3、具体使用

怎么使用呢?这要看框架支持怎样的依赖注入了,主要有以下几种方式:

3.1、构造函数注入

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    /**
     * TestController constructor.
     * 这里$request变量已经是Request的实例了,不用显示的new(也不能去new,Request构造函数也会依赖其他服务,这些依赖处理交给框架的服务容器去解析就好了)
     */
    public function __construct(Request $request)
    {
        dump($request->all());
    }
}

3.2、函数参数注入

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    /**.
     * 和构造函数注入同理
     */
    public function hello(Request $request)
    {
        dump('Hello'.$request->name);
    }
}

3.3、手动解析

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    public function test(Request $request)
    {
        // 1、解析服务,这里取出来的redis对象是否为单例取决于哪种方式注册
        $redis = app()->make('redis');
        $redis = app()->makeWith('redis', ['param' => 'test']);
        $redis = resolve('redis');
        $redis = app('redis');
        $redis = app()->get('redis');

        // 2、实例化类
        app(\App\Test::class, ['var1' => 'var1'])
        app(\App\Test::class, ['var2' => 'var2', 'var1' => 'var1'])
    }
}

实例化类为什么不直接new呢?因为这样实例化,不用处理Test的依赖Request

// 示例类
<?php
namespace App;

use Illuminate\Http\Request;

class Test
{
    public $var1;
    public $var2;

     /**
     * Test constructor. * @param $var1
      * @param $var2
      */
    public function __construct(Request $request, $var1, $var2 = 'var2')
    {  
        $this->var1 = $var1;
        $this->var2 = $var2;

        // 不用处理,交给容器解析
        dump($request->all());
    }
 }

4、总结

文章主要讲了容器对外提供的绑定和解析服务的几种方式,随后讲了服务提供者对容器的使用,也给了具体的例子,希望对大家有所帮助。文中只给出了一些基本的实现逻辑,关于上下文的处理,有兴趣的可以结合社区文档继续深挖。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!