This article brings you relevant knowledge about Laravel, which mainly introduces related issues about containers, control inversion and dependency injection. Let’s take a look at the related content. ,I hope everyone has to help.
Recommended learning: Getting started with Laravel
As the scale of current applications becomes larger and larger, the dependencies between objects also It becomes more and more complex, the degree of coupling is getting higher and higher, and there are often multiple dependencies between objects. For such a large and complex application, any modification may affect the whole body, which causes a lot of trouble for the later maintenance of the application.
In order to solve the problem of high coupling between objects, the idea of Inversion of Control (IoC) was also born. The so-called inversion of control is a design principle in object-oriented programming. Its purpose is to reduce the degree of coupling between codes. In Laravel, inversion of control is implemented through dependency injection (DI).
The basic idea of inversion of control is to use the IoC container to decouple the dependencies between objects. After the introduction of the IoC container, control of all objects is handed over to the IoC container. The IoC container becomes the core of the entire system, gluing all objects together to function. Containers in Laravel serve this purpose.
The so-called container in Laravel refers to the \Illuminate\Foundation\Application object, which is created when the Laravel framework starts.
# public/index.php $app = require_once __DIR__.'/../bootstrap/app.php'; # bootstrap/app.php $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
During the process of creating a container, Laravel will also perform some basic binding and service registration for the container. Laravel will first bind the container instance to the app and Illuminate\Container\Container; then, Laravel will register basic service providers to the container instance, including event, log, and routing service providers; finally, Laravel will bind the framework The core class is registered with the container instance together with its corresponding alias.
// 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; }
After completing these three basic steps of registration, we can easily access the object instances that have been registered in the container. For example, the container itself can be accessed directly via $app['app'] or $app['Illuminate\Container\Container'] , and the database connection can be accessed directly via $app['db'] .
Registering service providers
In the process of container creation, basic service providers will be registered , the registration process is completed by calling the register() method.
// 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 will first determine whether the specified service provider has been registered in the container (implemented by calling the getProvider() method). If the specified service provider has been registered in the container, and this registration operation is not If forced execution, the registered service provider will be returned directly.
If the above conditions are not met, Laravel will start to register the service provider. At this time, if the parameter is a string, Laravel will default to the class name of the service provider and instantiate it (through the resolveProvider() method). After that, the register() method defined by the service provider is called to register. Taking the log service provider as an example, the method body of its register() method is as follows
// namespace Illuminate\Log\LogServiceProvider public function register() { $this->app->singleton('log', function ($app) { return new LogManager($app); }); }
The function of the register() method is to register the Illuminate\Log\LogManager object into the container in singleton mode. The registration is completed. After that, an item will be added to the $bindings property of the container.
$app->bindings['log'] = [ 'concrete' => 'Illuminate\Log\LogManager {#162}', 'shared' => true, ];
If the service provider itself also defines the $bindings property and the $singletons property, Laravel will also call the corresponding bind() method and singleton() Method to complete the registration of custom bindings from these service providers.
After this, Laravel will mark the service provider as registered, and then call the boot() method defined by the service provider to start the service provider (provided that the application has been started).
When registering a binding in a container, there are two methods, bind() and singleton(). The only difference lies in whether the registered binding is in singleton mode, that is, whether the shared attribute is 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 ); }; }
Still taking the log service provider as an example, the log service provider is registered in singleton mode when registering, and the $concrete parameter is a closure. Before binding begins, Laravel first removes the old binding. Since $concrete is a closure at this time, Laravel will not perform any operations, but only stores the binding information in the $bindings property.
Accessing the service
After the service provider registration is completed, we can access the service in the same way as accessing the database connection mentioned above. Still taking the log service as an example, we can access the log service through $app['log']. In addition, in Laravel, we can also use facade to access services. For example, we can call Illuminate\Support\Facades\Log::info() to record logs.
// 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入门
The above is the detailed content of Detailed explanation of Laravel examples: containers, inversion of control and dependency injection. For more information, please follow other related articles on the PHP Chinese website!