明白流程不代表精通Laravel。因為還需要把這個流程與實際開發需要融匯貫通:學以致用。比如,如何修改Laravel日誌存放目錄?Laravel並沒有自定義日誌目錄的配置,需要修改原始碼,這就需要了解Laravel的生命流程,明白Laravel是在哪裡,什麼時候進行日誌目錄設定。
一、 整體流程
繫結的異常處理:App\Exceptions\Handler::class 可以在這裡自定義
二、 Application初始化流程
2.1 App初始化解釋
由此看來,Applicantion的初始化並沒有很複雜
- 初始化容器
- Events/Log/Router註冊基礎服務
- 設定容器中abstract與alias的對映關係
Events :單例繫結 events => Illuminate\Events\Dispatcher
- 並且設定了`setQueueResolver` Resolver 一般是一個callable型別,用於向容器獲取某個物件。比如這裡的是一個佇列
Log : 單例繫結 log => \Illuminate\Log\Writer
public function createLogger()
{
$log =
// \Monolog\Logger
new Monolog($this->channel()),
// \Illuminate\Contracts\Events\Dispatcher 上面Events的繫結
$this->app['events']
if ($this->app->hasMonologConfigurator()) {
call_user_func($this->app->getMonologConfigurator(), $log->getMonolog());
} else {
// 預設會走這裡
$this->configureHandler($log);
}
return $log;
}
// channel 只是得到當前的環境
protected function channel()
{
return $this->app->bound('env') ? $this->app->environment() : 'production';
}
預設會走configureHandler
方法:
protected function configureHandler(Writer $log)
{
$this->{'configure'.ucfirst($this->handler()).'Handler'}($log);
}
// 從配置中得到 是single/daily/Syslog/Errorlog
protected function handler()
{
if ($this->app->bound('config')) {
return $this->app->make('config')->get('app.log', 'single');
}
return 'single';
}
// single 呼叫: $log->useFiles
// daily 呼叫:$log->useDailyFiles
// Syslog 呼叫:$log->useSyslog
// Errorlog 呼叫:$log->useErrorLog
這裡是要從配置上獲取啊,關鍵是現在還沒有載入配置!!這是什麼情況?
$this->app->singleton('log', function () {
return $this->createLogger();
});
雖然是繫結,但只是繫結一個Resolver
,一個閉包,並不是馬上例項化Logger的。所以在載入配置前,將會使用預設值形式記錄日誌的。
意思是設定monolog 該把日誌寫在哪裡,怎麼寫,比如 daily。
protected function configureDailyHandler(Writer $log)
{
$log->useDailyFiles(
// 固定storage目錄和檔名 可以在這修改
$this->app->storagePath().'/logs/laravel.log', $this->maxFiles(),
$this->logLevel()
);
}
如果想要自定義處理,則:
// 設定一個回撥即可
$app->configureMonologUsing('myLogCallback');
function myLogCallback($log)
{
$log->useDailyFiles(
'/tmp/logs/laravel.log',
5, // 最多幾個日誌檔案,可以寫成配置
'debug' // 日誌等級,可以寫成配置
);
}
//關鍵是在哪裡呼叫這個設定比較好?
如果只是修改日誌路徑,我建議修改對應handler的路徑即可,比如修改configureDailyHandler
的路徑修改一下。甚至可以在服務提供者下新增一個自定義的handler。我認為Laravel應該把這個Log服務提供者做成Kernel這樣的繼承。
比如:
App 的 registerBaseServiceProviders 中:
$this->register(new \Illuminate\Log\LogServiceProvider($this));
改成:
$this->register(new \App\Providers\LogServiceProvider($this));
然後再app/Providers目錄下新增一個LogServiceProvider extens \Illuminate\Log\LogServiceProvider
。 這樣我們就可以重寫或者新增日誌的configuredHandler日誌配置handler
了.
Illuminate\Routing\RoutingServiceProvider
的註冊 register
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
/*
繫結 :router => Illuminate\Routing\Router
Router 定義了 get post put
*/
$this->registerRouter();
/*
繫結: url => Illuminate\Routing\UrlGenerator
繫結: routes => \Illuminate\Routing\RouteCollection
回撥: session :setSessionResolver
回撥: 上述繫結的 rebind
*/
$this->registerUrlGenerator();
/*
繫結:redirect => Illuminate\Routing\Redirector
*/
$this->registerRedirector();
$this->registerPsrRequest();
$this->registerPsrResponse();
$this->registerResponseFactory();
}
暫時可以把Kernel看作一個黑匣子:把請求往裡放,響應返回出來。
registerRouter 註冊了 Router路由
Facade 中的Route實際上呼叫的就是 Illuminate\Routing\Router 例項的方法。Router還有魔術方法__call 類似
as
,domain
,middleware
,name
,namespace
,prefix
這些Router沒有的方法,會從新建立一個新的Illuminate\Routin\RouteRegistrar
例項呼叫其attribute
方法 (Registrar是登記的意思)
這些都回在RouteServiceProvider
上呼叫:
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web') // 這裡就建立了RouteRegistrar 並且返回自身
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
最終 RouteRegistrar 還是對router物件進行操作。
疑問:為什麼不直接操作router,而走這麼複雜的路?
主要是給RouteServiceProvider 的mapWebRoutes
和 mapApiRoutes
方法使用。作用是區分並且儲存 路由設定的屬性。因為每次呼叫都會重新建立一個RouteRegistrar
例項,因此 其attributes都是不共享的,起到Web與Api理由設定的屬性不會相互影響。
要注意:服務提供者註冊的時候沒有進行依賴注入,也不需要使用依賴注入;因為例項化的時候就把app
作為引數傳遞進去,如果需要直接使用app
獲取依賴的物件即可
三、Http Kernal 的流程
看程式碼:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
上面看,Kernel大體做了4件事:
- 初始化Kernel
- 捕捉請求與handle請求
- 傳送請求
- terminate
3.1 Kernel初始化
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
// 設定中介軟體優先順序列表
$router->middlewarePriority = $this->middlewarePriority;
// 中介軟體組 web 和 api 的中介軟體組
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
// 路由中介軟體
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
Kernel初始化做了3件事,總體是一件事(設定中介軟體):
- 給路由設定中介軟體固定優先順序列表
- 設定中介軟體分組(Web與Api的分組)
- 設定路由中介軟體(在路由中設定的中介軟體)
後兩步的中介軟體屬性都是空的,具體的值在App\Http\Kernel
中
在設定路由中介軟體中aliasMiddleware
顧名思義,就是給中介軟體設定一個別名而已。
3.2 Kernel處理請求
捕獲請求後面再說
這裡是核心部分了:路由排程,中介軟體棧(閉包),自定義異常處理。在此之前都沒有進行過一次的依賴注入
。因為還沒有在容器當中。
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
// 把請求透過以下路由,就變成了響應
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
event(new Events\RequestHandled($request, $response));
return $response;
}
注意:Throwable
能夠捕捉 Exception 和 Error。這是php7 的特徵。
enableHttpMethodParameterOverride
做什麼?方法欺騙!
/**
* Enables support for the _method request parameter to determine the intended HTTP method.
*
* Be warned that enabling this feature might lead to CSRF issues in your code.
* Check that you are using CSRF tokens when required.
* If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
* and used to send a "PUT" or "DELETE" request via the _method request parameter.
* If these methods are not protected against CSRF, this presents a possible vulnerability.
*
* The HTTP method can only be overridden when the real HTTP method is POST.
*/
public static function enableHttpMethodParameterOverride()
{
self::$httpMethodParameterOverride = true;
}
主要看註釋。大概意思:
為了能夠透過_method引數進行方法欺騙.被警告,啟用這個特性可能會導致你的程式碼中的CSRF問題。檢查您是否在需要時使用CSRF令牌。如果啟用HTTP方法引數覆蓋,則可以更改具有方法“POST”的html表單,並透過_method請求引數傳送“PUT”或“DELETE”請求。如果這些方法不受CSRF保護,就會出現一個可能的漏洞。方法前片只有在真正的HTTP方法是POST的時候才會被修改
sendRequestThroughRouter做的幾件事
- 繫結request
- 啟動App容器(重要)
- 載入.env環境配置
- 載入config目錄配置
- 設定錯誤和異常的handler
- 設定Facade別名自動載入
- 註冊服務提供者
- 啟動服務提供者 (這個時候才開始可以使用依賴注入)
- 把請求透過中介軟體和路由
- 使用管道(閉包棧)
- 路由排程
protected function sendRequestThroughRouter($request)
{
// 透過路由的時候才繫結請求到容器中
$this->app->instance('request', $request);
// 清除Facade的$resolvedInstance屬性儲存的物件。
// 當使用Facade的時候如果不存在則從app容器獲取物件
Facade::clearResolvedInstance('request');
// 啟動App 其實是呼叫一些類
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
啟動App 其實是呼叫一些類
App 下的
public function bootstrapWith(array $bootstrappers)
{
// 修改為已經啟動
$this->hasBeenBootstrapped = true;
// 啟動類 觸發事件
foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
// 呼叫這些類的bootstrap方法
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
看一下App啟動流程有哪些?
protected $bootstrappers = [
// 載入 .env環境變數
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
// 載入config目錄的配置
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
// 註冊配置的服務提供者
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
// 啟動服務提供者 即呼叫 boot方法
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
注意:環境配置與config配置的載入之前已經說過了,這裡就不說了:
這裡簡單總結一下上面兩個連結:
- 不能在config目錄外使用env輔助函式
- 不能在config目錄內定義配置以外的內容,比如定義類
為什麼有這樣的要求?因為如果使用 php artisan config:cache 把配置快取後,env函式將不再去載入.env檔案,config函式也不會載入config目錄(目錄中的類也就不能載入了)
異常錯誤handler
Illuminate\Foundation\Bootstrap\HandleExceptions
的bootstrap
方法:
public function bootstrap(Application $app)
{
$this->app = $app;
// 設定報所有的錯 -1 的補碼全都是1
error_reporting(-1);
// 設定系統錯誤地handler:warning notice等
set_error_handler([$this, 'handleError']);
// 設定異常處理
set_exception_handler([$this, 'handleException']);
// 設定php結束時的處理函式
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}
其實就是設定一些錯誤和異常處理回撥,最終的handler是之前說的App\Exceptions\Handler::class
類。
我們看看:
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
只是把一些可以捕獲的PHP Error轉為Exception,然後再由ExceptionHandler處理。
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
// 報告錯誤,比如寫入錯誤日誌
$this->getExceptionHandler()->report($e);
// 根據不同的執行環境輸出不一樣的錯誤形式
if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
}
好了,看到這裡大家明白了自定義異常處理是在:`App\Exceptions\Handler::class
處理的.
Facade的註冊:
這個是需要在app.aliases
上進行配置的。
呼叫了一個AliasLoader
類:顧名思義,就是一個類別名自動載入者;就是為這些Facade設定自動載入。
/**
* AliasLoader類
* Prepend the load method to the auto-loader stack.
*
* @return void
*/
protected function prependToLoaderStack()
{
spl_autoload_register([$this, 'load'], true, true);
}
bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )
Facade效能: Laravel把Facade的載入放在最前面。因此使用Facade是可以提高自動載入速度的。但是如果在app.aliases加了一個不是Facade的一個別名,這將會降低效能(因為會去寫檔案,建立一個Facade)
看看Facade是怎麼載入的:
public function load($alias)
{
// 如果沒有Facade的名稱空間,說明不是Facade
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
// 使用Facade的模板自動建立一個與別名一樣的Facade,並且載入它:ensureFacadeExists
$this->loadFacade($alias);
// 返回 true 告訴PHP不用再去載入,已經載入了
return true;
}
// 有Facade這個名稱空間: Facades\\
if (isset($this->aliases[$alias])) {
// 設定別名,並且自動載入
return class_alias($this->aliases[$alias], $alias);
}
}
// PHP class_alias原生函式
bool class_alias ( string $original , string $alias [, bool $autoload = TRUE ] )
register服務提供者
public function registerConfiguredProviders()
{
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($this->config['app.providers']);
}
服務提供者的緩載介紹:服務提供者緩載
這裡簡單說:
- 首先使用
$this->getCachedServicesPath()
得到服務提供者快取起來的檔案 - 是否有快取或者配置是否變化(變化就更新)
- 使用配置的進行判斷哪些是緩載(需要時才載入)
主要看Illuminate\Foundation\ProviderRepository
的load
方法:
public function load(array $providers)
{
// 載入快取的services.php
$manifest = $this->loadManifest();
// 是否需要從新生成快取檔案
if ($this->shouldRecompile($manifest, $providers)) {
$manifest = $this->compileManifest($providers);
}
// 對於 when形式的服務提供者 在某事件觸發才註冊
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}
// 這些是馬上就註冊的
foreach ($manifest['eager'] as $provider) {
$this->app->register($provider);
}
// 這個是緩載的。只是新增到App的deferredServices屬性
$this->app->addDeferredServices($manifest['deferred']);
}
// 新增事件監聽
protected function registerLoadEvents($provider, array $events)
{
if (count($events) < 1) {
return;
}
$this->app->make('events')->listen($events, function () use ($provider) {
$this->app->register($provider);
});
}
boot 啟動服務提供者
這個時候會使用App 的 call方法來呼叫所有註冊的服務提供者。為什麼使用
call
方法,而不是直接呼叫?因為使用依賴注入。要知道這個call
方法是public的,我們可以在其他地方也可以顯示使用
依賴注入
/**
* Call the given Closure / class@method and inject its dependencies.
*
* @param callable|string $callback
* @param array $parameters
* @param string|null $defaultMethod
* @return mixed
*/
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
Illuminate\Container\BoundMethod
就是依賴注入實現的類了(需要傳遞App容器進去才行)
依賴注入流程
從
Illuminate\Container\BoundMethod
的call
方法開始graph TD A[A BoundMethod::call] --> B B{B callback 是字串型} -->|Yes|C B --> |No|E C[C 轉為 array 型callable] --> E E[E callBoundMethod] --> F F{F callback 是 array 型} --> |No|G F --> |Yes|H G[G 直接呼叫default並且返回] --> Z H{H callback 是 app的bindingMethod} --> |Yes|I I[I 直接呼叫app的bindingethod] --> Z H --> |No|J J[J 直接呼叫default並且返回] --> Z Z[Z 返回call結果]
註釋:
- B中字串型:是字串;且有‘@’字元或者有預設方法。
- F判斷是否為array型。因為callback仍可能是字串,且沒有滿足B中條件,這就直接到G。為什麼?
比如你要對某個函式進行依賴注入
G情況的虛擬碼:function test(Request $request) { ... } $app->call('test');
- G中直接呼叫或者返回$default,是?
return $default instanceof Closure ? $default() : $default;
- default 個是實現依賴注入的關鍵!!!下面是 這個閉包的程式碼:
function () use ($container, $callback, $parameters) { return call_user_func_array( $callback, static::getMethodDependencies($container, $callback, $parameters) ); }
看出,進行判斷$callback 需要哪些依賴和引數的方法是:
getMethodDependencies
// 得到方法或者函式的引數列表 包括依賴注入的和$parameters本身傳入的 protected static function getMethodDependencies($container, $callback, array $parameters = []) { $dependencies = [];
foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
}
return array_merge($dependencies, $parameters);
}
// 透過反射得到方法或者函式定義的引數列表
protected static function getCallReflector($callback)
{
if (is_string($callback) && strpos($callback, '::') !== false) {
$callback = explode('::', $callback);
}
return is_array($callback)
? new ReflectionMethod($callback[0], $callback[1])
: new ReflectionFunction($callback); // 這裡相容了函式的依賴注入
}
// 計算方法或者函式的 依賴引數 or $parameters or 本身的預設值
protected static function addDependencyForCallParameter($container, $parameter,
array &$parameters, &$dependencies)
{
// 如果 定義的引數列表順序 指定 手動引數
放在自動引數
前面。
// $parameters 是關聯陣列的時候才有用
if (array_key_exists($parameter->name, $parameters)) {
$dependencies[] = $parameters[$parameter->name];
unset($parameters[$parameter->name]);
} elseif ($parameter->getClass()) {
// 類的依賴注入
$dependencies[] = $container->make($parameter->getClass()->name);
} elseif ($parameter->isDefaultValueAvailable()) {
// 有預設值
$dependencies[] = $parameter->getDefaultValue();
}
}
可以使用依賴注入的地方:
* 服務提供者 boot方法
* 路由中指定的控制器的處理方法和控制器的建構函式
* 凡事呼叫App的 call進行呼叫的方法或函式都可以使用依賴注入
**注意:**
* `需要注入或有預設值的引數` 這裡統一稱為`自動引數`;其他為`手動引數`,就是需要顯示傳遞的引數
* 依賴注入不僅僅是把依賴注入到類方法中,還可以注入到函式中
* 最好把`自動引數`放在前面,`手動引數`放後面
* 如果無需注入且無預設值的引數定義在 需要注入或有預設值的前面,那麼$parameters需要為關聯陣列(並且需要與接下來的`手動引數`順序保持一致)
- 原因是:`call_user_func_array` 呼叫的時候引數列表只是按照順序來傳遞
```php
$array = [
'param2' => '1111',
'param1' => '2222'
];
function test($param1, $param2){
var_dump([$param1, $param2]);
}
call_user_func_array('test', $array);
// 輸出:只是按照順序傳遞
array(2) {
[0] =>
string(4) "1111"
[1] =>
string(4) "2222"
}
按照 官網文件 上說第二個的$param_arr
需要時索引陣列。但是上面的例子使用了一個關聯陣列也是可以正常執行的;難道是低版本才會有這種要求?這個留給讀者去研究了!
3.3 管道
Laravel有管道?什麼鬼?怎麼還不說
中介軟體
和路由
?Laravel的中介軟體
和路由
是透過管道傳輸的,在傳輸的過程中就把請求轉化為響應。
// 管道就只有四個方法
interface Pipeline
{
/**
* Set the traveler object being sent on the pipeline.
* 把需要加工的東西放入管道
*
* @param mixed $traveler
* @return $this
*/
public function send($traveler);
/**
* Set the stops of the pipeline.
* 管道的停留點。就管道流程的加工點
*
* @param dynamic|array $stops
* @return $this
*/
public function through($stops);
/**
* Set the method to call on the stops.
* 設定停留點呼叫的方法名,比如中介軟體的handle
*
* @param string $method
* @return $this
*/
public function via($method);
/**
* Run the pipeline with a final destination callback.
* 執行管道(重點),並且把結果送到目的地
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination);
}
管道實現中介軟體棧
為什麼叫中介軟體棧?因為這與棧的思想是一致的。請求最先進入的中介軟體,最後才會出去:先進後出。則麼做到的?使用閉包一層一層地包裹著控制器!!先把middleware 3
包裹 控制器
,然後middleware 2
再對其進行包裝一層,最後包上最後一層middlware 1
。
看一下管道是怎麼使用的:
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
// Pipeline 下:
public function then(Closure $destination)
{
// array_reduce 是PHP原生的函式,用於陣列迭代
$pipeline = array_reduce(
// 這裡對pipes進行反轉 是需要從最後的 middleware 3 開始包裹
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
// 執行得到的閉包棧
return $pipeline($this->passable);
}
// PHP 版本的實現
function array_reduce($array, $callback, $initial=null)
{
$acc = $initial;
foreach($array as $a)
$acc = $callback($acc, $a);
return $acc;
}
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
protected function carry()
{
// 返回一個處理 $pipes的callback;$stack=上次迭代完成的中介軟體棧; $pipe 本次迭代的中介軟體;
// $passable每一層中介軟體都需要處理的$request請求
return function ($stack, $pipe) {
// 本次迭代 包裹完成的$stack 閉包棧。每一層閉包棧都只有一個 $passable 引數
return function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
// 如果管道是一個Closure的例項,我們將直接呼叫它,否則我們將把這些管道從容器中解出來,並用適當的方法和引數呼叫它,並返回結果。
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
// 字串形式可以 傳遞引數
list($name, $parameters) = $this->parsePipeString($pipe);
// 如果管道是一個字串,我們將解析字串並將該類從依賴注入容器中解析出來。 然後,我們可以構建一個可呼叫的函式,並執行所需引數中的管道函式。
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// 如果管道已經是一個物件,我們只需要呼叫一個物件,然後將它傳遞給管道。 沒有必要做任何額外的解析和格式化,因為我們提供的物件已經是一個完全例項化的物件。
$parameters = [$passable, $stack];
}
// 執行中介軟體 引數順序: $request請求, 下一層閉包棧,自定義引數
return $pipe->{$this->method}(...$parameters);
};
};
}
看了原始碼之後,其實管道並不是真的把中介軟體閉包一層一層地進行包裹的。而是把閉包連成一個連結串列!,把相鄰的中介軟體連線起來:
middleware 的 $next 引數其實就是 下一個中介軟體,可以選擇在什麼時候呼叫。比如在middleware
的handle
方法中:
前置操作... 可以進行許可權控制
$response = $next($request);
後置操作... 可以統一處理返回格式,比如返回jsonp格式
注意:上面使用透過管道經過的中介軟體是在Http Kernel上配置的必然經過的中介軟體。但是路由的中介軟體/控制器中介軟體呢?接下來就是路由排程的是了!!
3.4 路由排程
其實上面流程圖最後的節點是
控制器
,其實並不是,而是被路由中介軟體棧包裹
的控制器。這也是需要使用管道的
public function dispatchToRoute(Request $request)
{
// First we will find a route that matches this request. We will also set the
// route resolver on the request so middlewares assigned to the route will
// receive access to this route instance for checking of the parameters.
// 路由排程是這裡
$route = $this->findRoute($request);
// 給$request 設定$route例項
$request->setRouteResolver(function () use ($route) {
return $route;
});
// 事件配發 就是觸發某事件
$this->events->dispatch(new Events\RouteMatched($route, $request));
// 把請求透過管道(中介軟體是管道節點) 得到請求 這裡就不說了
$response = $this->runRouteWithinStack($route, $request);
// 這裡只是封裝以下請求 不是路由排程的內容
return $this->prepareResponse($request, $response);
}
- 載入路由
- 等等?什麼時候載入路由?這個在App初始化的時候就載入了。不過本文並沒有詳細說明路由是如何實現:比如路由組是怎麼實現?還是使用了棧的原理來儲存路由組共有的屬性!!
- 匹配路由
Illuminate\Routing\RouteCollection
:所有路由到儲存在這裡。
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
// First, we will see if we can find a matching route for this current request
// method. If we can, great, we can just return it so that it can be called
// by the consumer. Otherwise we will check for routes with another verb.
// 翻譯:首先,我們將看看我們是否可以找到這個當前請求方法的匹配路由。 如果我們可以的話,那麼我們可以把它歸還給消費者consumer。 否則,我們將檢查另一個verb的路線。
$route = $this->matchAgainstRoutes($routes, $request);
// 找到匹配路由
if (! is_null($route)) {
return $route->bind($request);
}
// If no route was found we will now check if a matching route is specified by
// another HTTP verb. If it is we will need to throw a MethodNotAllowed and
// inform the user agent of which HTTP verb it should use for this route.
// 翻譯:如果沒有找到路由,我們現在檢查一個匹配路由是否由另一個HTTP verb指定。 如果是這樣的話,我們需要丟擲一個MethodNotAllowed方法,並告知使用者代理它應該為這個路由使用哪個HTTP verb。意思是提示:方法不被允許的Http 405 錯誤
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
// Arr::first 返回提一個符合條件的陣列元素
return Arr::first($routes, function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
// Illuminate\Routing\Route 類的 matches
public function matches(Request $request, $includingMethod = true)
{
//
$this->compileRoute();
foreach ($this->getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
// 任意一個不符合 就不符合要求
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
public static function getValidators()
{
if (isset(static::$validators)) {
return static::$validators;
}
// To match the route, we will use a chain of responsibility pattern with the
// validator implementations. We will spin through each one making sure it
// passes and then we will know if the route as a whole matches request.
// 翻譯:為了匹配路線,我們將使用驗證器實現的責任模式鏈。 我們將透過每一個確保它透過,然後我們將知道是否整個路線符合要求。
return static::$validators = [
new UriValidator, new MethodValidator,
new SchemeValidator, new HostValidator,
];
}
注意:
- HTTP verb是指http 動詞,比如POST,GET等
- 這裡不詳細說明了,因為沒有什麼特別的
- 可能重要的一點就是:路由匹配只會使用第一條符合條件的路由。
4 請求輸出
Symfony\Component\HttpFoundation\Response
public function send()
{
// 設定 header 和 cookie
$this->sendHeaders();
// echo $this->content
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
// ob_end_flush()
static::closeOutputBuffers(0, true);
}
return $this;
}
public static function closeOutputBuffers($targetLevel, $flush)
{
// 獲取有效的緩衝區巢狀層數
$status = ob_get_status(true);
$level = count($status);
// PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3
$flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
// 一層一層進行清除
while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) {
if ($flush) {
ob_end_flush();
} else {
ob_end_clean();
}
}
}
5 kernel terminate
http Kernel的terminate終止。一般情況都是不需要做些什麼的,因為很少去設定終止回撥
- 先 終止中介軟體(這裡不會使用連結串列的形式了,直接遍歷每個中介軟體的
terminate
方法) - 後 終止app程式
public function terminate($request, $response)
{
// 逐個呼叫中介軟體的terminate 。沒有 $next 引數
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}
// App
public function terminate()
{
// 使用依賴注入形式 呼叫設定的回撥
foreach ($this->terminatingCallbacks as $terminating) {
$this->call($terminating);
}
}
本文還不是還完整:比如請求捕獲;路由如何呼叫控制器等等。之後有時間會繼續更新的!!
本作品採用《CC 協議》,轉載必須註明作者和本文連結