ThinkPHP6 原始碼閱讀(十二):系統服務

tsin發表於2019-09-04

說明

什麼是系統服務?可以拆解為:誰對誰提供服務?提供了什麼服務?這個物件為什麼需要使用系統服務?回答曰:以下面要介紹到的ModelService為例,ModelService類提供服務,服務物件是Model類,ModelService類主要對Model類的一些成員變數進行初始化,為後面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\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類了。

總結

開頭的回答還漏了一個問題:這個物件(類)為什麼需要使用系統服務?——當然你也可以使用簡單粗暴的方法直接修改一個類的「配置」,但使用系統服務有大大的好處和避免了直接修改類的壞處。從以上分析來看,個人覺得,使用系統服務,可以對一個類進行非入侵式的「配置」,如果哪天一個類的某些設定需要修改,我們不用直接修改這個類,只需要修改服務提供類就好了。

Was mich nicht umbringt, macht mich stärker

相關文章