随着现在应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,耦合程度越来越高,经常会出现对象之间多重依赖的情况。对于如此庞大复杂的应用,任何修改都可能会牵一发而动全身,这就为应用的后期维护造成了很多困扰。
为了解决对象之间耦合度高的问题,控制反转(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
的实例,然后注入到控制器方法或闭包中。