ホームページ >PHPフレームワーク >Laravel >Laravel の例の詳細な説明: コンテナー、制御の反転、依存関係の注入
この記事では、Laravel に関する関連知識を提供します。主にコンテナ、制御反転、依存関係注入に関する関連問題を紹介します。関連コンテンツを見てみましょう。皆さんが助けてくれることを願っています。
推奨学習: Laravel の入門
現在のアプリケーションの規模がますます大きくなるにつれて、オブジェクト間の依存関係もそれはますます複雑になり、結合の度合いはますます高くなり、オブジェクト間に複数の依存関係が存在することがよくあります。このように大規模で複雑なアプリケーションの場合、変更は本体全体に影響を与える可能性があり、その後のアプリケーションのメンテナンスに多大な問題を引き起こす可能性があります。
オブジェクト間の高結合の問題を解決するために、Inversion of Control (IoC) というアイデアも生まれました。いわゆる制御の反転はオブジェクト指向プログラミングの設計原則であり、その目的はコード間の結合度を減らすことです。 Laravel では、制御の反転は依存関係注入 (DI) によって実装されます。
制御の反転の基本的な考え方は、IoC コンテナを使用してオブジェクト間の依存関係を分離することです。 IoC コンテナの導入後は、すべてのオブジェクトの制御が IoC コンテナに引き継がれ、IoC コンテナがシステム全体の核となり、すべてのオブジェクトを結合して機能します。 Laravel のコンテナはこの目的を果たします。
Laravel のいわゆるコンテナは、Laravel フレームワークの開始時に作成される \Illuminate\Foundation\Application オブジェクトを指します。
# 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 は最初にコンテナ インスタンスをアプリと Illuminate\Container\Container にバインドします。次に、Laravel はイベント、ログ、ルーティング サービス プロバイダを含む基本的なサービス プロバイダをコンテナ インスタンスに登録します。最後に、Laravel はフレームワークのコア クラスをバインドします。は、対応するエイリアスとともにコンテナ インスタンスに登録されます。
// 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; }
これら 3 つの基本的な登録手順を完了すると、コンテナに登録されているオブジェクト インスタンスに簡単にアクセスできるようになります。たとえば、コンテナ自体には $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 はデフォルトでサービスプロバイダーのクラス名を設定し、インスタンス化します (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 はまた、対応する binding() メソッドと singleton() メソッドを呼び出して、これらのサービス プロバイダーからのカスタム バインディングの登録を完了します。 この後、Laravel はサービスプロバイダーを登録済みとしてマークし、サービスプロバイダーによって定義された boot() メソッドを呼び出してサービスプロバイダーを起動します (アプリケーションが開始されている場合)。 コンテナにバインディングを登録する場合、bind()とsingleton()の2つのメソッドがあり、違いは登録されたバインディングがシングルトンモードであるかどうか、つまり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 では、ファサードを使用してサービスにアクセスすることもでき、たとえば、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 中国語 Web サイトの他の関連記事を参照してください。