从零实现一个 PHP 微框架 - IoC 容器
前言
差不多该写写该系列文章了,咕了好几天 ?。
在 XK-PHP 中 IoC 容器是框架的核心,其掌管着框架中实例的存储和初始化,并提供自动依赖注入等功能,我们可以把 IoC 容器看成一个拥有存储功能的工厂,当我们需要某个实例的时候,工厂会依靠需求将实例组装好并返回给需求者,如果实例是单例的,那么制作好的实例就可以存到仓库中,当需求者再次需要的时候就可以直接返回实例。需求者无需关心实例是如何制造的,只需要将需求提交给工厂即可。这看起来似乎就是工厂模式?IoC 容器和 工厂模式 很类似,但是工厂模式注入的依赖是定死的,而 IoC 容器可以依据需求按需注入依赖。
DI & IoC
由于我之前写过 DI 和 IoC 的介绍文章,这里就不重复写了,链接见下方:
IoC 容器
由于之前的文章已经说明了 IoC 容器的实现了,这里就不再讲解 IoC 容器内部的细节了,本文就只讲述将 IoC 容器集成到我们上次创建的项目之中。
首先,因为我们的容器需要兼容 PSR-11 ,那么就需要引入 psr/container
的包,来引入 ContainerInterface
接口:
_1composer require psr/container
然后容器需要使用两个自定义函数,我们将其放到 app/Helper/functions.php
中,并修改 composer.json
,使函数能被 Composer 自动导入并且全局生效:
_36<?php_36// functions.php_36_36// 解析 class@method 的字符串,返回 [class, method] 数组_36function str_parse_callback($callback, $default = null)_36{_36 if (is_array($callback)) {_36 return $callback;_36 }_36 if (strpos($callback, '@') !== false) {_36 return explode('@', $callback);_36 }_36 if (strpos($callback, '::') !== false) {_36 return explode('::', $callback);_36 }_36 return [$callback, $default];_36}_36_36// 解析 [class, method] 或 [class, split, method] 到 class@method 或 class::method_36function str_stringify_callback(_36 $callback,_36 $default = null,_36 bool $isStatic = false_36) {_36 $split = $isStatic ? '::' : '@';_36 if (is_array($callback)) {_36 return implode($split, $callback);_36 }_36 if (preg_match('/@|::/', $callback) > 0) {_36 return $callback;_36 }_36 if ($default === null) {_36 return $callback;_36 }_36 return "{$callback}{$split}{$default}";_36}
_9{_9 "autoload": {_9 "psr-4": {_9 "App\\": "app/"_9 },_9 // 新增_9 "files": ["app/Helper/functions.php"]_9 }_9}
修改了 composer.json
的 autoload
后需要运行以下命令后才能生效:
_1composer dump-auto
然后就可以写容器的代码了,首先创建 app/Kernel/Container.php
的文件,输入以下代码,本文的容器代码和之前的文章中的容器不一样,但是流程是一样的:
_559<?php_559_559namespace App\Kernel;_559_559use Closure;_559use Psr\Container\ContainerInterface;_559use ReflectionClass;_559use ReflectionException;_559use ReflectionFunction;_559use ReflectionMethod;_559use ReflectionParameter;_559use RuntimeException;_559use function array_pad;_559use function class_exists;_559use function compact;_559use function count;_559use function is_array;_559use function is_bool;_559use function is_int;_559use function is_string;_559use function preg_match;_559use function str_parse_callback;_559use function strpos;_559_559/**_559 * IoC 容器,兼容 PSR-11_559 */_559class Container implements ContainerInterface_559{_559 /**_559 * 容器中存储依赖的数组_559 * 存储的是闭包,运行闭包会返回对应的依赖实例_559 *_559 * @var array_559 */_559 protected $bindings = [];_559_559 /**_559 * 绑定方法_559 *_559 * @var array_559 */_559 protected $methodBindings = [];_559_559 /**_559 * 已创建的单例实例_559 *_559 * @var array_559 */_559 protected $instances = [];_559_559 /**_559 * 自动通过类名绑定类_559 *_559 * @var bool_559 */_559 protected $autobind = true;_559_559 /**_559 * 依赖别名_559 *_559 * @var string[]_559 */_559 protected $aliases = [];_559_559 /**_559 * 绑定依赖_559 *_559 * @param string|array $abstract 依赖名或者依赖列表_559 * @param Closure|string|null $concrete 依赖闭包_559 *_559 * @param bool $shared_559 * @param bool|string $alias_559 * @param bool $overwrite_559 * @return Container_559 */_559 public function bind(_559 $abstract,_559 $concrete = null,_559 bool $shared = false,_559 $alias = false,_559 bool $overwrite = false_559 ): Container {_559 // 同时绑定多个依赖_559 if (is_array($abstract)) {_559 foreach ($abstract as $_abstract => $value) {_559 if (is_int($_abstract)) {_559 $_abstract = $value;_559 }_559 $_concrete = null;_559 $_shared = false;_559 $_alias = false;_559 $_overwrite = false;_559 if (is_bool($value)) {_559 $_shared = $value;_559 } elseif (is_array($value)) {_559 [$_concrete, $_shared, $_alias, $_overwrite] = array_pad(_559 $value,_559 3,_559 false_559 );_559 }_559 $this->bind(_559 $_abstract,_559 $_concrete === false ? null : $_concrete,_559 $_shared,_559 $_alias,_559 $_overwrite_559 );_559 }_559 return $this;_559 }_559 [$abstract, $alias] = $this->getAbstractAndAliasByAlias(_559 $abstract,_559 $alias_559 );_559 // 为了方便绑定依赖,可以节省一个参数_559 if ($concrete === null) {_559 $concrete = $abstract;_559 }_559 $this->setBinding($abstract, $concrete, $shared, $overwrite);_559 if ($alias) {_559 $this->alias($abstract, $alias);_559 }_559 // 返回 this 使其支持链式调用_559 return $this;_559 }_559_559 // 设置 binding_559 protected function setBinding(_559 string $abstract,_559 $concrete,_559 bool $shared = false,_559 bool $overwrite = false_559 ): void {_559 $abstract = $this->getAbstractByAlias($abstract);_559 // 传入的默认是闭包,如果没有传入闭包则默认创建_559 if (!$concrete instanceof Closure) {_559 $concrete = function (Container $c, array $args = []) use (_559 $concrete_559 ) {_559 return $c->build($concrete, $args);_559 };_559 }_559 // 判断是否是单例,是否被设置过_559 if (!$overwrite && $shared && isset($this->bindings[$abstract])) {_559 throw new RuntimeException(_559 "Target [$abstract] is a singleton and has been bind"_559 );_559 }_559 // 设置绑定的闭包_559 $this->bindings[$abstract] = compact('concrete', 'shared');_559 }_559_559 // 获取 binding_559 protected function getBinding(string $abstract)_559 {_559 $abstract = $this->getAbstractByAlias($abstract);_559 if (!isset($this->bindings[$abstract])) {_559 // 尝试自动绑定_559 if (_559 $this->autobind &&_559 $abstract[0] !== '$' &&_559 class_exists($abstract)_559 ) {_559 $this->setBinding($abstract, $abstract);_559 } else {_559 throw new RuntimeException(_559 "Target [$abstract] is not binding or fail autobind"_559 );_559 }_559 }_559 return $this->bindings[$abstract];_559 }_559_559 // 判断 binding 是否存在_559 protected function hasBinding(string $abstract): bool_559 {_559 $abstract = $this->getAbstractByAlias($abstract);_559 return isset($this->bindings[$abstract]);_559 }_559_559 /**_559 * 实例化对象_559 *_559 * @param string $abstract 对象名称_559 * @param array $args_559 *_559 * @return mixed_559 */_559 public function make(string $abstract, array $args = [])_559 {_559 $abstract = $this->getAbstractByAlias($abstract);_559 $binding = $this->getBinding($abstract);_559 $concrete = $binding['concrete'];_559 $shared = $binding['shared'];_559 // 判断是否是单例,若是单例并且已经实例化过就直接返回实例_559 if ($shared && isset($this->instances[$abstract])) {_559 return $this->instances[$abstract];_559 }_559 // 构建实例_559 $instance = $concrete($this, $args);_559 // 判断是否是单例,若是则设置到容器的单例列表中_559 if ($shared) {_559 $this->instances[$abstract] = $instance;_559 }_559 return $instance;_559 }_559_559 /**_559 * 绑定单例_559 *_559 * @param string $abstract 依赖名称_559 * @param mixed $concrete 依赖闭包_559 * @param bool|string $alias_559 *_559 * @param bool $overwrite_559 * @return Container_559 */_559 public function singleton(_559 string $abstract,_559 $concrete = null,_559 $alias = false,_559 bool $overwrite = false_559 ): Container {_559 $this->bind($abstract, $concrete, true, $alias, $overwrite);_559 return $this;_559 }_559_559 /**_559 * 绑定已实例化的单例_559 *_559 * @param string $abstract 依赖名称_559 * @param mixed $instance 已实例化的单例_559 * @param string|false $alias_559 *_559 * @param bool $overwrite_559 * @return Container_559 */_559 public function instance(_559 string $abstract,_559 $instance,_559 $alias = false_559 ): Container {_559 [$abstract, $alias] = $this->getAbstractAndAliasByAlias(_559 $abstract,_559 $alias_559 );_559 $this->instances[$abstract] = $instance;_559 $this->bind(_559 $abstract,_559 function () use ($instance) {_559 return $instance;_559 },_559 true,_559 $alias,_559 true_559 );_559 return $this;_559 }_559_559 /**_559 * 构建实例_559 *_559 * @param Closure|string $class 类名或者闭包_559 * @param array $args_559 * @return mixed_559 *_559 * @throws ReflectionException_559 */_559 public function build($class, array $args = [])_559 {_559 if ($class instanceof Closure) {_559 return $class($this, $args);_559 }_559 if (!class_exists($class)) {_559 return $class;_559 }_559 // 取得反射类_559 $reflector = new ReflectionClass($class);_559 // 检查类是否可实例化_559 if (!$reflector->isInstantiable()) {_559 // 如果不能,意味着接口不能正常工作,报错_559 throw new RuntimeException("Target [$class] is not instantiable");_559 }_559 // 取得构造函数_559 $constructor = $reflector->getConstructor();_559 // 检查是否有构造函数_559 if ($constructor === null) {_559 // 如果没有,就说明没有依赖,直接实例化_559 $instance = new $class();_559 } else {_559 // 返回已注入依赖的参数数组_559 $dependency = $this->injectingDependencies($constructor, $args);_559 // 利用注入后的参数创建实例_559 $instance = $reflector->newInstanceArgs($dependency);_559 }_559 return $instance;_559 }_559_559 /**_559 * 注入依赖_559 *_559 * @param ReflectionFunction|ReflectionMethod $method_559 * @param array $args_559 *_559 * @return array_559 */_559 protected function injectingDependencies($method, array $args = []): array_559 {_559 $dependency = [];_559 $parameters = $method->getParameters();_559 foreach ($parameters as $parameter) {_559 if (isset($args[$parameter->name])) {_559 $dependency[] = $args[$parameter->name];_559 continue;_559 }_559 // 利用参数的类型声明,获取到参数的类型,然后从 bindings 中获取依赖注入_559 $dependencyClass = $parameter->getClass();_559 if ($dependencyClass === null) {_559 $dependency[] = $this->resolvePrimitive($parameter);_559 } else {_559 // 实例化依赖_559 $dependency[] = $this->resolveClass($parameter);_559 }_559 }_559 return $dependency;_559 }_559_559 /**_559 * 处理非类的依赖_559 *_559 * @param ReflectionParameter $parameter_559 *_559 * @return mixed_559 */_559 protected function resolvePrimitive(ReflectionParameter $parameter)_559 {_559 $abstract = $parameter->name;_559 // 通过 bind 获取_559 if ($this->hasBinding('$' . $parameter->name)) {_559 $abstract = '$' . $parameter->name;_559 }_559 // 匹配别名_559 if ($this->isAlias($abstract)) {_559 $abstract = $this->getAbstractByAlias($abstract);_559 }_559 try {_559 $concrete = $this->getBinding($abstract)['concrete'];_559 } catch (RuntimeException $e) {_559 $concrete = null;_559 }_559 if ($concrete !== null) {_559 return $concrete instanceof Closure ? $concrete($this) : $concrete;_559 }_559 if ($parameter->isDefaultValueAvailable()) {_559 return $parameter->getDefaultValue();_559 }_559 throw new RuntimeException("Target [$$parameter->name] is not binding");_559 }_559_559 /**_559 * 处理类依赖_559 *_559 * @param ReflectionParameter $parameter_559 *_559 * @return mixed_559 */_559 protected function resolveClass(ReflectionParameter $parameter)_559 {_559 try {_559 return $this->make($parameter->getClass()->name);_559 } catch (RuntimeException $e) {_559 if ($parameter->isDefaultValueAvailable()) {_559 return $parameter->getDefaultValue();_559 }_559 throw $e;_559 }_559 }_559_559 /**_559 * 设置自动绑定_559 *_559 * @param bool $use 是否自动绑定类_559 *_559 * @return void_559 */_559 public function useAutoBind(bool $use): void_559 {_559 $this->autobind = $use;_559 }_559_559 /**_559 * 判断是否绑定了指定的依赖_559 *_559 * @param $id_559 * @return bool_559 */_559 public function has($id): bool_559 {_559 return $this->hasBinding($id);_559 }_559_559 /**_559 * 同 make_559 *_559 * @param string $id 对象名称_559 *_559 * @return mixed_559 */_559 public function get($id)_559 {_559 return $this->make($id);_559 }_559_559 public function hasMethod(string $method): bool_559 {_559 return isset($this->methodBindings[$method]);_559 }_559_559 public function bindMethod(string $method, $callback): void_559 {_559 $this->methodBindings[$method] = $callback;_559 }_559_559 protected function getMethodBind(string $method)_559 {_559 if (isset($this->methodBindings[$method])) {_559 return $this->methodBindings[$method];_559 }_559 throw new RuntimeException("Target [$method] is not binding");_559 }_559_559 public function call(_559 $method,_559 array $args = [],_559 $object = null,_559 $isStatic = false_559 ) {_559 if ($object !== null) {_559 return $this->callMethod($object, $method, $isStatic, $args);_559 }_559 if (_559 is_array($method) ||_559 (is_string($method) && preg_match('/@|::/', $method) > 0)_559 ) {_559 return $this->callClass($method, $args);_559 }_559 if (is_string($method)) {_559 $method = $this->getMethodBind($method);_559 }_559 return $this->callFunction($method, $args);_559 }_559_559 protected function callFunction($method, array $args = [])_559 {_559 $reflector = new ReflectionFunction($method);_559 $dependency = $this->injectingDependencies($reflector, $args);_559 return $reflector->invokeArgs($dependency);_559 }_559_559 /**_559 * @param string|array $target_559 * @param array $args_559 * @return mixed_559 */_559 protected function callClass($target, array $args = [])_559 {_559 $class = null;_559 $method = null;_559 $object = null;_559 $isStatic = false;_559 if (is_string($target)) {_559 $isStatic = strpos($target, '@') === false;_559 [$class, $method] = str_parse_callback($target);_559 $object = $this->bindAndMakeReflection($class, $isStatic);_559 } else {_559 if (count($target) === 3) {_559 [$class, $split, $method] = $target;_559 $isStatic = $split === '::';_559 } else {_559 [$class, $method] = $target;_559 }_559 $object = $this->bindAndMakeReflection($class, $isStatic);_559 }_559 return $this->callMethod($object, $method, $isStatic, $args);_559 }_559_559 protected function bindAndMakeReflection(_559 string $class,_559 bool $isStatic = false_559 ) {_559 if ($isStatic) {_559 return $class;_559 }_559 if (!$this->has($class)) {_559 $this->bind($class);_559 }_559 return $this->make($class);_559 }_559_559 protected function callMethod(_559 $object,_559 $method,_559 $isStatic = false,_559 array $args = []_559 ) {_559 $reflector = new ReflectionMethod($object, $method);_559 $dependency = $this->injectingDependencies($reflector, $args);_559 return $reflector->invokeArgs($isStatic ? null : $object, $dependency);_559 }_559_559 public function isAlias(string $name): bool_559 {_559 return isset($this->aliases[$name]);_559 }_559_559 public function alias(string $abstract, string $alias): void_559 {_559 if ($abstract === $alias) {_559 return;_559 }_559 $this->aliases[$alias] = $abstract;_559 }_559_559 public function getAlias($abstract)_559 {_559 foreach ($this->aliases as $alias => $value) {_559 if ($value === $abstract) {_559 return $alias;_559 }_559 }_559 return $abstract;_559 }_559_559 public function removeAlias($alias): void_559 {_559 unset($this->aliases[$alias]);_559 }_559_559 protected function getAbstractByAlias($alias)_559 {_559 return $this->aliases[$alias] ?? $alias;_559 }_559_559 protected function getAbstractAndAliasByAlias(_559 $alias,_559 $inAlias = false_559 ): array {_559 $abstract = $this->getAbstractByAlias($alias);_559 if ($alias === $abstract) {_559 return [$abstract, $inAlias];_559 }_559 if (!$inAlias) {_559 return [$abstract, $alias];_559 }_559 return [$abstract, $inAlias];_559 }_559}
测试
完成以上步骤后就可以测试下容器是否可以正常工作了,首先创建几个测试类:
_59<?php_59_59namespace App\Entry;_59_59// app/Entry/Cat.php_59class Cat_59{_59 public function name(): string_59 {_59 return "Cat";_59 }_59}_59_59// app/Entry/Dog.php_59class Dog_59{_59 public function name(): string_59 {_59 return "Dog";_59 }_59}_59_59// app/Entry/CatShop.php_59class CatShop_59{_59 /**_59 * @var Cat_59 */_59 protected $cat;_59_59 public function __construct(Cat $cat)_59 {_59 $this->cat = $cat;_59 }_59_59 public function getName(): string_59 {_59 return $this->cat->name();_59 }_59}_59_59// app/Entry/DogShop.php_59class DogShop_59{_59 /**_59 * @var Dog_59 */_59 protected $dog;_59_59 public function __construct(Dog $dog)_59 {_59 $this->dog = $dog;_59 }_59_59 public function getName(): string_59 {_59 return $this->dog->name();_59 }_59}
然后修改 public/index.php
文件,把之前 Test 相关的代码都删了,然后添加以下代码:
_18<?php_18// public/index.php_18_18$container = new Container();_18_18$container->bind(Cat::class, null, 'cat');_18$container->bind(Dog::class, null, 'dog');_18_18$container->singleton(CatShop::class);_18$container->singleton(DogShop::class);_18_18/* @var CatShop $cat_shop */_18$cat_shop = $container->make(CatShop::class);_18/* @var DogShop $dog_shop */_18$dog_shop = $container->make(DogShop::class);_18_18echo $cat_shop->getName() . "\n"; // Cat_18echo $dog_shop->getName() . "\n"; // Dog
添加完毕后就可以进行测试了,运行 index.php
:

可以看到,我们并没有写赋值 Cat
和 Dog
的代码,按理使用的时候应该为 null
,而 CatShop
和 DogShop
却可以正常使用,这是因为 IoC 容器中为我们完成了赋值的工作,我们只需要关心需要使用什么而不需要关心依赖是如何来的,这样就可以很好的解耦代码,同时也简化了代码的编写。
结语
结语。。。实在不知道写什么了 ?。
从零实现一个 PHP 微框架 - IoC 容器
https://blog.ixk.me/post/implement-a-php-microframework-from-zero-3许可协议
发布于
2020-05-13
本文作者
Otstar Lin
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!