Laravel核心原理学习:匿名函数、服务容器、服务提供者

欢迎指正内容不严谨或有误的地方!

Reference
  1. Laravel 8 中文文档
  2. cxp1539/laravel-core-learn
  3. 3-php/laravel底层核心代码分析之匿名函数
  4. 4-php/laravel底层核心代码分析核心概念serviceContainer&serviceProvider
1.匿名函数
  1. 匿名函数: 允许临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。
  2. 回调函数: 不是直接执行的函数,而是通过其他方式(call_user_func_array()/array_map())来调用的函数。
  3. 闭包: 创建时封装周围状态的函数,即使周围的环境不存在了,闭包中的状态也还存在(子函数使用父函数中的局部变量,这种行为叫做闭包,这也是使用闭包的原因);闭包就是能够读取其他函数内部变量的函数(即匿名函数+use关键字);

①闭包如果不结合 use 关键字使用,毫无意义,可以使用普通函数来代替该匿名函数。闭包常见的使用方式是:1.当做参数传递;2.当做返回值返回。
可以说回调函数和闭包都是匿名函数的用法之一,本质上它们都是匿名函数。
③匿名函数只有在调用的时候才会真正的被解析。而调用匿名函数的时候值需要传递的参数是紧接着 function 后的变量,而不是 use 关键字;

// ①回调函数
function backFunc($a) {
    return $a;
}
// 触发回调
call_user_func_array('backFunc',['a']);

// ②匿名函数
$anoFunc = function($var) {
    return $var;
}
$anoFunc('a');

// ③闭包
function parentFunc(){
    $a = 1;
    $b = 2;
    // 返回了一个匿名函数,这个函数调用了其父函数中的参数
    return function($date) use ($a, $b) {
        echo $date.":".$a.':'.$b
    }
}
$res = parentFunc();
$res('2021-11-12');
2.服务容器和服务提供者
2.1 概念理解
  1. 我们使用框架的原因就是框架已经集成好了一些模块和功能(比如:路由、缓存、图片处理、支付模块…),使得开发变得简单快速。
  2. 传统框架的做法是将这些模块直接集成在框架中,如果框架没有某项服务(比如:短信服务、邮件服务、队列…)就需要开发者自己去开发,这些服务是作为框架的一部分的。
  3. 而 Laravel 则将这些模块/服务提取出来,不再作为框架的一部分,而是以第三方的形式做了处理。不再是框架本身提供这些服务,而是由 serviceProvider 来提供,包括我们常见的数据库管理、图片管理都是第三方提供。
  4. serviceContainer 则提供了注册机制,通过依赖注入可以将这些服务注册进服务容器本身,很好的解决了类与类之间的依赖关系。
  5. 当开发者需要调用这些服务时,只需要通过 serviceContainer 实例化对象即可使用。

serviceProvider 是有能力提供服务的服务提供者;而 serviceContainer 则是为了更加方便的管理这些服务,即一个类的实例化对象,在启动之初 serviceContainer 会对所有可能用到的服务进行加载,用到的时候再去解析调用其中的方法。

Laravel 中除去基本的事件、异常处理、Facades 外,其他服务全都是通过 serviceProvider 提供的,比如常见的路由、缓存、数据库管理等。
由于 serviceProvider 是被定义为第三方的存在,所以它们不作为 Laravel 的必须项目一定要加载,原则上开发者可以不加载任何用不到的非核心的 serviceProvider。

config\app.php->providers 下可以看到我们用到的所有 serviceProvider,服务提供者主要分为三类:

  1. Framework Service Providers: 框架级别的服务提供者;
  2. Package Service Providers: 第三方包级别的服务提供者;
  3. Application Service Providers: 应用程序级别的服务提供者;

现实场景帮助理解:服务容器就像我们用到的手机,服务提供者就是手机里的APP,当我们需要使用某项功能时,我们就去调用APP(服务提供者),而我们下载安装APP的过程就是服务提供者注册的过程。

2.2 服务容器生成:instance、singleton、bind

核心逻辑

// ①public/index.php: 生成服务容器
$app = require_once __DIR__.'/../bootstrap/app.php';

// bootstrap/app.php: 实例化服务容器
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

// src/Illuminate/Foundation/Application.php
// 创建一个 Illuminate application 实例
public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }
    // 注册基础的 Bindings
    // 为 instances 属性赋值
    $this->registerBaseBindings();
    // 注册基础服务提供者
    // 为 bindings 属性赋值,将服务提供者注册进去
    $this->registerBaseServiceProviders();
    // 注册核心容器别名
    // 为 aliases 属性赋值
    $this->registerCoreContainerAliases();
}

关键代码

/**
 * src/Illuminate/Container/Container.php
 * Register an existing instance as shared in the container.
 * 将一个已经存在的实例共享到容器中
 *
 * @param  string  $abstract
 * @param  mixed  $instance
 * @return mixed
 */
public function instance($abstract, $instance)
{
    // 从上下文绑定别名缓存中删除别名。
    // 第一次进入时是没有对实例进行缓存的
    $this->removeAbstractAlias($abstract);

    // $this->instance('app', $this);
    // $abstract = 'app';
    // 确定给定的抽象类型是否已绑定。
    // 第一次进入时当然也是没有绑定的
    $isBound = $this->bound($abstract);

    // 取消别名绑定
    unset($this->aliases[$abstract]);

    // 将别名与实例绑定到 instances 属性中
    $this->instances[$abstract] = $instance;

    // 若之前已经绑定了该别名
    if ($isBound) {
        $this->rebound($abstract);
    }

    return $instance;
}


/**
 * src/Illuminate/Container/Container.php
 * Register a shared binding in the container.
 *
 * @param  string  $abstract
 * @param  \Closure|string|null  $concrete
 * @return void
 */
public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}


/**
 * src/Illuminate/Container/Container.php
 * Register a binding with the container.
 *
 * @param  string  $abstract
 * @param  \Closure|string|null  $concrete
 * @param  bool  $shared
 * @return void
 *
 * @throws \TypeError
 *
 * // ① 使用 singleton 进行 eventServiceProvider bind 时:$abstract = 'events', $concrete = 匿名函数, $shared = true
 * // ② $this->singleton(Mix::class); 是,$abstract = 'Illuminate\Foundation\Mix', $concrete = null, $shared = true
 * // ③ $app->singleton(Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);
 */
public function bind($abstract, $concrete = null, $shared = false)
{
    // 删除历史实例与别名
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    // 如果 factory 不是一个闭包,则意味着它是一个绑定到容器中的抽象类的类名
    // 将其包装在闭包中是为了在扩展时更加方便
    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }
        // 如果绑定的不是闭包,则会将字符串转换为闭包
        $concrete = $this->getClosure($abstract, $concrete);
    }

    // 对每个参数,compact() 在当前的符号表中查找该变量名并将它添加到输出的数组中,变量名成为键名而变量的内容成为该键的值。
    // 核心实现
    $this->bindings[$abstract] = compact('concrete', 'shared');

    // 若抽象类型已经被解析到容器中,我们将会触发回弹监听器
    // 这样任何已经解析的对象都可以通过监听器回调更新其对象副本
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

/**
 * Get the Closure to be used when building a type.
 *
 * @param  string  $abstract
 * @param  string  $concrete
 * @return \Closure
 *
 * // ② $this->singleton(Mix::class); 是,$abstract = 'Illuminate\Foundation\Mix', $concrete = null, $shared = true
 * // ③ $app->singleton(Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);
 */
protected function getClosure($abstract, $concrete)
{
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        //
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }

        return $container->resolve(
            $concrete, $parameters, $raiseEvents = false
        );
    };
}

追查服务容器生成部分的相关代码可以发现主要逻辑是放在 src/Illuminate/Foundation/Application.php__construct中,这里完成了 Laravel 服务容器的核心功能的绑定,主要是通过 src/Illuminate/Container/Container.php中的 instance/bind/alias 方法实现了 服务容器 Applicationbindings/instances/aliases属性的绑定。
instance/bind/alias中值得一看的其实就是 bind方法,该方法注册了一个 binding 到我们的 Laravel 服务容器中,而其中调用的 getClosure函数更是我们后续对 binding进行解析时有着至关重要的函数。

2.3 服务容器解析:make
// 使用 bindings 对获取抽象类的实例
// 使用 bind 方法允许对实例化对象进行个性化操作,而 make 方法只是粗糙的获取到了抽象类的一个实例
// 这里的 Kernel::class 是 Illuminate\Contracts\Http\Kernel::class
$kernel = $app->make(Kernel::class);
// 直接实例化 Kernel 而不是通过注册的 bindings 进行实例化
//$kernel = $app->make(\App\Http\Kernel::class);

在入口文件中可以看到,紧接着服务容器生成后的代码就是对 make函数的调用,顾名思义 make函数是对抽象类的实例化。

/**
 * src/Illuminate/Foundation/Application.php
 * Resolve the given type from the container.
 *
 * @param string $abstract
 * @param array $parameters
 * @return mixed
 */
public function make($abstract, array $parameters = [])
{
    $this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));

    return parent::make($abstract, $parameters);
}

/**
 * src/Illuminate/Container/Container.php
 * Resolve the given type from the container.
 *
 * @param  string|callable  $abstract
 * @param  array  $parameters
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

/**
 * src/Illuminate/Container/Container.php
 * Resolve the given type from the container.
 *
 * @param  string|callable  $abstract
 * @param  array  $parameters
 * @param  bool  $raiseEvents
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 * @throws \Illuminate\Contracts\Container\CircularDependencyException
 *
 * // ① $kernel = $app->make(Kernel::class); $abstract = 'Illuminate\Contracts\Http\Kernel';
 *
 */
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    if ($raiseEvents) {
        $this->fireBeforeResolvingCallbacks($abstract, $parameters);
    }

    $concrete = $this->getContextualConcrete($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    if (is_null($concrete)) {
        // 这里获取了 abstract 在服务容器中绑定的实例(匿名函数|闭包)
        // 1. 解析过程中第一次调用时,$abstract = Illuminate\Contracts\Http\Kernel::class; $concrete = Closure;
        // 关键是此时闭包 $concrete 中的 $abstract = "Illuminate\Contracts\Http\Kernel";$concrete = "App\Http\Kernel";
        // 闭包中封闭的参数是在注册时取得的
        // 2. 解析过程中第二次调用时,$abstract = 'App\Http\Kernel'; 而 App\Http\Kernel 并未在服务容器中注册过
        // 此时返回的 $concrete 就是 $abstract ,也就是 $abstract = $concrete = 'App\Http\Kernel;
        $concrete = $this->getConcrete($abstract);

//            if ($abstract == 'App\Http\Kernel') dd($abstract, $concrete);
    }

    if ($this->isBuildable($concrete, $abstract)) {
        // 判断 concreate 和 abstract 相等 或者说 concrete 是否是匿名函数
        $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;
    }

    if ($raiseEvents) {
        $this->fireResolvingCallbacks($abstract, $object);
    }

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

可以看到对 make方法的调用最终还是对 src/Illuminate/Container/Container.php中的 resolve函数的调用,以 $app->make(Kernel::class); 的实例化为例,在早先服务容器的初始化过程中我们就已经将 HTTP\Kernel注册到了服务容器的 bindings属性中。由于是第一次进行实例化,此时上下文中是不存在 Illuminate\Contracts\Http\Kernel::class的实例的,因此此时的 $concrete = null;,因此会调用 getConcrete获取 bindings中的 concrete,也就是前面在 getClosure方法中生成的闭包,由于此时的 concrete 还是一个闭包,因此会紧接着执行 build方法并执行该闭包,很显然此时的 $abstract == $concrete不成立,因此会将 $concrete再次传入 $container->resolve($concrete, $parameters, $raiseEvents = false);进行递归。此时也就是第二次调用 resolve函数中的形参 $abstract变为了我们 bindings中绑定的 App\Http\Kernel::class,而这个别名在 bindings中是没有注册的,因此经过 getConcrete函数后$abstract = $concrete = 'App\Http\Kernel;。此时再次 build,因为 $concrete不再是一个闭包而是一个代表了类名的字符串,因此会通过IOC容器进行实例化,最终返回实例。

// bootstrap/app.php
// 将 HTTP\Kernel 注册到服务容器中
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);
2.4 容器事件

服务容器每次解析对象时都会触发一个事件,我们可以使用 resolving() 方法监听这个事件;

$this->app->resolving(function ($object, $app) {
    // 当容器解析任何类型的对象时调用...
});

$this->app->resolving(\HelpSpot\API::class, function ($api, $app) {
    // 当容器解析类型为 "HelpSpot\API" 的对象时调用...
});

// 被解析的对象将会被传入回调函数,这使得我们能够在对象被传给调用者之前给它设置额外的属性
2.5 服务提供者

所有的服务提供者都会继承 Illuminate\Support\ServiceProvider 类,大部分的 ServiceProvider 都会包含一个 register 和一个 boot 方法。

  1. register 方法中,可以将服务直接绑定到服务容器中(部分框架级的 ServiceProvider 是不需 register 的,因为它们在服务容器生成的时候就已经注册了。此外不要尝试在 register 方法中注册任何监听器、路由或者任何功能(因为服务提供者是在一起进行注册的,如果互相具有依赖,可能会出现所依赖的提供者未及时注册的情况);
  2. boot 引导方法,该方法在所有服务提供者被注册后才会被调用 RouteServiceProvider(实现了该方法调度路由),也就是说可在该方法中访问框架已经注册的其他服务。
// public/index.php: 生成服务容器
$app = require_once __DIR__.'/../bootstrap/app.php';

// src/Illuminate/Foundation/Application.php
// 创建一个 Illuminate application 实例
public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }
    // 注册基础的 Bindings
    // 为 instances 属性赋值
    $this->registerBaseBindings();
    // 注册基础服务提供者
    // 为 bindings 属性赋值,将服务提供者注册进去
    $this->registerBaseServiceProviders();
    // 注册核心容器别名
    // 为 aliases 属性赋值
    $this->registerCoreContainerAliases();
}

在服务容器的初始化过程中,我们将基础的服务提供者(事件/日志/路由)注册到了容器中,而其他的服务提供者都将在 Kernelhandle方法中进行注册。

// 使用 bindings 对获取抽象类的实例
// 使用 bind 方法允许对实例化对象进行个性化操作,而 make 方法只是粗糙的获取到了抽象类的一个实例
// Illuminate\Contracts\Http\Kernel;
$kernel = $app->make(Kernel::class);
// 直接实例化 Kernel 而不是通过注册的 bindings 进行实例化
//$kernel = $app->make(\App\Http\Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();
protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,   // 加载环境变量
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,          // 加载配置文件
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,           // 处理异常
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,            // 注册 Facades
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,          // 注册提供者(主要是调用 provider 中的 register 方法)
    \Illuminate\Foundation\Bootstrap\BootProviders::class,              // 启动提供者(主要是调用 provider 中的 boot 方法)
];

$kernel->handle()中首先值得注意的是 sendRequestThroughRouter的调用,紧接着就是核心逻辑 $this->bootstrap();该方法对 kernel中的 $bootstrappers属性调用了服务容器的 bootstrapWith方法,该方法对 $bootstrappers属性中的元素进行了迭代实现,并调用了每个实例中的 bootstrap方法。需要注意的是最后俩个 bootstrapper,也就是 RegisterProviders::classBootProviders::class。既然是迭代调用也就是按顺序调用的,在 RegisterProviders::classbootstrap中调用了服务容器的 $app->registerConfiguredProviders();,该方法对框架中的服务提供者进行了整合,最后注入到了 ProviderRepository中的 load方法进行迭代注册(这里需要注意的是仅进行了非延迟加载服务提供者的注册加载),至此服务提供者的注册结束。

// $providers->collapse()->toArray() 这里就是将 Collection 中的 items 数组再次合成了一个数组
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());

然后就是 BootProviders::classbootstrap,该方法调用了服务容器 $app->boot();该方法对前面注册服务提供者进行迭代操作,操作就是迭代调用了服务提供者的 boot方法,该操作的意义就是为了在解决服务提供者之间因为依赖可能出现的注册不及时问题。

// array_walk 使用用户自定义函数对数组中的每个元素做回调处理
array_walk($this->serviceProviders, function ($p) {
    $this->bootProvider($p);
});

protected function bootProvider(ServiceProvider $provider)
{
    $provider->callBootingCallbacks();

    if (method_exists($provider, 'boot')) {
        $this->call([$provider, 'boot']);
    }

    $provider->callBootedCallbacks();
}

服务提供者的 bindings 和 singletons 属性:

  1. bindings:所有需要注册的容器绑定;singletons:所有需要注册的容器单例;
  2. 当 serviceProvider 被框架加载时,将自动检查这些属性并注册相应的绑定;

延迟提供者
如果你的服务提供者仅需要在服务容器中注册,而不需要实现(这样做的目的是为了提升应用性能),可以选择延迟加载该绑定直到注册绑定的服务真的需要时再加载。
Laravel 编译并保存延迟服务提供者提供的所有服务的列表(存储在 bootstrap/cache/services.php下),以及其服务提供者类的名称。因此,只有当你在尝试解析其中一项服务时,Laravel 才会加载服务提供者。
要延迟加载提供者,需要实现 \Illuminate\Contracts\Support\DeferrableProvider 接口并置一个 provides 方法。这个 provides 方法返回该提供者注册的服务容器绑定:

3. Facades
3.1 外观模式 Facade
  1. Facades 为应用程序的服务容器中可用的类提供了静态代理
  2. Laravel 所有的 Facades 都在 Illuminate\Support\Facades 命名空间中定义。

Facades的实现:

  1. Laravel中,Facade 就是一个可以从容器访问对象的类,其中核心的类就是 Facade 类;
  2. 不管是 Laravel 自带的 Facade,还是自定义的 Facade都继承自 Illuminate\Support\Facades\Facade 类;
  3. Facade 基类使用了 __callStatic() 魔术方法,直到对象从容器中解析出来后,才会进行调用。
  4. 可以看到在 __callStatic方法中通过 static::getFacadeRoot();获取到了类的实例,而 static::getFacadeAccessor()通过静态代理的方式获取了当前类下的该方法返回的类在服务容器中的别名(一般是别名)。
  5. 如果是对象就直接使用对象中对应的方法,如果已经解析过的实例就是直接调用,否则则在服务容器中进行解析后调用。
// src/Illuminate/Support/Facades/Facade.php
public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    if (static::$app) {
        return static::$resolvedInstance[$name] = static::$app[$name];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值