這篇文章為大家帶來了關於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入门
以上是Laravel實例詳解之容器、控制反轉與依賴注入的詳細內容。更多資訊請關注PHP中文網其他相關文章!