ThinkPHP6 核心分析(十二):系统服务

说明

更新日志:2019-11-1 更新到6.0正式版。

什么是系统服务?系统服务是对于程序要用到的类在使用前先进行类的标识的绑定,以便容器能够对其进行解析(通过服务类的register方法),还有就是初始化一些参数、注册路由等(不限于这些操作,主要是看一个类在使用之前的需要,进行一些配置,使用的是服务类的boot方法)。以下面要介绍到的ModelService为例,ModelService类提供服务,ModelService类主要对Model类的一些成员变量进行初始化(在boot方法中),为后面Model类的「出场」布置好「舞台」。

下面先来看看系统自带的服务,看看服务是怎么实现的。

内置服务

系统内置的服务有:ModelServicePaginatorServiceValidateService类,我们来看看它们是怎么被注册和初始化的。

App::initialize()有这么一段:

foreach ($this->initializers as $initializer) {
    $this->make($initializer)->init($this);
}

这里通过循环App::initializers的值,并使用容器类的make方法获取每个$initializer的实例,然后调用实例对应的init方法。App::initializers成员变量的值为:

protected $initializers = [
    Error::class,
    RegisterService::class,
    BootService::class,
];

这里重点关注后面两个:服务注册和服务初始化。

服务注册

执行$this->make($initializer)->init($this)$initializer等于RegisterService::class时,调用该类中的init方法,该方法代码如下:

public function init(App $app)
{
    // 加载扩展包的服务
    $file = $app->getRootPath() . 'vendor/services.php';

    $services = $this->services;

    //合并,得到所有需要注册的服务
    if (is_file($file)) {
        $services = array_merge($services, include $file);
    }
    // 逐个注册服务
    foreach ($services as $service) {
        if (class_exists($service)) {
            $app->register($service);
        }
    }
}

服务注册类中,定义了系统内置服务的值:

protected $services = [
    PaginatorService::class,
    ValidateService::class,
    ModelService::class,
];

这三个服务和扩展包定义的服务将逐一被注册,其注册的方法register代码如下:

public function register($service, bool $force = false)
{
    // 比如 think\service\PaginatorService
    // getService方法判断服务的实例是否存在于App::$services成员变量中
    // 如果是则直接返回该实例
    $registered = $this->getService($service);
    // 如果服务已注册且不强制重新注册,直接返回服务实例
    if ($registered && !$force) {
        return $registered;
    }
    // 实例化该服务
    // 比如 think\service\PaginatorService,
    // 该类没有构造函数,其父类Service类有构造函数,需要传入一个App类的实例
    // 所以这里传入$this(App类的实例)进行实例化
    if (is_string($service)) {
        $service = new $service($this);
    }
    // 如果存在「register」方法,则调用之
    if (method_exists($service, 'register')) {
        $service->register();
    }
    // 如果存在「bind」属性,添加容器标识绑定
    if (property_exists($service, 'bind')) {
        $this->bind($service->bind);
    }
    // 保存服务实例
    $this->services[] = $service;
}

详细分析见代码注释。如果服务类定义了register方法,在服务注册的时候会被执行,该方法通常是用于将服务绑定到容器;此外,也可以通过定义bind属性的值来将服务绑定到容器。

服务逐个注册之后,得到App::services的值大概是这样的:

ThinkPHP6 源码阅读(十二):系统服务

每个服务的实例都包含一个App类的实例。

服务初始化

执行$this->make($initializer)->init($this)$initializer等于BootService::class时,调用该类中的init方法,该方法代码如下:

public function init(App $app)
{
    $app->boot();
}

实际上是执行App::boot():

public function boot(): void
{
    array_walk($this->services, function ($service) {
        $this->bootService($service);
    });
}

这里是将每个服务实例传入bootService方法中。重点关注bootService方法:

public function bootService($service)
{
    if (method_exists($service, 'boot')) {
        return $this->invoke([$service, 'boot']);
    }
}

这里调用服务实例对应的boot方法。接下来,我们以ModelServiceboot方法为例,看看boot方法大概可以做哪些工作。ModelServiceboot方法代码如下:

public function boot()
{
    // 设置Db对象
    Model::setDb($this->app->db);
    // 设置Event对象
    Model::setEvent($this->app->event);
    // 设置容器对象的依赖注入方法
    Model::setInvoker([$this->app, 'invoke']);
    // 保存闭包到Model::maker
    Model::maker(function (Model $model) {
        //保存db对象
        $db     = $this->app->db;
        //保存$config对象
        $config = $this->app->config;
        // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
        $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();

        if (is_null($isAutoWriteTimestamp)) {
            // 自动写入时间戳 (从配置文件获取)
            $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp'));
        }
        // 时间字段显示格式
        $dateFormat = $model->getDateFormat();

        if (is_null($dateFormat)) {
            // 设置时间戳格式 (从配置文件获取)
            $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s'));
        }

    });
}

可以看出,这里都是对Model类的静态成员进行初始化。这些静态成员变量的访问属性为protected,所以,可以在Model类的子类中使用这些值。

自定义系统服务

接着,我们自己动手来写一个简单的系统服务。

  • 定义被服务的对象(类)

    创建一个文件:app\common\MyServiceDemo.php,写入代码如下:

    <?php
    namespace app\common;
    class MyServiceDemo
    {
        //定义一个静态成员变量
        protected static $myStaticVar = '123';
        // 设置该变量的值
        public static function setVar($value){
            self::$myStaticVar = $value;
        }
        //用于显示该变量
        public function showVar()
        {
            var_dump(self::$myStaticVar);
        }
    }
  • 定义服务提供者

    在项目根目录,命令行执行php think make:service MyService,将会生成一个app\service\MyService.php文件,在其中写入代码:

    <?php
    namespace app\service;
    use think\Service;
    use app\common\MyServiceDemo;
    class MyService  extends Service
    {
        // 系统服务注册的时候,执行register方法
        public function register()
        {
            // 将绑定标识到对应的类
            $this->app->bind('my_service', MyServiceDemo::class);
        }
        // 系统服务注册之后,执行boot方法
        public function boot()
        {
            // 将被服务类的一个静态成员设置为另一个值
            MyServiceDemo::setVar('456');
        }
    }
  • 配置系统服务

    app\service.php文件(如果没有该文件则创建之),写入:

    <?php
        return [
            '\app\service\MyService'
        ];
  • 在控制器中调用
    创建一个控制器文件app\controller\Demo.php,写入代码:

    <?php
    namespace app\controller;
    use app\BaseController;
    use app\common\MyServiceDemo;
    class Demo extends BaseController
    {
        public function testService(MyServiceDemo $demo){
            // 因为在服务提供类app\service\MyService的boot方法中设置了$myStaticVar=‘456’\
            // 所以这里输出'456'
            $demo->showVar();
        }
    
        public function testServiceDi(){
            // 因为在服务提供类的register方法已经绑定了类标识到被服务类的映射
            // 所以这里可以使用容器类的实例来访问该标识,从而获取被服务类的实例
            // 这里也输出‘456’
            $this->app->my_service->showVar();
        }
    }

    执行原理和分析见代码注释。另外说说自定义的服务配置是怎么加载的:App::initialize()中调用了App::load()方法,该方法结尾有这么一段:

    if (is_file($appPath . 'service.php')) {
        $services = include $appPath . 'service.php';
        foreach ($services as $service) {
            $this->register($service);
        }
    }

    正是在这里将我们自定义的服务加载进来并且注册。

在Composer扩展包中使用服务

这里以think-captcha扩展包为例,该扩展使用了系统服务,其中,服务提供者为think\captcha\CaptchaService类,被服务的类为think\captcha\Captcha

首先,项目根目录先运行composer require topthink/think-captcha安装扩展包;安装完成后,我们查看vendor\services.php文件,发现新增一行:

return array (
  0 => 'think\\captcha\\CaptchaService',  //新增
);

这是怎么做到的呢?这是因为在vendor\topthink\think-captcha\composer.json文件配置了:

"extra": {
    "think": {
        "services": [
            "think\\captcha\\CaptchaService"
        ]
    }
},

而在项目根目录下的composer.json,有这样的配置:

"scripts": {
    "post-autoload-dump": [
        "@php think service:discover",
        "@php think vendor:publish"
    ]
}

扩展包安装后,会执行这里的脚本,其中,跟这里的添加系统服务配置相关的是:php think service:discover。该指令执行的代码在vendor\topthink\framework\src\think\console\command\ServiceDiscover.php,相关的代码如下:

foreach ($packages as $package) {
    if (!empty($package['extra']['think']['services'])) {
        $services = array_merge($services, (array) $package['extra']['think']['services']);
    }
}

$header = '// This file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL . 'declare (strict_types = 1);' . PHP_EOL;

$content = '<?php ' . PHP_EOL . $header . "return " . var_export($services, true) . ';';

file_put_contents($this->app->getRootPath() . 'vendor/services.php', $content);

可以看出,扩展包如果有配置['extra']['think']['services'],也就是系统服务配置,都会被写入到vendor\services.php文件,最终,所有服务在系统初始化的时候被加载、注册和初始化。

分析完了扩展包中服务配置的实现和原理,接着我们看看CaptchaService服务提供类做了哪些初始化工作。该类只有一个boot方法,其代码如下:

public function boot(Route $route)
{
    // 配置路由
    $route->get('captcha/[:config]', "\\think\\captcha\\CaptchaController@index");
    // 添加一个验证器
    Validate::maker(function ($validate) {
        $validate->extend('captcha', function ($value) {
            return captcha_check($value);
        }, ':attribute错误!');
    });
}

有了以上的先行配置,我们就可以愉快地使用Captcha类了。

总结

使用系统服务有大大的好处和避免了直接修改类的坏处。从以上分析来看,个人觉得,使用系统服务,可以对一个类进行非入侵式的「配置」,如果哪天一个类的某些设定需要修改,我们不用直接修改这个类,只需要修改服务提供类就好了。对于扩展包来说,系统服务使其可以在扩展中灵活配置程序,达到开箱即用的效果。不过,有个缺点是系统服务类都要在程序初始化是进行实例化,如果一个系统的服务类很多,势必影响程序的性能。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Was mich nicht umbringt, macht mich stärker
本帖由系统于 4年前 自动加精
讨论数量: 1

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