

从零实现一个 PHP 微框架 - IoC 容器
source link: https://blog.ixk.me/post/implement-a-php-microframework-from-zero-3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

差不多该写写该系列文章了,咕了好几天 ?。
在 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 自动导入并且全局生效:
1<?php 2// functions.php 3 4// 解析 class@method 的字符串,返回 [class, method] 数组 5function str_parse_callback($callback, $default = null) 6{ 7 if (is_array($callback)) { 8 return $callback; 9 } 10 if (strpos($callback, '@') !== false) { 11 return explode('@', $callback); 12 } 13 if (strpos($callback, '::') !== false) { 14 return explode('::', $callback); 15 } 16 return [$callback, $default]; 17} 18 19// 解析 [class, method] 或 [class, split, method] 到 class@method 或 class::method 20function str_stringify_callback( 21 $callback, 22 $default = null, 23 bool $isStatic = false 24) { 25 $split = $isStatic ? '::' : '@'; 26 if (is_array($callback)) { 27 return implode($split, $callback); 28 } 29 if (preg_match('/@|::/', $callback) > 0) { 30 return $callback; 31 } 32 if ($default === null) { 33 return $callback; 34 } 35 return "{$callback}{$split}{$default}"; 36}
1{ 2 "autoload": { 3 "psr-4": { 4 "App\\": "app/" 5 }, 6 // 新增 7 "files": ["app/Helper/functions.php"] 8 } 9}
修改了 composer.json
的 autoload
后需要运行以下命令后才能生效:
1composer dump-auto
然后就可以写容器的代码了,首先创建 app/Kernel/Container.php
的文件,输入以下代码,本文的容器代码和之前的文章中的容器不一样,但是流程是一样的:
1<?php 2 3namespace App\Kernel; 4 5use Closure; 6use Psr\Container\ContainerInterface; 7use ReflectionClass; 8use ReflectionException; 9use ReflectionFunction; 10use ReflectionMethod; 11use ReflectionParameter; 12use RuntimeException; 13use function array_pad; 14use function class_exists; 15use function compact; 16use function count; 17use function is_array; 18use function is_bool; 19use function is_int; 20use function is_string; 21use function preg_match; 22use function str_parse_callback; 23use function strpos; 24 25/** 26 * IoC 容器,兼容 PSR-11 27 */ 28class Container implements ContainerInterface 29{ 30 /** 31 * 容器中存储依赖的数组 32 * 存储的是闭包,运行闭包会返回对应的依赖实例 33 * 34 * @var array 35 */ 36 protected $bindings = []; 37 38 /** 39 * 绑定方法 40 * 41 * @var array 42 */ 43 protected $methodBindings = []; 44 45 /** 46 * 已创建的单例实例 47 * 48 * @var array 49 */ 50 protected $instances = []; 51 52 /** 53 * 自动通过类名绑定类 54 * 55 * @var bool 56 */ 57 protected $autobind = true; 58 59 /** 60 * 依赖别名 61 * 62 * @var string[] 63 */ 64 protected $aliases = []; 65 66 /** 67 * 绑定依赖 68 * 69 * @param string|array $abstract 依赖名或者依赖列表 70 * @param Closure|string|null $concrete 依赖闭包 71 * 72 * @param bool $shared 73 * @param bool|string $alias 74 * @param bool $overwrite 75 * @return Container 76 */ 77 public function bind( 78 $abstract, 79 $concrete = null, 80 bool $shared = false, 81 $alias = false, 82 bool $overwrite = false 83 ): Container { 84 // 同时绑定多个依赖 85 if (is_array($abstract)) { 86 foreach ($abstract as $_abstract => $value) { 87 if (is_int($_abstract)) { 88 $_abstract = $value; 89 } 90 $_concrete = null; 91 $_shared = false; 92 $_alias = false; 93 $_overwrite = false; 94 if (is_bool($value)) { 95 $_shared = $value; 96 } elseif (is_array($value)) { 97 [$_concrete, $_shared, $_alias, $_overwrite] = array_pad( 98 $value, 99 3, 100 false 101 ); 102 } 103 $this->bind( 104 $_abstract, 105 $_concrete === false ? null : $_concrete, 106 $_shared, 107 $_alias, 108 $_overwrite 109 ); 110 } 111 return $this; 112 } 113 [$abstract, $alias] = $this->getAbstractAndAliasByAlias( 114 $abstract, 115 $alias 116 ); 117 // 为了方便绑定依赖,可以节省一个参数 118 if ($concrete === null) { 119 $concrete = $abstract; 120 } 121 $this->setBinding($abstract, $concrete, $shared, $overwrite); 122 if ($alias) { 123 $this->alias($abstract, $alias); 124 } 125 // 返回 this 使其支持链式调用 126 return $this; 127 } 128 129 // 设置 binding 130 protected function setBinding( 131 string $abstract, 132 $concrete, 133 bool $shared = false, 134 bool $overwrite = false 135 ): void { 136 $abstract = $this->getAbstractByAlias($abstract); 137 // 传入的默认是闭包,如果没有传入闭包则默认创建 138 if (!$concrete instanceof Closure) { 139 $concrete = function (Container $c, array $args = []) use ( 140 $concrete 141 ) { 142 return $c->build($concrete, $args); 143 }; 144 } 145 // 判断是否是单例,是否被设置过 146 if (!$overwrite && $shared && isset($this->bindings[$abstract])) { 147 throw new RuntimeException( 148 "Target [$abstract] is a singleton and has been bind" 149 ); 150 } 151 // 设置绑定的闭包 152 $this->bindings[$abstract] = compact('concrete', 'shared'); 153 } 154 155 // 获取 binding 156 protected function getBinding(string $abstract) 157 { 158 $abstract = $this->getAbstractByAlias($abstract); 159 if (!isset($this->bindings[$abstract])) { 160 // 尝试自动绑定 161 if ( 162 $this->autobind && 163 $abstract[0] !== '$' && 164 class_exists($abstract) 165 ) { 166 $this->setBinding($abstract, $abstract); 167 } else { 168 throw new RuntimeException( 169 "Target [$abstract] is not binding or fail autobind" 170 ); 171 } 172 } 173 return $this->bindings[$abstract]; 174 } 175 176 // 判断 binding 是否存在 177 protected function hasBinding(string $abstract): bool 178 { 179 $abstract = $this->getAbstractByAlias($abstract); 180 return isset($this->bindings[$abstract]); 181 } 182 183 /** 184 * 实例化对象 185 * 186 * @param string $abstract 对象名称 187 * @param array $args 188 * 189 * @return mixed 190 */ 191 public function make(string $abstract, array $args = []) 192 { 193 $abstract = $this->getAbstractByAlias($abstract); 194 $binding = $this->getBinding($abstract); 195 $concrete = $binding['concrete']; 196 $shared = $binding['shared']; 197 // 判断是否是单例,若是单例并且已经实例化过就直接返回实例 198 if ($shared && isset($this->instances[$abstract])) { 199 return $this->instances[$abstract]; 200 } 201 // 构建实例 202 $instance = $concrete($this, $args); 203 // 判断是否是单例,若是则设置到容器的单例列表中 204 if ($shared) { 205 $this->instances[$abstract] = $instance; 206 } 207 return $instance; 208 } 209 210 /** 211 * 绑定单例 212 * 213 * @param string $abstract 依赖名称 214 * @param mixed $concrete 依赖闭包 215 * @param bool|string $alias 216 * 217 * @param bool $overwrite 218 * @return Container 219 */ 220 public function singleton( 221 string $abstract, 222 $concrete = null, 223 $alias = false, 224 bool $overwrite = false 225 ): Container { 226 $this->bind($abstract, $concrete, true, $alias, $overwrite); 227 return $this; 228 } 229 230 /** 231 * 绑定已实例化的单例 232 * 233 * @param string $abstract 依赖名称 234 * @param mixed $instance 已实例化的单例 235 * @param string|false $alias 236 * 237 * @param bool $overwrite 238 * @return Container 239 */ 240 public function instance( 241 string $abstract, 242 $instance, 243 $alias = false 244 ): Container { 245 [$abstract, $alias] = $this->getAbstractAndAliasByAlias( 246 $abstract, 247 $alias 248 ); 249 $this->instances[$abstract] = $instance; 250 $this->bind( 251 $abstract, 252 function () use ($instance) { 253 return $instance; 254 }, 255 true, 256 $alias, 257 true 258 ); 259 return $this; 260 } 261 262 /** 263 * 构建实例 264 * 265 * @param Closure|string $class 类名或者闭包 266 * @param array $args 267 * @return mixed 268 * 269 * @throws ReflectionException 270 */ 271 public function build($class, array $args = []) 272 { 273 if ($class instanceof Closure) { 274 return $class($this, $args); 275 } 276 if (!class_exists($class)) { 277 return $class; 278 } 279 // 取得反射类 280 $reflector = new ReflectionClass($class); 281 // 检查类是否可实例化 282 if (!$reflector->isInstantiable()) { 283 // 如果不能,意味着接口不能正常工作,报错 284 throw new RuntimeException("Target [$class] is not instantiable"); 285 } 286 // 取得构造函数 287 $constructor = $reflector->getConstructor(); 288 // 检查是否有构造函数 289 if ($constructor === null) { 290 // 如果没有,就说明没有依赖,直接实例化 291 $instance = new $class(); 292 } else { 293 // 返回已注入依赖的参数数组 294 $dependency = $this->injectingDependencies($constructor, $args); 295 // 利用注入后的参数创建实例 296 $instance = $reflector->newInstanceArgs($dependency); 297 } 298 return $instance; 299 } 300 301 /** 302 * 注入依赖 303 * 304 * @param ReflectionFunction|ReflectionMethod $method 305 * @param array $args 306 * 307 * @return array 308 */ 309 protected function injectingDependencies($method, array $args = []): array 310 { 311 $dependency = []; 312 $parameters = $method->getParameters(); 313 foreach ($parameters as $parameter) { 314 if (isset($args[$parameter->name])) { 315 $dependency[] = $args[$parameter->name]; 316 continue; 317 } 318 // 利用参数的类型声明,获取到参数的类型,然后从 bindings 中获取依赖注入 319 $dependencyClass = $parameter->getClass(); 320 if ($dependencyClass === null) { 321 $dependency[] = $this->resolvePrimitive($parameter); 322 } else { 323 // 实例化依赖 324 $dependency[] = $this->resolveClass($parameter); 325 } 326 } 327 return $dependency; 328 } 329 330 /** 331 * 处理非类的依赖 332 * 333 * @param ReflectionParameter $parameter 334 * 335 * @return mixed 336 */ 337 protected function resolvePrimitive(ReflectionParameter $parameter) 338 { 339 $abstract = $parameter->name; 340 // 通过 bind 获取 341 if ($this->hasBinding('$' . $parameter->name)) { 342 $abstract = '$' . $parameter->name; 343 } 344 // 匹配别名 345 if ($this->isAlias($abstract)) { 346 $abstract = $this->getAbstractByAlias($abstract); 347 } 348 try { 349 $concrete = $this->getBinding($abstract)['concrete']; 350 } catch (RuntimeException $e) { 351 $concrete = null; 352 } 353 if ($concrete !== null) { 354 return $concrete instanceof Closure ? $concrete($this) : $concrete; 355 } 356 if ($parameter->isDefaultValueAvailable()) { 357 return $parameter->getDefaultValue(); 358 } 359 throw new RuntimeException("Target [$$parameter->name] is not binding"); 360 } 361 362 /** 363 * 处理类依赖 364 * 365 * @param ReflectionParameter $parameter 366 * 367 * @return mixed 368 */ 369 protected function resolveClass(ReflectionParameter $parameter) 370 { 371 try { 372 return $this->make($parameter->getClass()->name); 373 } catch (RuntimeException $e) { 374 if ($parameter->isDefaultValueAvailable()) { 375 return $parameter->getDefaultValue(); 376 } 377 throw $e; 378 } 379 } 380 381 /** 382 * 设置自动绑定 383 * 384 * @param bool $use 是否自动绑定类 385 * 386 * @return void 387 */ 388 public function useAutoBind(bool $use): void 389 { 390 $this->autobind = $use; 391 } 392 393 /** 394 * 判断是否绑定了指定的依赖 395 * 396 * @param $id 397 * @return bool 398 */ 399 public function has($id): bool 400 { 401 return $this->hasBinding($id); 402 } 403 404 /** 405 * 同 make 406 * 407 * @param string $id 对象名称 408 * 409 * @return mixed 410 */ 411 public function get($id) 412 { 413 return $this->make($id); 414 } 415 416 public function hasMethod(string $method): bool 417 { 418 return isset($this->methodBindings[$method]); 419 } 420 421 public function bindMethod(string $method, $callback): void 422 { 423 $this->methodBindings[$method] = $callback; 424 } 425 426 protected function getMethodBind(string $method) 427 { 428 if (isset($this->methodBindings[$method])) { 429 return $this->methodBindings[$method]; 430 } 431 throw new RuntimeException("Target [$method] is not binding"); 432 } 433 434 public function call( 435 $method, 436 array $args = [], 437 $object = null, 438 $isStatic = false 439 ) { 440 if ($object !== null) { 441 return $this->callMethod($object, $method, $isStatic, $args); 442 } 443 if ( 444 is_array($method) || 445 (is_string($method) && preg_match('/@|::/', $method) > 0) 446 ) { 447 return $this->callClass($method, $args); 448 } 449 if (is_string($method)) { 450 $method = $this->getMethodBind($method); 451 } 452 return $this->callFunction($method, $args); 453 } 454 455 protected function callFunction($method, array $args = []) 456 { 457 $reflector = new ReflectionFunction($method); 458 $dependency = $this->injectingDependencies($reflector, $args); 459 return $reflector->invokeArgs($dependency); 460 } 461 462 /** 463 * @param string|array $target 464 * @param array $args 465 * @return mixed 466 */ 467 protected function callClass($target, array $args = []) 468 { 469 $class = null; 470 $method = null; 471 $object = null; 472 $isStatic = false; 473 if (is_string($target)) { 474 $isStatic = strpos($target, '@') === false; 475 [$class, $method] = str_parse_callback($target); 476 $object = $this->bindAndMakeReflection($class, $isStatic); 477 } else { 478 if (count($target) === 3) { 479 [$class, $split, $method] = $target; 480 $isStatic = $split === '::'; 481 } else { 482 [$class, $method] = $target; 483 } 484 $object = $this->bindAndMakeReflection($class, $isStatic); 485 } 486 return $this->callMethod($object, $method, $isStatic, $args); 487 } 488 489 protected function bindAndMakeReflection( 490 string $class, 491 bool $isStatic = false 492 ) { 493 if ($isStatic) { 494 return $class; 495 } 496 if (!$this->has($class)) { 497 $this->bind($class); 498 } 499 return $this->make($class); 500 } 501 502 protected function callMethod( 503 $object, 504 $method, 505 $isStatic = false, 506 array $args = [] 507 ) { 508 $reflector = new ReflectionMethod($object, $method); 509 $dependency = $this->injectingDependencies($reflector, $args); 510 return $reflector->invokeArgs($isStatic ? null : $object, $dependency); 511 } 512 513 public function isAlias(string $name): bool 514 { 515 return isset($this->aliases[$name]); 516 } 517 518 public function alias(string $abstract, string $alias): void 519 { 520 if ($abstract === $alias) { 521 return; 522 } 523 $this->aliases[$alias] = $abstract; 524 } 525 526 public function getAlias($abstract) 527 { 528 foreach ($this->aliases as $alias => $value) { 529 if ($value === $abstract) { 530 return $alias; 531 } 532 } 533 return $abstract; 534 } 535 536 public function removeAlias($alias): void 537 { 538 unset($this->aliases[$alias]); 539 } 540 541 protected function getAbstractByAlias($alias) 542 { 543 return $this->aliases[$alias] ?? $alias; 544 } 545 546 protected function getAbstractAndAliasByAlias( 547 $alias, 548 $inAlias = false 549 ): array { 550 $abstract = $this->getAbstractByAlias($alias); 551 if ($alias === $abstract) { 552 return [$abstract, $inAlias]; 553 } 554 if (!$inAlias) { 555 return [$abstract, $alias]; 556 } 557 return [$abstract, $inAlias]; 558 } 559}
完成以上步骤后就可以测试下容器是否可以正常工作了,首先创建几个测试类:
1<?php 2 3namespace App\Entry; 4 5// app/Entry/Cat.php 6class Cat 7{ 8 public function name(): string 9 { 10 return "Cat"; 11 } 12} 13 14// app/Entry/Dog.php 15class Dog 16{ 17 public function name(): string 18 { 19 return "Dog"; 20 } 21} 22 23// app/Entry/CatShop.php 24class CatShop 25{ 26 /** 27 * @var Cat 28 */ 29 protected $cat; 30 31 public function __construct(Cat $cat) 32 { 33 $this->cat = $cat; 34 } 35 36 public function getName(): string 37 { 38 return $this->cat->name(); 39 } 40} 41 42// app/Entry/DogShop.php 43class DogShop 44{ 45 /** 46 * @var Dog 47 */ 48 protected $dog; 49 50 public function __construct(Dog $dog) 51 { 52 $this->dog = $dog; 53 } 54 55 public function getName(): string 56 { 57 return $this->dog->name(); 58 } 59}
然后修改 public/index.php
文件,把之前 Test 相关的代码都删了,然后添加以下代码:
1<?php 2// public/index.php 3 4$container = new Container(); 5 6$container->bind(Cat::class, null, 'cat'); 7$container->bind(Dog::class, null, 'dog'); 8 9$container->singleton(CatShop::class); 10$container->singleton(DogShop::class); 11 12/* @var CatShop $cat_shop */ 13$cat_shop = $container->make(CatShop::class); 14/* @var DogShop $dog_shop */ 15$dog_shop = $container->make(DogShop::class); 16 17echo $cat_shop->getName() . "\n"; // Cat 18echo $dog_shop->getName() . "\n"; // Dog
添加完毕后就可以进行测试了,运行 index.php
:
可以看到,我们并没有写赋值 Cat
和 Dog
的代码,按理使用的时候应该为 null
,而 CatShop
和 DogShop
却可以正常使用,这是因为 IoC 容器中为我们完成了赋值的工作,我们只需要关心需要使用什么而不需要关心依赖是如何来的,这样就可以很好的解耦代码,同时也简化了代码的编写。
结语。。。实在不知道写什么了 ?。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK