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