Laravel框架源碼解析-請求生命周期

Laravel是我最熟悉的框架,卻一直沒認(rèn)真的梳理過它的源碼,甚是遺憾,遂在此梳理一遍laravel的源碼。

Laravel版本:5.7
Composer版本:1.6.5

請求生命周期

大部分框架都有一個(gè)入口文件,Laravel也不例外,入口文件即public/index.php,我們在Nginx中配置server的時(shí)候就是將index屬性指向到public/index.php的絕對路徑。
我們先從入口文件講起:

define('LARAVEL_START', microtime(true));

定義了框架運(yùn)行的開始時(shí)間,方便打印整個(gè)request請求的執(zhí)行時(shí)間,5.5版本之前是沒有的,這里是5.5新加的

require __DIR__.'/../vendor/autoload.php';

加載composer提供的類,進(jìn)入到vender/autoload_real.php文件中可以看到這個(gè)文件具體加載了什么東西


//此處開始按照psr-4規(guī)范設(shè)置加載器 //設(shè)置命名空間、md解析器、測試框架、console高亮顯示器等 
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}
//設(shè)置tinker、log、Symfony組件等(Laravel的路由、http核心等是基于Symfony框架的) 
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
    $loader->setPsr4($namespace, $path);
}
//設(shè)置映射類 
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
    $loader->addClassMap($classMap);
}

最后返回一個(gè)autoloader,這時(shí)Composer提供的東西全部加載完畢。
讓我們繼續(xù)返回到public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';

下一步就是開始初始化框架,可以看到在bootstrap/app.php引導(dǎo)文件中,第一行便是初始化一個(gè)Laravel容器,在初始化的過程中做了哪些工作呢?

這是Application.php的構(gòu)造器:

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    $this->registerBaseBindings();

    $this->registerBaseServiceProviders();

    $this->registerCoreContainerAliases();
}

首先設(shè)置整個(gè)項(xiàng)目的根目錄,并且將config_path,storage_path,database_path,resource_path,bootstrap_path等綁定到容器的instance屬性

1542619427025-1b1da1bf-bc65-43a8-8c5e-a496de30a492.png

下一步便是注冊基本綁定,注冊BaseServiceProviders

/**
 * Register all of the base service providers.  
 * @return void 
 */ 
protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}

可以看到容器初始化的時(shí)候已經(jīng)注冊了三個(gè)基本服務(wù):event,log,routing
再次回到Application.php,構(gòu)造器的最后一步是注冊alias,這時(shí)一個(gè)完整的app容器才算注冊完成。容器有了,接下來如何處理http請求和控制臺請求,如何處理異常呢?
答案是繼續(xù)往容器里綁定服務(wù):

//綁定http處理服務(wù)
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class );
//綁定從控制臺發(fā)起的請求處理服務(wù)
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class );
//綁定異常處理服務(wù)
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class );

至此我們可以看到,整個(gè)Laravel框架幾乎都是通過這樣的思想來注冊服務(wù)的,當(dāng)需要用哪個(gè)服務(wù)的時(shí)候,ok,綁定一下。如果要移除一個(gè)服務(wù),從容器實(shí)例中解除綁定即可。大大降低了系統(tǒng)的耦合,提高了靈活性。
再次回到public/index.php,
由于容器已經(jīng)綁定了將 Illuminate\Contracts\Http\Kernel::class,綁定為App\Http\Kernel::class,

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

當(dāng)容器傳入make一個(gè)http核心時(shí),實(shí)際是創(chuàng)建了一個(gè)類型為App\Http\Kernel::class的kernal對象。通過____construct發(fā)放可以看到處理http請求之前的中間件都已準(zhǔn)備完畢。

下一步開始創(chuàng)建request對象:

public static function capture()
{
    static::enableHttpMethodParameterOverride();

    return static::createFromBase(SymfonyRequest::createFromGlobals());
}

public static function createFromGlobals()
{
    $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES,   $_SERVER);

    if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
        && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
    ) {
        parse_str($request->getContent(), $data);
        $request->request = new ParameterBag($data);
    }

    return $request;
}

可以看到request請求是基于php的全局變量_GET, _POST,_COOKIE, _FILES,_SERVER 來創(chuàng)建的。
將$request對象傳入httpKernal之后,執(zhí)行handle方法,handle方法中需要先加載環(huán)境變量、配置文件等

protected $bootstrappers = [
    //從env文件加載環(huán)境變量
  \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    //從app/config目錄加載各種配置文件
  \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    //自定義異常處理函數(shù),php執(zhí)行終止函數(shù)
  \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    //注冊provider
  \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    //啟動provider
  \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

各種配置、中間件加載完畢后將request傳入Pipeline

return (new Pipeline($this->app))
            ->send($request)
            ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
            ->then($this->dispatchToRouter());

pipeline翻譯過來就是管道,request對象通過pipeline被傳送到middleware,經(jīng)過middleware的層層傳遞,進(jìn)入到then方法,這也是pipeline類的關(guān)鍵。

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

雖然只有短短的幾行代碼,理解起來卻特別晦澀。
我們一個(gè)一個(gè)解析
$this->pipes 經(jīng)過之前through方法的處理,其實(shí)就是middleware數(shù)組,將這個(gè)數(shù)組的順序反轉(zhuǎn)。(至于為什么要用array_reverse將中間件執(zhí)行的順序反過來,我也沒搞懂)

this->prepareDestination(destination)

/**
 * Get the final piece of the Closure onion. 
 * 
 * @param  \Closure  $destination 
 * @return \Closure 
 */ 
protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        try {
            return $destination($passable);
        } catch (Exception $e) {
            return $this->handleException($passable, $e);
        } catch (Throwable $e) {
            return $this->handleException($passable, new FatalThrowableError($e));
        }
    };
}

destination 本身就是一個(gè)閉包函數(shù),經(jīng)過prepareDestination方法的處理,返回值仍是一個(gè)閉包。經(jīng)過debug我們可以看到destination參數(shù)變?yōu)榱朔祷刂档囊粋€(gè)屬性,可以這么理解:上面的prepareDestination方法相當(dāng)于把 $destination參數(shù)格式化了一下,變?yōu)榉祷刂档囊粋€(gè)靜態(tài)類型的屬性。

1542619487183-009d3143-d51d-4cd9-93b1-171b706e3187.png

再次回到then方法,array_reduce函數(shù)在php手冊上是這樣解釋的:

mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
array_reduce() 將回調(diào)函數(shù) callback 迭代地作用到 array 數(shù)組中的每一個(gè)單元中,從而將數(shù)組簡化為單一的值。

array_reverse(this->pipes)是經(jīng)過反轉(zhuǎn)后的中間件數(shù)組,this->carry()返回的是一個(gè)用來處理中間件的數(shù)組,prepareDestination方法的返回值(假設(shè)為p)是array_reduce的可選參數(shù),p有兩個(gè)作用:

  1. 如果p不為空則在處理中間件數(shù)組之前執(zhí)行
  2. 如果中間件數(shù)組為空,則返回p

而我們知道,框架默認(rèn)會有5個(gè)中間件(見上圖pipes屬性),所以array_reverse的執(zhí)行結(jié)果一定是1,至此,Pipeline對象的then方法作用已明確:
先用prepareDestination方法格式化經(jīng)過路由分發(fā)后返回的閉包函數(shù),然后用 this->carry()方法挨個(gè)執(zhí)行中間件。
可以看下this->carry()的具體代碼:

protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {   // $pipe類型一
            
//如果$pipe是一個(gè)可以調(diào)用的閉包函數(shù),則直接調(diào)用 //否則將從容器中解析$pipe,然后用響應(yīng)的方法和參數(shù)調(diào)用
  
        return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {  // $pipe類型二

//如果$pipe不是對象類型,是字符串,則將此字符串從依賴注入容器中解析成class對象, //然后調(diào)用class的handle方法來處理參數(shù)(handle方法見下方注釋)
        [$name, $parameters] = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else {     // $pipe類型三

 //如果$pipe是對象類型,則直接將參數(shù)傳入$pipe調(diào)用它的handle方法即可  
        $parameters = [$passable, $stack];
            }

            //可以看到method屬性默認(rèn)值為 'handle',
 //$pipe如果存在handle方法則執(zhí)行handle //若不存在,說明$pipe是Closure類型,直接執(zhí)行 $pipe
  
        $response = method_exists($pipe, $this->method)
                            ? $pipe->{$this->method}(...$parameters)
                            : $pipe(...$parameters);
            //經(jīng)過中間件處理后的$response 如果是Responsable 類型,則按resopnse標(biāo)準(zhǔn)輸出,
 //若不是,則直接輸出 $response  
        return $response instanceof Responsable
                        ? $response->toResponse($this->container->make(Request::class))
                        : $response;
        };
    };
}

回顧一下Laravel框架中間件的實(shí)現(xiàn)方式:

  1. 通過運(yùn)行 make:middleware Artisan 命令來創(chuàng)建中間件。
    創(chuàng)建后的中間件在 app/Http/Middleware/ 目錄下,自帶handle方法,是由上面 elseif (! is_object($pipe)) 下的代碼塊來處理
  2. 創(chuàng)建中間件組
    中間件組也有兩種形式
protected  $middlewareGroups  =  [  
    'web'  =>  [ 
    \App\Http\Middleware\EncryptCookies::class, 
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,    
    \Illuminate\Session\Middleware\StartSession::class, 
    \Illuminate\View\Middleware\ShareErrorsFromSession::class, 
    \App\Http\Middleware\VerifyCsrfToken::class, 
    \Illuminate\Routing\Middleware\SubstituteBindings::class,  
    ],  
    'api'  =>  [  'throttle:60,1',  'auth:api',  ],  
];

api數(shù)組下的字符串類型對應(yīng) 上述 elseif (! is_object($pipe)) 下的代碼塊來處理
web數(shù)組下的class類型跟 1相同

  1. Terminable中間件
    這種中間件是用于HTTP 響應(yīng)構(gòu)建完畢之后處理一些工作,創(chuàng)建時(shí)直接在中間件class中添加 terminate 方法即可,這種中間件是由carry方法中的if (is_callable($pipe)) 代碼塊來處理

至此,this->carry()方法中正好將中間件的三種類型,都處理了。

繞了這么久終于回到public/index.php了,最后兩行:
發(fā)送response,發(fā)送完畢之后調(diào)用fastcgi_finish_request方法關(guān)閉fastcgi
kernal調(diào)用terminate方法結(jié)束請求聲明周期。

End

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容

  • 先說幾句廢話,調(diào)和氣氛。事情的起由來自客戶需求頻繁變更,偉大的師傅決定橫刀立馬的改革使用新的框架(created ...
    wsdadan閱讀 3,073評論 0 12
  • Laravel 學(xué)習(xí)交流 QQ 群:375462817 本文檔前言Laravel 文檔寫的很好,只是新手看起來會有...
    Leonzai閱讀 8,001評論 2 12
  • 原文鏈接 必備品 文檔:Documentation API:API Reference 視頻:Laracasts ...
    layjoy閱讀 8,619評論 0 121
  • Laravel框架一:原理機(jī)制篇 Laravel作為在國內(nèi)國外都頗為流行的PHP框架,風(fēng)格優(yōu)雅,其擁有自己的一些特...
    Mr_Z_Heng閱讀 3,722評論 0 13
  • 世間萬物皆有生命周期,當(dāng)我們使用任何工具時(shí)都需要理解它的工作原理,那么用起來就會得心應(yīng)手,應(yīng)用開發(fā)也是如此。理解了...
    伊Summer閱讀 12,538評論 6 36