Laravel學習筆記-核心概念

Laravel核心概念

PHP的生命週期

萬物皆有生命,每個生命都有自己的生命週期。

Laravel是什麼?一個PHP框架。所以要想真正說清Laravel的生命週期,必須說清PHP的生命週期:

  • 模塊初始化階段 --- php_module_startup()
  • 請求初始化階段 --- php_request_startup()
  • 執行腳本階段 --- php_execute_script()
  • 請求關閉階段 --- php_request_shutdown()
  • 模塊關閉階段 --- php_module_shutdown()

Php有兩種運行模式:

  • WEB模式
  • CLI(命令行)模式。

當我們在終端敲入php這個命令的時候,使用的是CLI模式;當使用Nginx或者別web服務器作為宿主處理一個到來的請求時,會調用Php運行,此時使用的是WEB模式。

當我們請求一個Php文件時,比如Laravel 的public\index.php文件時,Php 為了完成這次請求,會發生5個階段的生命週期切換:

  1. 模塊初始化(MINIT),即調用php.ini中指明的擴展的初始化函數進行初始化工作,如mysql擴展。
  2. 請求初始化(RINIT),即初始化為執行本次腳本所需要的變量名稱和變量值內容的符號表,如$_SESSION變量。
  3. 執行該PHP腳本。
  4. 請求處理完成(Request Shutdown),按順序調用各個模塊的RSHUTDOWN方法,對每個變量調用unset函數,如unset $_SESSION變量。
  5. 關閉模塊(Module Shutdown) , PHP調用每個擴展的MSHUTDOWN方法,這是各個模塊最後一次釋放內存的機會。這意味著沒有下一個請求了。

WEB模式和CLI(命令行)模式很相似,區別是:

CLI 模式會在每次腳本執行經歷完整的5個週期,因為你腳本執行完不會有下一個請求;

WEB模式為了應對併發,可能採用多線程,因此生命週期1和5有可能只執行一次,下次請求到來時重複2-4的生命週期,這樣就節省了系統模塊初始化所帶來的開銷。

可以看到,Php生命週期是很對稱的。說了這麼多,就是為了定位Laravel運行在哪裡,沒錯,Laravel僅僅運行再第三個階段:

Laravel學習筆記-核心概念

image

知道這些有什麼用?你可以優化你的Laravel代碼,可以更加深入的瞭解Larave的singleton(單例)。至少你知道了,每一次請求結束,Php的變量都會unset,Laravel的singleton只是在某一次請求過程中的singleton;你在Laravel 中的靜態變量也不能在多個請求之間共享,因為每一次請求結束都會unset。理解這些概念,是寫高質量代碼的第一步,也是最關鍵的一步。因此記住,Php是一種腳本語言,所有的變量只會在這一次請求中生效,下次請求之時已被重置,而不像Java靜態變量擁有全局作用。

好了,開始Laravel的生命週期。

Laravel學習筆記-核心概念

laravel核心-生命週期.png

Laravel的生命週期

概述

Laravel 的生命週期從public\index.php開始,從public\index.php結束。

Laravel學習筆記-核心概念

image

注意:以下幾圖箭頭均代表Request流向

這麼說有點草率,但事實確實如此。下面是public\index.php的全部源碼(

Laravel源碼的註釋是最好的Laravel文檔),更具體來說可以分為四步:

Copy1. require __DIR__.'/../bootstrap/autoload.php';
2. $app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
3. $response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
4. $kernel->terminate($request, $response);

這四步詳細的解釋是:

1.註冊加載composer自動生成的class loader,包括所有你composer require的依賴(對應代碼1).

2.生成容器Container,Application實例,並向容器註冊核心組件(HttpKernel,ConsoleKernel,ExceptionHandler)(對應代碼2,容器很重要,後面詳細講解)。

3.處理請求,生成併發送響應(對應代碼3,毫不誇張的說,你99%的代碼都運行在這個小小的handle方法裡面)。

4.請求結束,進行回調(對應代碼4,還記得可終止中間件嗎?沒錯,就是在這裡回調的)。

Laravel學習筆記-核心概念

image

啟動Laravel基礎服務

我們不妨再詳細一點:

第一步註冊加載composer自動生成的class loader就是加載初始化第三方依賴,不屬於Laravel核心,到此為止。

第二步生成容器Container,並向容器註冊核心組件,這裡牽涉到了容器Container和合同Contracts,這是Laravel的重點,下面將詳細講解。

重點是第三步處理請求,生成併發送響應。

首先Laravel框架捕獲到用戶發到public\index.php的請求,生成Illuminate\Http\Request實例,傳遞給這個小小的handle方法。在方法內部,將該$request實例綁定到第二步生成的$app容器上。然後在該請求真正處理之前,調用bootstrap方法,進行必要的加載和註冊,如檢測環境,加載配置,註冊Facades(假象),註冊服務提供者,啟動服務提供者等等。這是一個啟動數組,具體在Illuminate\Foundation\Http\Kernel中,包括:

Copyprotected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];

看類名知意,Laravel是按順序遍歷執行註冊這些基礎服務的,注意順序:Facades先於ServiceProviders,Facades也是重點,後面說,這裡簡單提一下,註冊Facades就是註冊config\app.php中的aliases 數組,你使用的很多類,如Auth,Cache,DB等等都是Facades;而ServiceProviders的register方法永遠先於boot方法執行,以免產生boot方法依賴某個實例而該實例還未註冊的現象。

所以,你可以在ServiceProviders的register方法中使用任何Facades,在ServiceProviders的boot方法中使用任何register方法中註冊的實例或者Facades,這樣絕不會產生依賴某個類而未註冊的現象。

將請求傳遞給路由

注意到目前為止,Laravel 還沒有執行到你所寫的主要代碼(ServiceProviders中的除外),因為還沒有將請求傳遞給路由。

在Laravel基礎的服務啟動之後,就要把請求傳遞給路由了。傳遞給路由是通過Pipeline(另開篇章講解)來傳遞的,但是Pipeline有一堵牆,在傳遞給路由之前所有請求都要經過,這堵牆定義在app\Http\Kernel.php中的$middleware數組中,沒錯就是中間件,默認只有一個CheckForMaintenanceMode中間件,用來檢測你的網站是否暫時關閉。這是一個全局中間件,所有請求都要經過,你也可以添加自己的全局中間件。

然後遍歷所有註冊的路由,找到最先符合的第一個路由,經過它的路由中間件,進入到控制器或者閉包函數,執行你的具體邏輯代碼。

所以,在請求到達你寫的代碼之前,Laravel已經做了大量工作,請求也經過了千難萬險,那些不符合或者惡意的的請求已被Laravel隔離在外。

Laravel學習筆記-核心概念

image

服務容器

服務容器就是一個普通的容器,用來裝類的實例,然後在需要的時候再取出來。用更專業的術語來說是服務容器實現了控制反轉(Inversion of Control,縮寫為IoC),意思是正常情況下類A需要一個類B的時候,我們需要自己去new類B,意味著我們必須知道類B的更多細節,比如構造函數,隨著項目的複雜性增大,這種依賴是毀滅性的。控制反轉的意思就是,將類A主動獲取類B的過程顛倒過來變成被動,類A只需要聲明它需要什麼,然後由容器提供。

Laravel學習筆記-核心概念

image

這樣做的好處是,類A不依賴於類B的實現,這樣在一定程度上解決了耦合問題。

在Laravel的服務容器中,為了實現控制反轉,可以有以下兩種:

  1. 依賴注入(Dependency Injection)。
  2. 綁定。

依賴注入

依賴注入是一種類型提示,舉官網的例子:

Copyclass UserController extends Controller
{
/**
* The user repository implementation.
*
* @var UserRepository
*/
protected $users;
/**
* Create a new controller instance.
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* Show the proimage for the given user.
*
* @param int $id
* @return Response
*/
public function show($id)
{
$user = $this->users->find($id);
return view('user.proimage', ['user' => $user]);
}
}

這裡UserController需要一個UserRepository實例,我們只需在構造方法中聲明我們需要的類型,容器在實例化UserController時會自動生成UserRepository的實例(或者實現類,因為UserRepository可以為接口),而不用主動去獲取UserRepository的實例,這樣也就避免了了解UserRepository的更多細節,也不用解決UserRepository所產生的依賴,我們所做的僅僅是聲明我們所需要的類型,所有的依賴問題都交給容器去解決。

綁定

綁定操作一般在ServiceProviders中的register方法中,最基本的綁定是容器的bind方法,它接受一個類的別名或者全名和一個閉包來獲取實例:

Copy$this->app->bind('XblogConfig', function ($app) {
return new MapRepository();
});

還有一個singleton方法,和bind寫法沒什麼區別。你也可以綁定一個已經存在的對象到容器中,上文中提到的request實例就是通過這種方法綁定到容器的:$this->app->instance('request', $request);。綁定之後,我們可以通過一下幾種方式來獲取綁定實例:

Copy1. app('XblogConfig');
2. app()->make('XblogConfig');
3. app()['XblogConfig'];
4. resolve('XblogConfig');

以上四種方法均會返回獲得MapRepository的實例,唯一的區別是,在一次請求的生命週期中,bind方法的閉包會在每一次調用以上四種方法時執行,singleton方法的閉包只會執行一次。在使用中,如果每一個類要獲的不同的實例,或者需要“個性化”的實例時,這時我們需要用bind方法以免這次的使用對下次的使用造成影響;如果實例化一個類比較耗時或者類的方法不依賴該生成的上下文,那麼我們可以使用singleton方法綁定。singleton方法綁定的好處就是,如果在一次請求中我們多次使用某個類,那麼只生成該類的一個實例將節省時間和空間。

你也可以綁定接口與實現,例如:

Copy$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);

上文講述的Laravel的生命週期的第二步,Laravel默認(在bootstrap\app.php文件中)綁定了Illuminate\Contracts\Http\Kernel,Illuminate\Contracts\Console\Kernel,Illuminate\Contracts\Debug\ExceptionHandler接口的實現類,這些是實現類框架的默認自帶的。但是你仍然可以自己去實現。

還有一種上下文綁定,就是相同的接口,在不同的類中可以自動獲取不同的實現,例如:

Copy$this->app->when(PhotoController::class)
->needs(imagesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(imagesystem::class)
->give(function () {
return Storage::disk('s3');
});

上述表明,同樣的接口imagesystem,使用依賴注入時,在PhotoController中獲取的是local存儲而在VideoController中獲取的是s3存儲。

Contracts & Facades(合同&假象)

Laravel 還有一個強大之處是,比如你只需在配置文件中指明你需要的緩存驅動(redis,memcached,image......),Laravel 就自動辦你切換到這種驅動,而不需要你針對某種驅動更改邏輯和代碼。Why? 很簡單,Laravel定義了一系列Contracts(翻譯:合同),本質上是一系列PHP接口,一系列的標準,用來解耦具體需求對實現的依賴關係。其實真正強大的公司是制定標準的公司,程序也是如此,好的標準(接口)尤為重要。當程序變得越來大,這種通過合同或者接口來解耦所帶來的可擴展性和可維護性是無可比擬的。

Laravel學習筆記-核心概念

image

上圖不使用Contracts的情況下,對於一種邏輯,我們只能得到一種結果(方塊),如果變更需求,意味著我們必須重構代碼和邏輯。但是在使用Contracts的情況下,我們只需要按照接口寫好邏輯,然後提供不同的實現,就可以在不改動代碼邏輯的情況下獲得更加多態的結果。

這麼說有點抽象,舉一個真實的例子。在項目中使用了緩存,所以導致Repository中充滿了和cache相關的方法:remember,flush,forget等等。後來國外網友反映,簡單的博客並不一定需要緩存。所以我決定把它變成可選,但因為代碼中充滿和cache相關的方法,實現起來並不是很容易。於是想起Laravel的重要概念Contracts。於是,

我把與緩存有關的方法抽象出來形成一個Contracts:XblogCache,實際操作只與Contracts有關,這樣問題就得到了解決,而幾乎沒有改變原有的邏輯。

Copynamespace App\Contracts;
use Closure;
interface XblogCache
{
public function setTag($tag);
public function setTime($time_in_minute);
public function remember($key, Closure $entity, $tag = null);
public function forget($key, $tag = null);
public function clearCache($tag = null);
public function clearAllCache();
}

然後,我又完成了兩個實現類:Cacheable和NoCache:

  1. 實現具體緩存。
Copyclass Cacheable implements XblogCache
{
public $tag;
public $cacheTime;
public function setTag($tag)
{
$this->tag = $tag;
}
public function remember($key, Closure $entity, $tag = null)
{
return cache()->tags($tag == null ? $this->tag : $tag)->remember($key, $this->cacheTime, $entity);
}
public function forget($key, $tag = null)
{
cache()->tags($tag == null ? $this->tag : $tag)->forget($key);
}
public function clearCache($tag = null)
{
cache()->tags($tag == null ? $this->tag : $tag)->flush();
}
public function clearAllCache()

{
cache()->flush();
}
public function setTime($time_in_minute)
{
$this->cacheTime = $time_in_minute;
}
}
  1. 不緩存。
Copyclass NoCache implements XblogCache
{
public function setTag($tag)
{
// Do Nothing
}
public function setTime($time_in_minute)
{
// Do Nothing
}
public function remember($key, Closure $entity, $tag = null)
{
/**
* directly return
*/
return $entity();
}
public function forget($key, $tag = null)
{
// Do Nothing
}
public function clearCache($tag = null)
{
// Do Nothing
}
public function clearAllCache()
{
// Do Nothing
}
}

然後再利用容器的綁定,根據不同的配置,返回不同的實現

Copypublic function register()
{
$this->app->bind('XblogCache', function ($app) {
if (config('cache.enable') == 'true') {
return new Cacheable();
} else {
return new NoCache();
}
});
}

這樣,就實現了緩存的切換而不需要更改你的具體邏輯代碼。當然依靠接口而不依靠具體實現的好處不僅僅這些。實際上,Laravel所有的核心服務都是實現了某個Contracts接口(都在Illuminate\Contracts\文件夾下面),而不是依賴具體的實現,所以完全可以在不改動框架的前提下,使用自己的代碼改變Laravel框架核心服務的實現方式。

說一說Facades。在我們學習了容器的概念後,Facades就變得十分簡單了。在我們把類的實例綁定到容器的時候相當於給類起了個別名,然後覆蓋Facade的靜態方法getFacadeAccessor並返回你的別名,然後你就可以使用你自己的Facade的靜態方法來調用你綁定類的動態方法了。其實Facade類利用了__callStatic() 這個魔術方法來延遲調用容器中的對象的方法,這裡不過多講解,你只需要知道Facade實現了將對它調用的靜態方法映射到綁定類的動態方法上,這樣你就可以使用簡單類名調用而不需要記住長長的類名。這也是Facades的中文翻譯為假象的原因。

總結

Laravel強大之處不僅僅在於它給你提供了一系列腳手架,比如超級好用的ORM,基於Carbon的時間處理,以及文件存儲等等功能。但是Laravel的核心非常非常簡單:利用容器抽象解耦,實現高擴展性。容器和抽象是所有大型框架必須解決的問題,像Java的Spring,Android的Dagger2等等都是圍繞這幾個問題的。所以本質上講,Laravel之所以強大出名,是因為它的設計,思想,可擴展性。而Laravel的好用功能只是官方基於這些核心提供的腳手架,你同樣也可以很輕鬆的添加自己的腳手架。

所以不要覺得Laravel強大是因為他提供的很多功能,而是它的設計模式和思想。

  1. 理解Laravel生命週期和請求的生命週期概念。
  2. 所有的靜態變量和單例,在下一個請求到來時都會重新初始化。
  3. 將耗時的類或者頻繁使用的類用singleton綁定。
  4. 將變化選項的抽象為Contracts,依賴接口不依賴具體實現。
  5. 善於利用Laravel提供的容器。

參考:

  1. 深入理解php底層:php生命週期
  2. Laravel 官方文檔
  3. laravel/framework

鏈接:https://www.jianshu.com/p/fd0e3b9cae2b


分享到:


相關文章: