您好,登錄后才能下訂單哦!
這篇文章主要介紹“Laravel容器、控制反轉和依賴注入實例分析”,在日常操作中,相信很多人在Laravel容器、控制反轉和依賴注入實例分析問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Laravel容器、控制反轉和依賴注入實例分析”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
隨著現在應用的規模越來越龐大,對象之間的依賴關系也越來越復雜,耦合程度越來越高,經常會出現對象之間多重依賴的情況。對于如此龐大復雜的應用,任何修改都可能會牽一發而動全身,這就為應用的后期維護造成了很多困擾。
??為了解決對象之間耦合度高的問題,控制反轉(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容器、控制反轉和依賴注入實例分析”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。