ThinkPHP6 核心分析:系統服務


什么是系統服務?系統服務是對於程序要用到的類在使用前先進行類的標識的綁定,以便容器能夠對其進行解析(通過服務類的 register 方法),還有就是初始化一些參數、注冊路由等(不限於這些操作,主要是看一個類在使用之前的需要,進行一些配置,使用的是服務類的 boot 方法)。以下面要介紹到的 ModelService 為例,ModelService類提供服務,ModelService 類主要對 Model 類的一些成員變量進行初始化(在 boot 方法中),為后面 Model 類的「出場」布置好「舞台」。

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

 

內置服務

系統內置的服務有:ModelServicePaginatorService 和 ValidateService 類,我們來看看它們是怎么被注冊和初始化的。

在 App::initialize() 有這么一段:

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

 

這里通過循環 App::initializers 的值,並使用容器類的 make 方法獲取每個 $initializer 的實例,然后調用實例對應的 init 方法。App::initializers 成員變量的值為:

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

 

這里重點關注后面兩個:服務注冊和服務初始化。

 

服務注冊

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

 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 代碼如下:

 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 屬性的值來將服務綁定到容器。

服務逐個注冊之后,得到 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 類了。

 

總結

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

  • 多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那里入手去提升,對此我整理了一些資料,包括但不限於:分布式架構、高可擴展、高性能、高並發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階干貨需要的可以免費分享給大家,需要的加群(點擊→)677079770


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM