runWithRequest () 方法
在Http
類別的run()
方法中,得到think\\Request
類別的實例後,程式接著執行$response = $this->runWithRequest(request);
。其中,runWithRequest()
方法前面幾行如下:
protected function runWithRequest(Request $request) { $this->initialize(); // 加载全局中间件 $this->loadMiddleware(); . . .
該方法第一行執行$this->initialize();
,對應用程式進行初始化,接下來詳細分析這項初始化操作。 Http
類別的initialize()
方法:
protected function initialize() { //如果还未初始化,则初始化之 if (!$this->app->initialized()) { $this->app->initialize(); } }
實際上是呼叫App
類別的initialize()
方法。這個方法程式碼:
public function initialize() { // 设置应用状态为已经初始化 $this->initialized = true; //记录开始时间 $this->beginTime = microtime(true); //记录起始内存使用量 $this->beginMem = memory_get_usage(); // ==( A )== 加载环境变量 // $this->env跟前面的(new App())->http和$this->config都是同样的套路 if (is_file($this->rootPath . '.env')) { $this->env->load($this->rootPath . '.env'); } //设置配置文件后缀 $this->configExt = $this->env->get('config_ext', '.php'); // ==( B )== 设置调试模式 $this->debugModeInit(); // ==( C )== 加载应用文件和配置等操作 $this->load(); // 加载框架默认语言包 $langSet = $this->lang->defaultLangSet(); // 框架目录下对应的语言包 // 比如:D:\dev\tp6\vendor\topthink\framework\src\lang\zh-cn.php $this->lang->load($this->thinkPath . 'lang' . DIRECTORY_SEPARATOR . $langSet . '.php'); // 加载应用默认语言包 // 这个会扫描「app/lang」里面,对应语言包文件夹的所有「.php」文件 // 比如,app/lang/zh-cn/* 下的所有文件 // 然后加载解析 $this->loadLangPack($langSet); // 监听AppInit $this->event->trigger('AppInit'); // 设置时区 date_default_timezone_set($this->config->get('app.default_timezone', 'Asia/Shanghai')); // ==( D )== 初始化 // 初始化错误和异常处理、注册系统服务和初始化系统服务 foreach ($this->initializers as $initializer) { $this->make($initializer)->init($this); } return $this; }
應用程式的初始化做了大量的操作,其主要的操作有:載入環境變數、載入設定文件,載入語言包、監聽 AppInit、initializers 陣列包含的類別的初始化。
(A) 載入環境變數
對應語句:$this->env->load($this->rootPath . '.env');
,其中,$this->env
,與前面的(new App())->http
原理是一樣的(參見第一篇),它可以取得\think\Env
類別的實例。取得 Env
類別實例後,呼叫 load()
方法,傳入的參數是.env
檔案所在位址。 load()
方法實作如下:
public function load(string $file): void { $env = parse_ini_file($file, true) ?: []; $this->set($env); }
該方法讀取.env
檔案的值後,呼叫set()
方法,將配置保存在Env
類別的$data
成員變數。 set()
方法碼:
public function set($env, $value = null): void { if (is_array($env)) { //全部KEY转为大写字母 $env = array_change_key_case($env, CASE_UPPER); foreach ($env as $key => $val) { //有二级配置的,转为KEY1_KEY2 => $v 的形式 if (is_array($val)) { foreach ($val as $k => $v) { $this->data[$key . '_' . strtoupper($k)] = $v; } } else { $this->data[$key] = $val; } } //ENV的值不是数组的情况 } else { $name = strtoupper(str_replace('.', '_', $env)); $this->data[$name] = $value; } }
從.env
讀取到的值大概是這樣的:
$this->set($env)
之後得到的大概是這樣的:
(B) 調試模式設定
運作原理詳見註解。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">protected function debugModeInit(): void
{
// 应用调试模式
if (!$this->appDebug) {
$this->appDebug = $this->env->get('app_debug') ? true : false;
// 关闭错误显示
ini_set('display_errors', 'Off');
}
// 如果不是命令行模式
if (!$this->runningInConsole()) {
// 重新申请一块比较大的buffer
// php缓冲控制
// 参考:https://www.php.net/manual/en/ref.outcontrol.php
// https://www.cnblogs.com/saw2012/archive/2013/01/30/2882451.html
// 新版PHP默认开启缓冲区ob_start(),ob_get_level() == 1
if (ob_get_level() > 0) {
// 相当于ob_get_contents() 和 ob_clean()
// 获取缓冲区内容并删除缓冲区内容
$output = ob_get_clean();
}
// 开启新的缓冲区控制
ob_start();
if (!empty($output)) {
// 由于开启了新的缓冲区控制,
// 这里不会直接输出$output
// 而是等到依次执行了ob_flush()和flash()之后才将内容输出到浏览器
echo $output;
}
}
}</pre><div class="contentsignin">登入後複製</div></div>
要注意的是,這裡似乎有個Bug,應該先執行$this->appDebug = $this->env->get('app\_debug') ? true : false;
取得是否為偵錯模式的配置,然後再判斷:
。
(C)載入應用程式檔案和設定等操作接下來執行
,「load」方法具體實作如下:
protected function load(): void { $appPath = $this->getAppPath(); // 加载「app」目录下的「common.php」文件 if (is_file($appPath . 'common.php')) { include_once $appPath . 'common.php'; } // 加载核心目录下的「helper.php」文件 // 可以看到,这里的加载顺序:先「common.php」,后「helper.php」 // 且「helper.php」中的函数包裹在「if (!function_exists('xxx'))」下 // 所以可以在「common.php」文件中覆盖系统定义的助手函数 include_once $this->thinkPath . 'helper.php'; $configPath = $this->getConfigPath(); $files = []; // glob的作用是扫描给定路径模式下的文件,非常好用 // 这里扫描「config」目录下的所有「.php」文件 if (is_dir($configPath)) { $files = glob($configPath . '*' . $this->configExt); } foreach ($files as $file) { // $this->config 还是同样的套路获得了「Config」类的实例 // 「load」的第二个参数为一级配置名,这里传入一个文件名,所有文件名作为一级配置 // 比如「app.php」配置文件,一级配置为「app」 // 在 「Config」类作用域下的操作: // 「load」加载文件后,通过「parse」方法解析文件内容 // 最后,通过「set」方法将所有配置合并了「config」成员变量 $this->config->load($file, pathinfo($file, PATHINFO_FILENAME)); } // 加载「app」目录下的「event.php」文件 if (is_file($appPath . 'event.php')) { $this->loadEvent(include $appPath . 'event.php'); } // 注册自定义的服务 if (is_file($appPath . 'service.php')) { $services = include $appPath . 'service.php'; foreach ($services as $service) { $this->register($service); } } }
值得一提的是,程式先載入「common.php」,後來再載入「helper.php」,而「helper.php」的函式包裹在「if (!function_exists ('xxx') )」下,所以我們如果有需要,可以在「common.php」檔案中覆寫系統定義的助手函數。 除了載入這兩個文件,該方法還掃描了「config」目錄下的所有設定文件,並將其載入到
Config
類別的
成員變量,載入了“app”目錄下的“event.php”文件,以及載入並註冊自訂的服務。
(D) 初始化錯誤和異常處理、註冊系統服務和初始化系統服務
接著,看初始化函數的最後一段:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">foreach ($this->initializers as $initializer) {
$this->make($initializer)->init($this);
}</pre><div class="contentsignin">登入後複製</div></div>
這幾行程式碼做了比較多的操作:分別實例化包含在裡面的類,然後呼叫其“init”方法。
陣列的值如下:
protected $initializers = [ Error::class, //错误处理类 RegisterService::class, //注册系统服务类 BootService::class, //启动系统服务 ];
略過系統錯誤處理類,先看註冊系統服務類別。值得注意的是,這個類別有一個成員變數:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">protected $services = [
PaginatorService::class,
ValidateService::class,
ModelService::class,
];</pre><div class="contentsignin">登入後複製</div></div>
包含了三個系統核心服務。在其
方法中,這些服務被註冊到系統服務,與前面的自訂服務合併起來,其主要實現代碼:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">foreach ($services as $service) {
if (class_exists($service)) {
// 注册到系统服务
$app->register($service);
}
}</pre><div class="contentsignin">登入後複製</div></div>
最後實例化的是啟動系統服務類,該類別的init
方法僅呼叫了App
類別的boot
方法,該方法的作用是初始化每個系統服務,也就是呼叫每個服務的
boot
啟動系統服務類別實作如下:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">class BootService
{
public function init(App $app)
{
$app->boot();
}
}</pre><div class="contentsignin">登入後複製</div></div>
App
類別的
方法:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">public function boot(): void
{
array_walk($this->services, function ($service) {
$this->bootService($service);
});
}</pre><div class="contentsignin">登入後複製</div></div>
其中關鍵是
方法:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">public function bootService($service)
{
if (method_exists($service, 'boot')) {
return $this->invoke([$service, 'boot']);
}
}</pre><div class="contentsignin">登入後複製</div></div>
此方法分別呼叫了每個服務的
boot
- 從以上程式碼可以看到,系統註冊的服務的來源有三個地方:
-
系統自帶的,如
PaginatorService
,ValidateService
, ModelService - ;
- app 目錄下,在「service.php」檔案中自訂的;