推薦學習:
隨著現在應用的規模越來越龐大,物件之間的依賴關係也越來越複雜,耦合程度越來越高,經常會出現物件之間多重依賴的情況。對於如此龐大複雜的應用,任何修改都可能會牽一髮而動全身,這就為應用的後期維護造成了很多困擾。
為了解決物件之間耦合度高的問題,控制反轉(IoC)的思想也隨之誕生。所謂控制反轉,是物件導向程式設計中的一種設計原則,其目的是為了降低程式碼之間的耦合程度。在 Laravel 中,控制反轉是通過依賴注入(DI)的方式實現的。
控制反轉的基本思想是藉助 IoC 容器實現物件之間的依賴關係的解耦。引入 IoC 容器之後,所有物件的控制權都上交給 IoC 容器,IoC 容器成了整個系統的核心,把所有物件粘合在一起發揮作用。Laravel 中的容器即起到了這個作用。
所謂容器,在 Laravel 中指的是 \Illuminate\Foundation\Application 物件,Laravel 框架在啟動時即建立了該物件。
# public/index.php $app = require_once __DIR__.'/../bootstrap/app.php'; # bootstrap/app.php $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
在建立容器的過程中,Laravel 還會對容器進行一些基礎的繫結和服務註冊。Laravel 首先會將容器範例與 app 和 Illuminate\Container\Container 進行繫結;之後,Laravel 會將基礎的服務提供者註冊到容器範例中,包括事件、紀錄檔、路由服務提供者;最後,Laravel 會將框架核心 class 與其相對應的別名一起註冊到容器範例當中。
// namespace Illuminate\Foundation\Application public function __construct($basePath = null) { if ($basePath) { $this->setBasePath($basePath); } $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); } protected function registerBaseBindings() { static::setInstance($this); $this->instance('app', $this); $this->instance(Container::class, $this); /* ... ... */ } protected function registerBaseServiceProviders() { $this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this)); } public function registerCoreContainerAliases() { foreach ([ 'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class], /* ... ...*/ 'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], /* ... ... */ 'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class], 'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class], /* ... ... */ ] as $key => $aliases) { foreach ($aliases as $alias) { $this->alias($key, $alias); } } } // namespace Illuminate\Container\Container public function alias($abstract, $alias) { if ($alias === $abstract) { throw new LogicException("[{$abstract}] is aliased to itself."); } $this->aliases[$alias] = $abstract; $this->abstractAliases[$abstract][] = $alias; }
在完成這三步基本的註冊之後,我們可以很方便的存取已經註冊到容器中的物件範例。例如,可以直接通過 $app['app'] 或 $app['Illuminate\Container\Container'] 存取容器本身,還可以通過 $app['db'] 直接存取資料庫連線。
註冊服務提供者
在容器建立的過程中會註冊基礎服務提供者,其註冊過程通過呼叫 register() 方法完成。
// namespace Illuminate\Foundation\Application public function register($provider, $force = false) { if (($registered = $this->getProvider($provider)) && ! $force) { return $registered; } if (is_string($provider)) { $provider = $this->resolveProvider($provider); } $provider->register(); if (property_exists($provider, 'bindings')) { foreach ($provider->bindings as $key => $value) { $this->bind($key, $value); } } if (property_exists($provider, 'singletons')) { foreach ($provider->singletons as $key => $value) { $this->singleton($key, $value); } } $this->markAsRegistered($provider); if ($this->isBooted()) { $this->bootProvider($provider); } return $provider; }
Laravel 首先會判斷指定的服務提供者是否已經在容器中註冊(通過呼叫 getProvider() 方法實現),如果指定的服務提供者已經在容器中註冊,並且本次註冊操作並非強制執行,那麼直接返回已經註冊好的服務提供者。
如果不滿足上述條件,那麼 Laravel 就會開始註冊服務提供者。此時,如果傳參為字串,那麼 Laravel 會預設引數為服務提供者的 class 名稱並進行範例化(通過 resolveProvider() 方法實現)。之後,就會呼叫服務提供者定義的 register() 方法進行註冊。以紀錄檔服務提供者為例,其 register() 方法的方法體如下
// namespace Illuminate\Log\LogServiceProvider public function register() { $this->app->singleton('log', function ($app) { return new LogManager($app); }); }
register() 方法的作用就是將 Illuminate\Log\LogManager 物件以單例的模式註冊到容器當中,註冊完成之後,容器的 $bindings 屬性中會增加一項
$app->bindings['log'] = [ 'concrete' => 'Illuminate\Log\LogManager {#162}', 'shared' => true, ];
如果服務提供者自身還定義了 $bindings 屬性以及 $singletons 屬性,那麼 Laravel 還會呼叫相應的 bind() 方法和 singleton() 方法完成這些服務提供者自定義的繫結的註冊。
這之後 Laravel 會將服務提供者標記為已經註冊的狀態,隨後會呼叫服務提供者定義的 boot() 方法啟動服務提供者(前提是應用已經啟動)。
在向容器中註冊繫結時,有 bind() 和 singleton() 兩種方法,其區別僅在於註冊的繫結是否為單例模式,即 shared 屬性是否為 true 。
// namespace Illuminate\Container\Container public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); } public function bind($abstract, $concrete = null, $shared = false) { // 刪除舊的繫結 $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } 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 = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact('concrete', 'shared'); if ($this->resolved($abstract)) { $this->rebound($abstract); } } protected function getClosure($abstract, $concrete) { return function ($container, $parameters = []) use ($abstract, $concrete) { if ($abstract == $concrete) { return $container->build($concrete); } return $container->resolve( $concrete, $parameters, $raiseEvents = false ); }; }
仍然以紀錄檔服務提供者為例,紀錄檔服務提供者在註冊時以單例模式進行註冊,並且 $concrete 引數為閉包。在繫結開始之前,Laravel 首先會刪除舊的繫結。由於此時 $concrete 為閉包,所以 Laravel 並不會進行什麼操作,只是將繫結資訊存入 $bindings 屬性當中。
存取服務
在服務提供者註冊完成之後,我們可以用上文提到的類似存取資料庫連線的方式那樣存取服務。仍然以紀錄檔服務為例,我們可以通過 $app['log'] 的方式存取紀錄檔服務。另外,在 Laravel 中,我們還可以使用 facade 的方式存取服務,例如,我們可以呼叫 Illuminate\Support\Facades\Log::info() 來記錄紀錄檔。
// namespace Illuminate\Support\Facades\Log class Log extends Facade { protected static function getFacadeAccessor() { return 'log'; } } // namespace Illuminate\Support\Facades\Facade public static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); /* ... ... */ return $instance->$method(...$args); } public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); } protected static function resolveFacadeInstance($name) { if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { return static::$resolvedInstance[$name] = static::$app[$name]; } }
在通過靜態呼叫的方式進行紀錄檔記錄時,首先會存取 Facade 中的魔術方法 __callStatic() ,該方法的首先進行的就是解析出 facade 對應的服務範例,然後呼叫該服務範例下的方法來執行相應的功能。每個 facade 中都會定義一個 getFacadeAccessor() 方法,這個方法會返回一個 tag,在紀錄檔服務中,這個 tag 就是紀錄檔服務提供者的閉包在容器的 $bindings 屬性中的 key。也就是說,通過 facade 方式最終得到的是 $app['log']。
那麼為什麼可以通過關聯陣列的方式存取容器中註冊的物件/服務?Illuminate\Container\Container 實現了 ArrayAccess 並且定義了 OffsetGet() 方法,而 Illuminate\Foundation\Application 繼承了 Container ,$app 為 Application 範例化的物件,所以通過關聯陣列的方式存取容器中註冊的物件時會存取 Container 的 OffsetGet() 方法。在 OffsetGet() 方法中會呼叫 Container 的 make() 方法,而 make() 方法中又會呼叫 resolve() 方法。resolve() 方法最終會解析並返回相應的物件。
// namespace Illuminate\Container public function offsetGet($key) { return $this->make($key); } public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); } protected function resolve($abstract, $parameters = [], $raiseEvents = true) { /* ... ... */ $this->with[] = $parameters; if (is_null($concrete)) { $concrete = $this->getConcrete($abstract); } if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } /* ... ... */ $this->resolved[$abstract] = true; array_pop($this->with); return $object; } protected function getConcrete($abstract) { if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract]['concrete']; } return $abstract; } protected function isBuildable($concrete, $abstract) { return $concrete === $abstract || $concrete instanceof Closure; } public function build($concrete) { if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } /* ... ... */ } protected function getLastParameterOverride() { return count($this->with) ? end($this->with) : []; }
這裡需要說明,在通過 $app['log'] 的方式解析紀錄檔服務範例時,resolve() 方法中的 $concrete 解析得到的是一個閉包,導致 isBuildable() 方法返回結果為 true,所以 Laravel 會直接呼叫 build() 方法。而由於此時 $concrete 是一個閉包,所以在 build() 方法中會直接執行這個閉包函數,最終返回 LogManager 範例。
在基礎的繫結和服務註冊完成之後,容器建立成功並返回 $app 。之後 Laravel 會將核心(包括 Http 核心和 Console 核心)和例外處理註冊到容器當中。然後 Laravel 開始處理請求。
// namespace bootstrap/app.php $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); // public/index.php $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Request::capture() )->send(); $kernel->terminate($request, $response);
在開始處理請求之前,Laravel 首先會解析出 Http 核心物件 $kernel,即 App\Http\Kernel 範例化的物件。而 App\Http\Kernel 繼承了 Illuminate\Foundation\Kernel,所以 $kernel 實際呼叫的是 Illuminate\Foundation\Kernel 中的 handle() 方法。
namespace Illuminate\Foundation\Http use Illuminate\Contracts\Debug\ExceptionHandler public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Throwable $e) { $this->reportException($e); $response = $this->renderException($request, $e); } $this->app['events']->dispatch( new RequestHandled($request, $response) ); return $response; } // 上報錯誤 protected function reportException(Throwable $e) { $this->app[ExceptionHandler::class]->report($e); } // 渲染錯誤資訊 protected function renderException($request, Throwable $e) { return $this->app[ExceptionHandler::class]->render($request, $e); }
handle() 方法在處理請求的過程中如果出現任何異常或錯誤,Laravel 都會呼叫容器中已經註冊好的例外處理物件來上報異常並且渲染返回資訊。
在容器建立成功以後,Laravel 會將 Illuminate\Contracts\Debug\ExceptionHandler 和 App\Exceptions\Handler 之間的繫結註冊到容器當中,所以 Laravel 處理異常實際呼叫的都是 App\Exceptions\Handler 中的方法。在實際開發過程中,開發者可以根據自身需要在 App\Exceptions\Handler 中自定義 report() 和 render() 方法。
在 PHP 7 中,`Exception` 和 `Error` 是兩種不同的型別,但它們同時都繼承了 `Throwable` ,所以 `handler()` 方法中捕獲的是 `Throwable` 物件。
在正式開始處理請求之前,Laravel 會進行一些引導啟動,包括載入環境變數、設定資訊等,這些引導啟動在 Laravel 執行過程中起到了非常重要的作用。
// namespace Illuminate\Foundation\Http\Kernel protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; protected function sendRequestThroughRouter($request) { /* ... ... */ $this->bootstrap(); /* ... ... */ } public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } // namespace Illuminate\Foundation\Application public function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]); } }
從程式碼中可以看出,引導啟動的過程實際就是呼叫各個 class 中的 bootstrap() 方法。其中:
LoadEnvironmentVariables 用來載入環境變數
LoadConfiguration 用來載入 config 目錄下的組態檔
HandleExceptions 用來設定 PHP 的錯誤報告級別以及相應的異常和錯誤處理常式,另外還會設定 PHP 的程式終止執行函數
// namespace Illuminate\Foundation\Bootstrap\HandleExceptions public function bootstrap(Application $app) { /* ... ... */ $this->app = $app; error_reporting(-1); set_error_handler([$this, 'handleError']); set_exception_handler([$this, 'handleException']); register_shutdown_function([$this, 'handleShutdown']); /* ... ... */ } public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { /* ... ... */ throw new ErrorException($message, 0, $level, $file, $line); } } public function handleException(Throwable $e) { /* ... ... */ $this->getExceptionHandler()->report($e); /* ... ... */ } public function handleShutdown() { if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) { $this->handleException($this->fatalErrorFromPhpError($error, 0)); } } protected function getExceptionHandler() { return $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class); }
從以上程式碼中可以看出,雖然 HandleExceptions 中定義了異常、錯誤、程式終止的處理常式,但無論是哪種情況,最終還是呼叫 App\Exceptions\Handler 中的方法來處理異常或錯誤。
RegisterFacades 的作用一個是註冊組態檔以及第三方包中自定義的 alias 類,還有一個非常重要的作用就是為 Illuminate\Support\Facades\Facade 類設定 $app 屬性。
// namespace Illuminate\Foundation\Bootstrap\RegisterFAcades public function bootstrap(Application $app) { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); }
&emsp 我們在通過 facade 方式反問容器中註冊的服務時,Facade 在解析容器中的服務範例時用到的 static::$app 即是在這個時候設定的。
RegisterProviders 的作用是註冊組態檔以及第三方包中定義的服務提供者
// namespace Illuminate\Foundation\Bootstrap\RegisterProviders public function bootstrap(Application $app) { $app->registerConfiguredProviders(); } public function registerConfiguredProviders() { $providers = Collection::make($this->make('config')->get('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()); }
在實際註冊的過程中,Laravel 會按照 Laravel 框架的服務提供者 > 第三方包的服務提供者 > 開發者自定義的服務提供者 的順序進行註冊
BootProviders 則是按順序呼叫已經註冊到容器中的服務提供者的 boot() 方法(前提是服務提供者定義的 boot() 方法)
在引導啟動完成之後,Laravel 開始處理請求,首先要做的就是將全域性的中介軟體應用於 request 。這之後 Laravel 會將請求分發到相應的路由進行處理,處理之前需要先根據 request 找到相應的路由物件 Illuminate\Routing\Route。在 Laravel 中,除了全域性中介軟體,還有一些中介軟體只作用於特定的路由或路由分組,此時這些中介軟體就會被作用於 request 。這些工作都完成之後,路由物件開始執行程式碼,完成請求。
// namespace Illuminate\Foundation\Http\Kernel protected function sendRequestThroughRouter($request) { /* ... ... */ return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); } protected function dispatchToRouter() { return function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request); }; } // namespace Illuminate\Routing\Router public function dispatch(Request $request) { $this->currentRequest = $request; return $this->dispatchToRoute($request); } public function dispatchToRoute(Request $request) { return $this->runRoute($request, $this->findRoute($request)); } protected function runRoute(Request $request, Route $route) { /* ... ... */ return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) ); } protected function runRouteWithinStack(Route $route, Request $request) { /* ... ... */ return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { return $this->prepareResponse( $request, $route->run() ); }); }
Laravel 中的路由在註冊時,action 可以是控制器方法,也可以是閉包。但無論是那種形式,都需要傳參,而傳參就會遇到需要依賴注入的情況。
Route 物件在執行 run() 方法時會根據 action 的型別分別進行控制器方法呼叫或閉包函數的呼叫。但兩種方法最終都需要解析引數,而如果引數中用到了 class ,就需要進行依賴注入。
// namespace Illuminate\Routing\Router public function run() { $this->container = $this->container ?: new Container; try { if ($this->isControllerAction()) { return $this->runController(); } return $this->runCallable(); } catch (HttpResponseException $e) { return $e->getResponse(); } } protected function runController() { return $this->controllerDispatcher()->dispatch( $this, $this->getController(), $this->getControllerMethod() ); } protected function runCallable() { /* ... ... */ return $callable(...array_values($this->resolveMethodDependencies( $this->parametersWithoutNulls(), new ReflectionFunction($callable) ))); } // namespace Illuminate\Routing\ControllerDispatcher public function dispatch(Route $route, $controller, $method) { $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); /* ... ... */ } // namespace Illuminate\Routing\RouteDependencyResolverTrait protected function resolveClassMethodDependencies(array $parameters, $instance, $method) { /* ... ... */ return $this->resolveMethodDependencies( $parameters, new ReflectionMethod($instance, $method) ); } public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector) { /* ... ... */ foreach ($reflector->getParameters() as $key => $parameter) { $instance = $this->transformDependency($parameter, $parameters, $skippableValue); /* ... ... */ } return $parameters; } protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue) { $className = Reflector::getParameterClassName($parameter); if ($className && ! $this->alreadyInParameters($className, $parameters)) { return $parameter->isDefaultValueAvailable() ? null : $this->container->make($className); } return $skippableValue; }
在執行過程中,Laravel 首先通過反射取得參數列(對於控制器方法,使用 ReflectionMethod ,對於閉包函數,則使用 ReflectionFunction )。在得到參數列後,Laravel 仍然是利用反射,逐個判斷引數型別。如果引數型別為 PHP 的內建型別,那麼不需要什麼特殊處理;但如果引數不是 PHP 內建型別,則需要利用反射解析出引數的具體型別。在解析出引數的具體型別之後,緊接著會判斷該型別的物件是不是已經存在於參數列中,如果不存在並且該型別也沒有設定預設值,那麼就需要通過容器建立出該型別的範例。
要通過容器建立指定 class 的範例,仍然需要用到 resolve() 方法。前文已經敘述過使用 resolve() 方法解析閉包函數的情況,所以這裡值敘述範例化 class 的情況。
// namespace Illuminate\Container\Container public function build($concrete) { /* ... ... */ try { $reflector = new ReflectionClass($concrete); } catch (ReflectionException $e) { throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e); } 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); return $reflector->newInstanceArgs($instances); } protected function resolveDependencies(array $dependencies) { $results = []; foreach ($dependencies as $dependency) { if ($this->hasParameterOverride($dependency)) { $results[] = $this->getParameterOverride($dependency); continue; } $result = is_null(Util::getParameterClassName($dependency)) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); if ($dependency->isVariadic()) { $results = array_merge($results, $result); } else { $results[] = $result; } } return $results; }
容器在範例化 class 的時候,仍然是通過反射獲取 class 基本資訊。對於一些無法進行範例化的 class (例如 interface 、abstract class ),Laravel 會丟擲異常;否則 Laravel 會繼續獲取 class 的建構函式的資訊。對於不存在建構函式的 class ,意味著這些 class 在範例化的時候不需要額外的依賴,可以直接通過 new 來範例化;否則仍然是通過反射解析出建構函式的參數列資訊,然後逐個範例化這些參數列中用到的 class 。在這些參數列中的 class 都範例化完成之後,通過容器建立 class 的準備工作也已經完成,此時容器可以順利建立出指定 class 的範例,然後注入到控制器方法或閉包中。
推薦學習:
以上就是Laravel範例詳解之容器、控制反轉和依賴注入的詳細內容,更多請關注TW511.COM其它相關文章!