laravel容器和依赖注入
-
啥是Ioc容器,方便我们实现依赖注入的一种实现,也就是说依赖注入不一定需要控制反转容器,只不过使用容器可能会方便些。
-
laravel通过向容器中绑定接口的具体实现,可实现不同实现的快速切换,接口在laravel中有个好听的名字叫契约。
-
面向接口编程和容器结合使用,可以轻松实现代码解耦,实现了关注分离。
-
面向接口开发的好处:除了可以快速切换实现了相同契约的实现,对开发测试同步进行,以及对单元测试都是非常有好的。
-
下面是一个简单的使用示例,为了相对好理解没有加入service层。
1 创建文章接口 <?php namespace AppContracts; interface ArticleRepository { // 返回文章列表 public function getList(): array; } 2 创建一个文章具体实现类 <?php namespace App; use AppContractsArticleRepository; use AppModelsArticle; class DbArticle implements ArticleRepository { public function getList(): array { return Article::all()->toArray(); } } 3 在容器中进行绑定 <?php namespace AppProviders; use AppDbArticle; use IlluminateSupportServiceProvider; use AppContractsArticleRepository; use AppDbArticle; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { $this->app->bind(ArticleRepository::class, function () { return new DbArticle(); }); } 4 在控制器中使用 <?php namespace AppHttpControllersTest; use AppHttpControllersController; use IlluminateHttpRequest; use AppContractsArticleRepository; class ArticleController extends Controller { protected $articles; public function __construct(ArticleRepository $articles) { $this->articles = $articles; } public function index() { $articles = $this->articles->getList(); return view('article.index', compact('articles')); } } 以上便是一个比较标准的依赖注入的使用方式,其实还有很多使用方式,比如通过app()函数或者App门面直接获取依赖等等。看到这里应该能感觉到依赖注入为开发者带来的方便了,上述例子中文章是从ORM中取出,当需求改变要求文章从mongodb或者redis中取出的时候,我们只需要编写单独的实现类,然后在服务提供者中绑定新的实现,业务代码完全不需要改变,就能快速实现切换。
-
下面讲解laravel如何实现的依赖注入 laravel版本6.12
从bootstrap/app.php开始 // 实例化app容器类 并传递项目根目录给构造函数 $app = new IlluminateFoundationApplication( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); IlluminateFoundationApplication中 public function __construct($basePath = null) { if ($basePath) { // 注册基本路径 调用container的instance方法 添加路径实例到容器 // 结果就是app实例的instances属性下多了几条路径映射 可直接打印$this查看 $this->setBasePath($basePath); } // 注册基本绑定 跳转到下面的registerBaseBindings方法 $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); } protected function registerBaseBindings() { static::$instance = $this; // 绑定自身到容器 $this->instance('app', $this); // 依然还是绑定 $this->instance(Container::class, $this); // 重点来了!!! 跳转到下面的singleton方法 $this->singleton(Mix::class); $this->instance(PackageManifest::class, new PackageManifest( new Filesystem, $this->basePath(), $this->getCachedPackagesPath() )); } IlluminateContainerContainer中 // 绑定共享实例 就是单例绑定 可以看到singleton方法就是调用了bind方法 public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); } /** * Register a binding with the container. * * @param string $abstract * @param Closure|string|null $concrete 注意参数类型 * @param bool $shared * @return void */ public function bind($abstract, $concrete = null, $shared = false) { // 此处的$abstract = IlluminateFoundationMix // 删除老的实例绑定 $this->dropStaleInstances($abstract); // laravel默认的如果没传递对应抽象的实例,那么就认为实例应该是传递的抽象的一个实例 if (is_null($concrete)) { // 如果concrete为null $concrete = $abstract; } // 如果传递的不是一个闭包 if (!$concrete instanceof Closure) { // laravel会试图根据传递进来的抽象得到一个闭包 // 跳转到getClosure方法 // $concrete = $this->getClosure($abstract, $concrete); } // 将返回的闭包和是否shared标志保存到bindings数组下 $this->bindings[$abstract] = compact('concrete', 'shared'); if ($this->resolved($abstract)) { $this->rebound($abstract); } } /** * Get the Closure to be used when building a type. * * @param string $abstract * @param string $concrete * @return Closure */ // 值得注意的是此时生成的闭包不会立即执行,触发条件官方在注释中写的很清楚,当要使用时才会执行此闭包 protected function getClosure($abstract, $concrete) { // 此时的$abstract $concrete都是IlluminateFoundationMix return function ($container, $parameters = []) use ($abstract, $concrete) { // 如果调用bind方法只传入了abstract if ($abstract == $concrete) { // 返回容器的build方法返回的实例 return $container->build($concrete); } // 否则直接调用contaner的resolve方法进行解析 return $container->resolve( $concrete, $parameters, $raiseEvents = false ); }; } // 什么时候使用到这些绑定呢(如何从容器中解析需要的实例呢) 来看make方法 调用方式app()->make($yourAbstract) /** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed * * @throws IlluminateContractsContainerBindingResolutionException */ public function make($abstract, array $parameters = []) { // 跳转到resolve方法 return $this->resolve($abstract, $parameters); } /** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @param bool $raiseEvents * @return mixed * * @throws IlluminateContractsContainerBindingResolutionException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { $abstract = $this->getAlias($abstract); $needsContextualBuild = !empty($parameters) || !is_null( $this->getContextualConcrete($abstract) ); // 如果要解析的类名instances中已经有了 并且不需要临时build 那么就返回已经存在的实例 if (isset($this->instances[$abstract]) && !$needsContextualBuild) { // dd(123, $abstract); // dd($this->instances); return $this->instances[$abstract]; } $this->with[] = $parameters; // getConcrete方法 拿到之前可能绑定过的binding 绑定过就是一个闭包 // 没绑定过返回自身,即通过容器make可一个自定义的类 $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { // 当concrete === abstract 或者 concrete是一个闭包的时候 直接调用build方法 // 跳转到build方法 $object = $this->build($concrete); } else { // 当concrete是一个类名的时候 调用make方法 $object = $this->make($concrete); } foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // 如果是共享的实例 或者 不需要临时build 那么就将实例存放到容器的instances数组中 if ($this->isShared($abstract) && !$needsContextualBuild) { $this->instances[$abstract] = $object; } if ($raiseEvents) { $this->fireResolvingCallbacks($abstract, $object); } $this->resolved[$abstract] = true; array_pop($this->with); // 返回实例 return $object; } public function build($concrete) { // 如果传递进来的是一个闭包 (通过之前bind方法绑定得到的闭包) // 直接调用闭包返回实例 if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } // 如果传递进来的是一个可能的类名 那么调用反射api解析依赖 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(); // 没有构造方法直接new实例 if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } // 拿到构造方法中的依赖 $dependencies = $constructor->getParameters(); try { // 解析依赖 // 跳转到resolveDependencies方法 $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; } // 如果构造方法中不存在类型提示 那么会返回null // 跳转到resolvePrimitive方法 $results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) // 如果存在类型提示 根据类型尝试解析出依赖实例 getClass能拿到一个确切的类名 // 跳转到resolveClass方法 : $this->resolveClass($dependency); } return $results; } protected function resolvePrimitive(ReflectionParameter $parameter) { // 首先尝试获取闭包 if (!is_null($concrete = $this->getContextualConcrete('$' . $parameter->name))) { return $concrete instanceof Closure ? $concrete($this) : $concrete; } // 尝试获取默认值 if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); } // 如果上面都不符合那么就直接抛出异常 $this->unresolvablePrimitive($parameter); } protected function resolveClass(ReflectionParameter $parameter) { try { // 又回到了make方法 进行了递归调用 // 至此一切形成了闭环 完美!!! return $this->make($parameter->getClass()->name); } catch (BindingResolutionException $e) { if ($parameter->isOptional()) { return $parameter->getDefaultValue(); } throw $e; } } 可以看到: 1 当从laravel中解析对象的时候,实际调用的是build方法,而build方法首先会尝试从之前的绑定中获取,如果未找到绑定那么就通过反射机制尝试解析,当然解析依赖的过程依然存在尝试获得之前绑定的过程。 2 laravel的容器解析依赖需要使用其固定的规则,直接调用app()函数或者App门面从容器中解析。 3 建议使用容器进行解析自定义类的时候,一定要加上依赖的类型 4 绑定的时候尽量使用闭包形式
其中的临时绑定等需要配合laravel容器的when with等api使用,还有解析时的事件等,本文都没进行讲解。
下期预告:Application类实例化时剩下的两个方法