[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`的值大概是這樣的:

每個服務的實例都包含一個`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`類了。
# 總結
使用系統服務有大大的好處和避免了直接修改類的壞處。從以上分析來看,個人覺得,使用系統服務,可以對一個類進行非入侵式的「配置」,如果哪天一個類的某些設定需要修改,我們不用直接修改這個類,只需要修改服務提供類就好了。對于擴展包來說,系統服務使其可以在擴展中靈活配置程序,達到開箱即用的效果。不過,有個缺點是系統服務類都要在程序初始化是進行實例化,如果一個系統的服務類很多,勢必影響程序的性能。