言午月月鸟
编程,带娃以及思考人生
首页
编程
带娃
思考人生
编程画图秀
PHP系统解析-容器
dingusxp
2654
## 理论篇 ### 是什么? 通俗点,就是存放东西(主要是对象)的东西。 高大上点,是一个优雅的资源管理器和超级工厂。 PHP容器被广泛认知,主要得益于 laravel 的兴起。ioc容器是laravel的核心,也是PHP应用设计模式的集大成者。 伴随着容器常有的关键词是:`解耦`,`依赖注入`,`控制反转`。 单看概念不太好理解,我们用 [`talk is cheap, show me the code`](https://www.zhihu.com/question/23090743) 的方法解读,详见 “代码篇”。 ### PSR 规范 [PSR-11: Container interface](https://www.php-fig.org/psr/psr-11) 定义了一些关于容器的基本规范。 当前的定义比较简单,主要关注的只有: - 实现 ContainerInterface 定义的 has 和 get 方法; - 当要获取的资源不存在时,应该抛出一个 NotFoundExceptionInterface 的异常 。 仅供参考,实际项目中容器提供的方法,一般比这个丰富的多。 ## 代码篇 ### 案例 ```PHP redis = $redis; } public function set(string $key, $value, $ttl = null) { return $this->redis->set($key, serialize($value), $ttl ? intval($ttl) : null); } public function get(string $key, $default = null) { $value = $this->redis->get($key); return false === $value ? $default : unserialize($value); } } // 另一个用到 Cache 的组件:抓取 interface GrabberInterface { public function fetchUrl($url); } class SimpleGrabber implements GrabberInterface { private $cache = null; const CACHE_TIME = 300; public function __construct(CacheInterface $cache) { $this->cache = $cache; } public function fetchUrl($url) { $cacheKey = 'grabber:url:' . md5($url); $cacheData = $this->cache->get($cacheKey); if ($cacheData) { return $cacheData; } $data = file_get_contents($url); $this->cache->set($cacheKey, $data, self::CACHE_TIME); return $data; } } // 使用:无容器场景 $redis = new Redis(); $redis->connect('127.0.0.1'); $cache = new RedisCache($redis); $grabber = new SimpleGrabber($cache); $url = 'https://dingusxp.com'; echo $grabber->fetchUrl($url); ``` 说明:上面代码中 RedisCache 和 SimpleGrabber 就是 `依赖注入` 的方式。原本应该自己构造函数里去初始化的对象,现在只定义类接口,交由外部初始化好传递进来。 代码看起来 `new` 的有点多,运行起来也没啥问题。 但有几个点可以思考一下: - 随着系统规模更大,组件相互依赖更多,使用一个功能,可能要new的数目会继续增加 - 假如RedisCache类的参数发生了变化(比如增加了Logger组件的依赖),调用的地方都要修改 - 假如GrabberInterface有了一个更好的实现(如 CurlGrabber),调用的地方也都要修改 ### 简易容器 ```PHP // 看看引入一个 简单容器 的变化 class ContainerException extends Exception {} class Container { protected $binds = []; protected $instances = []; public function bind($id, $concrete) { if ($concrete instanceof \Closure) { $this->binds[$id] = $concrete; } else { $this->instances[$id] = $concrete; } } public function has($id) { return !empty($this->binds[$id]) ? true : (!empty($this->instances[$id]) ? true : false); } public function get($id, $params = []) { if (!$this->has($id)) { throw new ContainerException('entry was not found:' . $id); } if (isset($this->instances[$id])) { return $this->instances[$id]; } array_unshift($params, $this); return call_user_func_array($this->binds[$id], $params); } } // 配置 $redisInstance = new Redis(); $redisInstance->connect('127.0.0.1'); $containerConfig = [ Redis::class => $redisInstance, CacheInterface::class => function(Container $c) { return new RedisCache($c->get(Redis::class)); }, GrabberInterface::class => function(Container $c) { return new SimpleGrabber($c->get(CacheInterface::class)); } ]; $container = new Container(); foreach ($containerConfig as $abstract => $concrete) { $container->bind($abstract, $concrete); } // 使用 $grabber = $container->get(GrabberInterface::class); $url = 'https://dingusxp.com'; echo $grabber->fetchUrl($url); ``` 使用方也不用去构造实例了,交由容器处理,这就是 `控制反转`。 用起来更简单了。 当然,更重要的还是思想:有一个地方专门管理各种 “资源”(`解耦`),使用上心智负担减轻了。 ### 改进 #### 绑定方法支持类名方式 上面实例中 CacheInterface 和 GrabberInterface 绑定的方法,还是手动构造的,有一定维护成本。 借助于反射,我们可以自动构造。参见下例中 bindByClass 方法。 ```PHP class Container { protected $binds = []; protected $instances = []; public function bind($id, $concrete) { if ($concrete instanceof \Closure) { // 自定义函数 $this->binds[$id] = $concrete; } elseif (is_string($concrete) && class_exists($concrete)) { // 类反射 $this->bindByClass($id, $concrete); } else { // 直接设置 $this->instances[$id] = $concrete; } } private function bindByClass($id, $class) { if (!class_exists($class)) { return false; } $classRef = new ReflectionClass($class); // 判断是否能实例化 if (!$classRef->isInstantiable()) { return false; } $constructor = $classRef->getConstructor(); // 如果没有构造函数,可直接实例化 if (!$constructor) { $this->bind($id, function() use ($class) { return new $class(); }); return true; } // 根据参数初始化 $paramRef = $constructor->getParameters(); $this->bind($id, function() use ($classRef, $paramRef) { $args = func_get_args(); $container = array_shift($args); $params = []; foreach ($paramRef as $varRef) { // get 方法传了参数,优先使用 if (count($args)) { $params[] = array_shift($args); continue; } // 参数为 类,使用容器 get $varClassRef = $varRef->getClass(); if ($varClassRef) { $params[] = $container->get($varClassRef->getName()); continue; } // 无法自动填充参数,看是否有默认值 if ($varRef->isDefaultValueAvailable()) { $params[] = $varRef->getDefaultValue(); continue; } // 异常: 必须参数未设置 throw new ContainerException('required parameter was missing: '.$varRef->getName()); } return $classRef->newInstanceArgs($params); }); return true; } // 略 } // 配置更简单明了了 $container->bind(CacheInterface::class, RedisCache::class); $container->bind(GrabberInterface::class, SimpleGrabber::class); ``` #### 扩展更多方法 满足各种使用场景。如 laravel 的容器包含这些能力:(图来自参考文章 Laravel之容器) ![](https://public-pkg-1252772859.cos.ap-guangzhou.myqcloud.com/dingusxp-blog/misc/container.png) 参考文章: [PHP通过反射实现自动注入参数](https://blog.csdn.net/weixin_34194317/article/details/89002547) [Laravel之容器](https://www.jianshu.com/p/b7edc9b22b8a)
粤ICP备19051469号-1
Copyright©dingusxp.com - All Rights Reserved
Template by
OS Templates