[TOC] # 说明 什么是系统服务?系统服务是对于程序要用到的类在使用前先进行类的标识的绑定,以便容器能够对其进行解析(通过服务类的`register`方法),还有就是初始化一些参数、注册路由等(不限于这些操作,主要是看一个类在使用之前的需要,进行一些配置,使用的是服务类的`boot`方法)。以下面要介绍到的`ModelService`为例,`ModelService`类提供服务,`ModelService`类主要对`Model`类的一些成员变量进行初始化(在`boot`方法中),为后面`Model`类的「出场」布置好「舞台」。 下面先来看看系统自带的服务,看看服务是怎么实现的。 # 内置服务 系统内置的服务有:`ModelService`、`PaginatorService`和`ValidateService`类,我们来看看它们是怎么被注册和初始化的。 在`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 源码阅读(十二):系统服务](https://cdn.learnku.com/uploads/images/201909/01/27146/vVrTaWKoHK.PNG!/fw/1240) 每个服务的实例都包含一个`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`方法。接下来,我们以`ModelService`的`boot`方法为例,看看`boot`方法大概可以做哪些工作。`ModelService`的`boot`方法代码如下: ``` 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`类了。 # 总结 使用系统服务有大大的好处和避免了直接修改类的坏处。从以上分析来看,个人觉得,使用系统服务,可以对一个类进行非入侵式的「配置」,如果哪天一个类的某些设定需要修改,我们不用直接修改这个类,只需要修改服务提供类就好了。对于扩展包来说,系统服务使其可以在扩展中灵活配置程序,达到开箱即用的效果。不过,有个缺点是系统服务类都要在程序初始化是进行实例化,如果一个系统的服务类很多,势必影响程序的性能。