這篇部落格在草稿箱堆了挺久的,原本還想趁著過年期間寫完這篇部落格的,後來又沒寫成,不過還是趁著上班後的幾天寫好了,想著使用laravel希望能多瞭解其內部原始碼的實現原理,所以結合文件和原始碼寫了這篇部落格。以下內容如果有錯歡迎指出。
服務容器
服務容器的概念理解可以檢視這裡我寫的服務容器相關概念。
結合文件和原始碼來分析一下laravel是如何實現的
根據文件的介紹,Laravel 服務容器是一個用於管理類依賴以及實現依賴注入的強有力工具。而服務容器正是laravel實現功能的核心利器。
laravel的服務容器提供了幾個比較核心的方法
- singleton($abstract, $concrete = null)
- bind($abstract, $concrete = null, $shared = false)
- make($abstract, array $parameters = [])
實際上singleton和bind呼叫的函式實現是相同的,只是在呼叫引數上,將$shared變數置為true,通過$shared來判斷是否為單例物件
/**
* 新增單例類到容器中
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
檢視bind()方法,根據文件所給出的內容,bind 方法的第一個引數為要繫結的類 / 介面名,第二個引數是一個返回類例項的 Closure
/**
* 新增繫結類到容器中
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
//刪除容器例項和別名中$abstract的值
$this->dropStaleInstances($abstract);
//判斷是否為空,若為空則等於$abstract,後續容器可通過反射類獲取類例項
if (is_null($concrete)) {
$concrete = $abstract;
}
//判斷$concrete是否為閉包,如果不為閉包,則轉換成閉包的形式
if (! $concrete instanceof Closure) {
if (! is_string($concrete)) {
throw new \TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
}
//將$concrete以閉包形式返回,這裡呼叫反射類獲取類例項
$concrete = $this->getClosure($abstract, $concrete);
}
//將當前$abstract以key=>value的形式繫結到$this->binding變數中,其中$concrete為返回類例項,$shared用於判斷是否為單例物件
$this->bindings[$abstract] = compact('concrete', 'shared');
//判斷當前例項是否已存在,判斷是否需要回撥重新更新類例項
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
再來看看make()方法,從容器中解析出類例項,也是容器中比較核心的概念,其本質是呼叫了resolve方法實現,可檢視文件make方法
/**
* 從容器中解析當前例項
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
以下為resolve的原始碼實現
/**
* 解析指定類到容器中
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
//檢視$abstract是否為別名,如果是則返回真正的類名
$abstract = $this->getAlias($abstract);
//獲取返回類例項
$concrete = $this->getContextualConcrete($abstract);
//
$needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
//判斷單例陣列中是否已例項過,若已例項過則直接返回,這樣在整個框架的生命週期中,單例物件永遠只有一個
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
//判斷類例項是否為空,為空則檢視$this->bingding繫結物件中是否存在例項,若沒有則等於$abstract類名
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
}
//判斷當前類是否可被解析,可以則呼叫build()方法對閉包或類名進行解析,不可解析則回撥make()方法,其中build()方法使用反射機制是實現框架依賴注入的核心
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
//遍歷定義的擴充套件,若已定義擴充套件則例項返回擴充套件閉包,這裡應該是為了方便後續更改某些類的實現,所以在這裡遍歷擴充套件
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
//判斷是否為單例物件,為單例則將例項新增到$this->instances例項物件中
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
//將$this->resolved中的類名置為true,表示已解析完成
$this->resolved[$abstract] = true;
array_pop($this->with);
//返回例項
return $object;
}
最後再來看看依賴注入的核心build()方法
/**
* 例項化具體物件類
* @param \Closure|string $concrete
* @return mixed
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
//在上述講解,$concrete的值是一個閉包或者類名,這裡首先判斷值是否為閉包,若為閉包則直接執行,其中$this->getLastParameterOverride()方法用於獲取閉包對應引數
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
//生成$concrete的反射類,若類不存在則丟擲異常
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
//isInstantiable()為反射類中的函式,用於判斷類是否可以被例項化,若類為介面類和抽象類則不能被例項化,則丟擲異常
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
//獲取類的建構函式
$constructor = $reflector->getConstructor();
//判斷建構函式是否為空,若為空則說明該類沒有依賴引數,則直接例項化返回
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
//獲取建構函式引數
$dependencies = $constructor->getParameters();
//解析獲取類依賴引數,若解析失敗則丟擲異常
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
//通過newInstanceArgs()建立類例項並返回
return $reflector->newInstanceArgs($instances);
}
服務提供者
根據文件所講解的,服務提供者是所有 Laravel 應用程式的引導中心。你的應用程式,以及 通過伺服器引導的 Laravel 核心服務都是通過服務提供器引導。但是,「引導」是什麼意思呢? 通常,我們可以理解為註冊,比如註冊服務容器繫結,事件監聽器,中介軟體,甚至是路由。服務提供者是配置應用程式的中心。
在config/app.php
檔案中,我們可以檢視到所有服務提供者
這裡隨便檢視一個服務提供者,如以下RedisServiceProvider為使用redis操作的類,根據文件定義我們只需要在register()中定義singleton(),bind()將例項註冊到容器中,在這裡是將redis操作類繫結為單例物件,redis連線類簡單繫結到容器中。同時RedisServiceProvider繼承了\Illuminate\Contracts\Support\DeferrableProvider
介面並實現provides(),使服務可以被延時載入,註冊繫結的服務只有真的需要時才會被載入
深入分析服務提供者,檢視原始碼是如何實現的
/**
* 將配置中的服務提供者註冊到容器中
* @return void
*/
public function registerConfiguredProviders()
{
//用於獲取config/app.php中providers變數中的所有服務提供者
$providers = Collection::make($this->config['app.providers'])
->partition(function ($provider) {
return strpos($provider, 'Illuminate\\') === 0;
});
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
//載入註冊服務提供者
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
}
register()為註冊伺服器,將服務提供者中繫結的服務新增到容器中
/**
* 將單個服務提供者中的服務註冊到容器中
* @param \Illuminate\Support\ServiceProvider|string $provider
* @param bool $force
* @return \Illuminate\Support\ServiceProvider
*/
public function register($provider, $force = false)
{
//檢查該服務提供者是否已被載入過,若載入過則直接返回,如果$force為true則該服務提供者會重新載入
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
//判斷如果傳入的$provider變數為字串,則例項化服務提供者類
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
//執行服務提供者的register(),實際上就是註冊繫結服務到容器中
$provider->register();
//檢查類中是否定義$bindings變數,如果定義則將其繫結到容器中,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#311d29
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
//檢查類中是否定義$singletons變數,如果定義則將其繫結到容器中,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#311d29
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$this->singleton($key, $value);
}
}
//將該服務提供者標記為已載入
$this->markAsRegistered($provider);
//檢查服務提供者是否定義boot()方法,若定義則會被執行,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#ea148f
if ($this->isBooted()) {
$this->bootProvider($provider);
}
//返回服務提供者例項
return $provider;
}
Facades(門面)
根據文件描述Facades 為應用的 服務容器 提供了一個「靜態」 介面。Laravel 自帶了很多 Facades,可以訪問絕大部分功能。Laravel Facades 實際是服務容器中底層類的 「靜態代理」 ,相對於傳統靜態方法,在使用時能夠提供更加靈活、更加易於測試、更加優雅的語法。
Facades流程如下所示:
以Log門面類為例,檢視其程式碼,程式碼只定義了getFacadeAccessor()方法返回log
字串,實際上我們使用Log日誌門面通常為Log::info()
這樣的形式,當呼叫靜態方法時會觸發__callStatic魔術方法
public static function __callStatic($method, $args)
{
//獲取容器中指定的例項,如上述為log,則返回容器中繫結例項中log的例項
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
//呼叫例項中指定方法
return $instance->$method(...$args);
}
以上就是Facades(門面)的概念,也比較簡單,當然具體的內容也可以看文件Facades
Contracts(契約)
Contracts實際上就是定義一組介面,其理念符合基於介面而非實現程式設計的設計思想,這裡就沒什麼好說的了。
本作品採用《CC 協議》,轉載必須註明作者和本文連結