什麼是系統服務?系統服務是對於程序要用到的類在使用前先進行類的標識的綁定,以便容器可以對其進行解析(經過服務類的 register
方法),還有就是初始化一些參數、註冊路由等(不限於這些操做,主要是看一個類在使用以前的須要,進行一些配置,使用的是服務類的 boot
方法)。如下面要介紹到的 ModelService
爲例,ModelService
類提供服務,ModelService
類主要對 Model
類的一些成員變量進行初始化(在 boot
方法中),爲後面 Model
類的「出場」佈置好「舞臺」。php
下面先來看看系統自帶的服務,看看服務是怎麼實現的。json
系統內置的服務有:ModelService
、PaginatorService
和 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 }
正是在這裏將咱們自定義的服務加載進來而且註冊。
這裏以 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
類了。
使用系統服務有大大的好處和避免了直接修改類的壞處。從以上分析來看,我的以爲,使用系統服務,能夠對一個類進行非入侵式的「配置」,若是哪天一個類的某些設定須要修改,咱們不用直接修改這個類,只須要修改服務提供類就行了。對於擴展包來講,系統服務使其能夠在擴展中靈活配置程序,達到開箱即用的效果。不過,有個缺點是系統服務類都要在程序初始化是進行實例化,若是一個系統的服務類不少,勢必影響程序的性能。