ThinkPHP6 核心分析:系統服務

什麼是系統服務?系統服務是對於程序要用到的類在使用前先進行類的標識的綁定,以便容器可以對其進行解析(經過服務類的 register 方法),還有就是初始化一些參數、註冊路由等(不限於這些操做,主要是看一個類在使用以前的須要,進行一些配置,使用的是服務類的 boot 方法)。如下面要介紹到的 ModelService 爲例,ModelService類提供服務,ModelService 類主要對 Model 類的一些成員變量進行初始化(在 boot 方法中),爲後面 Model 類的「出場」佈置好「舞臺」。php

下面先來看看系統自帶的服務,看看服務是怎麼實現的。json

 

內置服務

系統內置的服務有:ModelServicePaginatorService 和 ValidateService 類,咱們來看看它們是怎麼被註冊和初始化的。閉包

在 App::initialize() 有這麼一段:app

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

 

這裏經過循環 App::initializers 的值,並使用容器類的 make 方法獲取每一個 $initializer 的實例,而後調用實例對應的 init 方法。App::initializers 成員變量的值爲:composer

1 protected $initializers = [
2     Error::class,
3     RegisterService::class,
4     BootService::class,
5 ];

 

這裏重點關注後面兩個:服務註冊和服務初始化。函數

 

服務註冊

執行 $this->make($initializer)->init($this)$initializer 等於 RegisterService::class 時,調用該類中的 init 方法,該方法代碼以下:post

 1 public function init(App $app)
 2 {
 3     // 加載擴展包的服務
 4     $file = $app->getRootPath() . 'vendor/services.php';
 5 
 6     $services = $this->services;
 7 
 8     //合併,獲得全部須要註冊的服務
 9     if (is_file($file)) {
10         $services = array_merge($services, include $file);
11     }
12     // 逐個註冊服務
13     foreach ($services as $service) {
14         if (class_exists($service)) {
15             $app->register($service);
16         }
17     }
18 }

 

服務註冊類中,定義了系統內置服務的值:性能

1 protected $services = [
2     PaginatorService::class,
3     ValidateService::class,
4     ModelService::class,
5 ];

 

這三個服務和擴展包定義的服務將逐一被註冊,其註冊的方法 register 代碼以下:ui

 1 public function register($service, bool $force = false)
 2 {
 3     // 好比 think\service\PaginatorService
 4     // getService方法判斷服務的實例是否存在於App::$services成員變量中
 5     // 若是是則直接返回該實例
 6     $registered = $this->getService($service);
 7     // 若是服務已註冊且不強制從新註冊,直接返回服務實例
 8     if ($registered && !$force) {
 9         return $registered;
10     }
11     // 實例化該服務
12     // 好比 think\service\PaginatorService,
13     // 該類沒有構造函數,其父類Service類有構造函數,須要傳入一個App類的實例
14     // 因此這裏傳入$this(App類的實例)進行實例化
15     if (is_string($service)) {
16         $service = new $service($this);
17     }
18     // 若是存在「register」方法,則調用之
19     if (method_exists($service, 'register')) {
20         $service->register();
21     }
22     // 若是存在「bind」屬性,添加容器標識綁定
23     if (property_exists($service, 'bind')) {
24         $this->bind($service->bind);
25     }
26     // 保存服務實例
27     $this->services[] = $service;
28 }

 

詳細分析見代碼註釋。若是服務類定義了 register 方法,在服務註冊的時候會被執行,該方法一般是用於將服務綁定到容器;此外,也能夠經過定義 bind 屬性的值來將服務綁定到容器。this

服務逐個註冊以後,獲得 App::services 的值大概是這樣的:

 

 

 


 

每一個服務的實例都包含一個 App 類的實例。

 

服務初始化

執行 $this->make($initializer)->init($this)$initializer 等於 BootService::class 時,調用該類中的 init 方法,該方法代碼以下:

 1 public function init(App $app)
 2 {
 3     $app->boot();
 4 }
 5 其實是執行 App::boot():
 6 
 7 public function boot(): void
 8 {
 9     array_walk($this->services, function ($service) {
10         $this->bootService($service);
11     });
12 }

 

這裏是將每一個服務實例傳入 bootService 方法中。重點關注 bootService 方法:

1 public function bootService($service)
2 {
3     if (method_exists($service, 'boot')) {
4         return $this->invoke([$service, 'boot']);
5     }
6 }

 

這裏調用服務實例對應的 boot 方法。接下來,咱們以 ModelService 的 boot 方法爲例,看看 boot 方法大概能夠作哪些工做。ModelService 的 boot 方法代碼以下:

 1 public function boot()
 2 {
 3     // 設置Db對象
 4     Model::setDb($this->app->db);
 5     // 設置Event對象
 6     Model::setEvent($this->app->event);
 7     // 設置容器對象的依賴注入方法
 8     Model::setInvoker([$this->app, 'invoke']);
 9     // 保存閉包到Model::maker
10     Model::maker(function (Model $model) {
11         //保存db對象
12         $db     = $this->app->db;
13         //保存$config對象
14         $config = $this->app->config;
15         // 是否須要自動寫入時間戳 若是設置爲字符串 則表示時間字段的類型
16         $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
17 
18         if (is_null($isAutoWriteTimestamp)) {
19             // 自動寫入時間戳 (從配置文件獲取)
20             $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp'));
21         }
22         // 時間字段顯示格式
23         $dateFormat = $model->getDateFormat();
24 
25         if (is_null($dateFormat)) {
26             // 設置時間戳格式 (從配置文件獲取)
27             $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s'));
28         }
29 
30     });
31 }

 

能夠看出,這裏都是對 Model 類的靜態成員進行初始化。這些靜態成員變量的訪問屬性爲 protected,因此,能夠在 Model 類的子類中使用這些值。

 

自定義系統服務

接着,咱們本身動手來寫一個簡單的系統服務。

  • 定義被服務的對象(類)

    建立一個文件:app\common\MyServiceDemo.php,寫入代碼以下:

     1 <?php
     2 namespace app\common;
     3 class MyServiceDemo
     4 {
     5     //定義一個靜態成員變量
     6     protected static $myStaticVar = '123';
     7     // 設置該變量的值
     8     public static function setVar($value){
     9         self::$myStaticVar = $value;
    10     }
    11     //用於顯示該變量
    12     public function showVar()
    13     {
    14         var_dump(self::$myStaticVar);
    15     }
    16 }

     

  • 定義服務提供者

    在項目根目錄,命令行執行 php think make:service MyService,將會生成一個 app\service\MyService.php 文件,在其中寫入代碼:

     1 <?php
     2 namespace app\service;
     3 use think\Service;
     4 use app\common\MyServiceDemo;
     5 class MyService  extends Service
     6 {
     7     // 系統服務註冊的時候,執行register方法
     8     public function register()
     9     {
    10         // 將綁定標識到對應的類
    11         $this->app->bind('my_service', MyServiceDemo::class);
    12     }
    13     // 系統服務註冊以後,執行boot方法
    14     public function boot()
    15     {
    16         // 將被服務類的一個靜態成員設置爲另外一個值
    17         MyServiceDemo::setVar('456');
    18     }
    19 }

     

  • 配置系統服務

    在 app\service.php 文件(若是沒有該文件則建立之),寫入:

    1 <?php
    2     return [
    3         '\app\service\MyService'
    4     ];

     

  • 在控制器中調用
    建立一個控制器文件 app\controller\Demo.php,寫入代碼:

     1 <?php
     2 namespace app\controller;
     3 use app\BaseController;
     4 use app\common\MyServiceDemo;
     5 class Demo extends BaseController
     6 {
     7     public function testService(MyServiceDemo $demo){
     8         // 由於在服務提供類app\service\MyService的boot方法中設置了$myStaticVar=‘456’\
     9         // 因此這裏輸出'456'
    10         $demo->showVar();
    11     }
    12 
    13     public function testServiceDi(){
    14         // 由於在服務提供類的register方法已經綁定了類標識到被服務類的映射
    15         // 因此這裏能夠使用容器類的實例來訪問該標識,從而獲取被服務類的實例
    16         // 這裏也輸出‘456’
    17         $this->app->my_service->showVar();
    18     }
    19 }

     

    執行原理和分析見代碼註釋。另外說說自定義的服務配置是怎麼加載的:App::initialize() 中調用了 App::load() 方法,該方法結尾有這麼一段:

    1 if (is_file($appPath . 'service.php')) {
    2     $services = include $appPath . 'service.php';
    3     foreach ($services as $service) {
    4         $this->register($service);
    5     }
    6 }

     

    正是在這裏將咱們自定義的服務加載進來而且註冊。

 

在 Composer 擴展包中使用服務

這裏以 think-captcha 擴展包爲例,該擴展使用了系統服務,其中,服務提供者爲 think\captcha\CaptchaService 類,被服務的類爲 think\captcha\Captcha

首先,項目根目錄先運行 composer require topthink/think-captcha 安裝擴展包;安裝完成後,咱們查看 vendor\services.php 文件,發現新增一行:

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

 

這是怎麼作到的呢?這是由於在 vendor\topthink\think-captcha\composer.json 文件配置了:

 1 "extra": {
 2     "think": {
 3         "services": [
 4             "think\\captcha\\CaptchaService"
 5         ]
 6     }
 7 },
 8 而在項目根目錄下的 composer.json,有這樣的配置:
 9 
10 "scripts": {
11     "post-autoload-dump": [
12         "@php think service:discover",
13         "@php think vendor:publish"
14     ]
15 }

 

擴展包安裝後,會執行這裏的腳本,其中,跟這裏的添加系統服務配置相關的是:php think service:discover。該指令執行的代碼在 vendor\topthink\framework\src\think\console\command\ServiceDiscover.php,相關的代碼以下:

 1 foreach ($packages as $package) {
 2     if (!empty($package['extra']['think']['services'])) {
 3         $services = array_merge($services, (array) $package['extra']['think']['services']);
 4     }
 5 }
 6 
 7 $header = '// This file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL . 'declare (strict_types = 1);' . PHP_EOL;
 8 
 9 $content = '<?php ' . PHP_EOL . $header . "return " . var_export($services, true) . ';';
10 
11 file_put_contents($this->app->getRootPath() . 'vendor/services.php', $content);

 

能夠看出,擴展包若是有配置 ['extra']['think']['services'],也就是系統服務配置,都會被寫入到 vendor\services.php 文件,最終,全部服務在系統初始化的時候被加載、註冊和初始化。

分析完了擴展包中服務配置的實現和原理,接着咱們看看 CaptchaService 服務提供類作了哪些初始化工做。該類只有一個 boot 方法,其代碼以下:

 1 public function boot(Route $route)
 2 {
 3     // 配置路由
 4     $route->get('captcha/[:config]', "\\think\\captcha\\CaptchaController@index");
 5     // 添加一個驗證器
 6     Validate::maker(function ($validate) {
 7         $validate->extend('captcha', function ($value) {
 8             return captcha_check($value);
 9         }, ':attribute錯誤!');
10     });
11 }

 

有了以上的先行配置,咱們就能夠愉快地使用 Captcha 類了。

 

總結

使用系統服務有大大的好處和避免了直接修改類的壞處。從以上分析來看,我的以爲,使用系統服務,能夠對一個類進行非入侵式的「配置」,若是哪天一個類的某些設定須要修改,咱們不用直接修改這個類,只須要修改服務提供類就行了。對於擴展包來講,系統服務使其能夠在擴展中靈活配置程序,達到開箱即用的效果。不過,有個缺點是系統服務類都要在程序初始化是進行實例化,若是一個系統的服務類不少,勢必影響程序的性能。

相關文章
相關標籤/搜索