說明
什麼是系統服務?可以拆解為:誰對誰提供服務?提供了什麼服務?這個物件為什麼需要使用系統服務?回答曰:以下面要介紹到的ModelService
為例,ModelService
類提供服務,服務物件是Model
類,ModelService
類主要對Model
類的一些成員變數進行初始化,為後面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\CaptchaService
。
首先,專案根目錄先執行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
類了。
總結
開頭的回答還漏了一個問題:這個物件(類)為什麼需要使用系統服務?——當然你也可以使用簡單粗暴的方法直接修改一個類的「配置」,但使用系統服務有大大的好處和避免了直接修改類的壞處。從以上分析來看,個人覺得,使用系統服務,可以對一個類進行非入侵式的「配置」,如果哪天一個類的某些設定需要修改,我們不用直接修改這個類,只需要修改服務提供類就好了。