站点图标

从零实现一个 PHP 微框架 - IoC 容器

2020-05-13折腾记录PHP / XK-PHP / 从零实现
本文最后更新于 410 天前,文中所描述的信息可能已发生改变

前言

差不多该写写该系列文章了,咕了好几天 ?。

在 XK-PHP 中 IoC 容器是框架的核心,其掌管着框架中实例的存储和初始化,并提供自动依赖注入等功能,我们可以把 IoC 容器看成一个拥有存储功能的工厂,当我们需要某个实例的时候,工厂会依靠需求将实例组装好并返回给需求者,如果实例是单例的,那么制作好的实例就可以存到仓库中,当需求者再次需要的时候就可以直接返回实例。需求者无需关心实例是如何制造的,只需要将需求提交给工厂即可。这看起来似乎就是工厂模式?IoC 容器和 工厂模式 很类似,但是工厂模式注入的依赖是定死的,而 IoC 容器可以依据需求按需注入依赖。

DI & IoC

由于我之前写过 DI 和 IoC 的介绍文章,这里就不重复写了,链接见下方:

IoC 容器

由于之前的文章已经说明了 IoC 容器的实现了,这里就不再讲解 IoC 容器内部的细节了,本文就只讲述将 IoC 容器集成到我们上次创建的项目之中。

首先,因为我们的容器需要兼容 PSR-11 ,那么就需要引入 psr/container 的包,来引入 ContainerInterface 接口:


_1
composer require psr/container

然后容器需要使用两个自定义函数,我们将其放到 app/Helper/functions.php 中,并修改 composer.json,使函数能被 Composer 自动导入并且全局生效


_36
<?php
_36
// functions.php
_36
_36
// 解析 class@method 的字符串,返回 [class, method] 数组
_36
function 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
_36
function 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.jsonautoload 后需要运行以下命令后才能生效


_1
composer dump-auto

然后就可以写容器的代码了,首先创建 app/Kernel/Container.php 的文件,输入以下代码,本文的容器代码和之前的文章中的容器不一样,但是流程是一样的:


_559
<?php
_559
_559
namespace App\Kernel;
_559
_559
use Closure;
_559
use Psr\Container\ContainerInterface;
_559
use ReflectionClass;
_559
use ReflectionException;
_559
use ReflectionFunction;
_559
use ReflectionMethod;
_559
use ReflectionParameter;
_559
use RuntimeException;
_559
use function array_pad;
_559
use function class_exists;
_559
use function compact;
_559
use function count;
_559
use function is_array;
_559
use function is_bool;
_559
use function is_int;
_559
use function is_string;
_559
use function preg_match;
_559
use function str_parse_callback;
_559
use function strpos;
_559
_559
/**
_559
* IoC 容器,兼容 PSR-11
_559
*/
_559
class 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
_59
namespace App\Entry;
_59
_59
// app/Entry/Cat.php
_59
class Cat
_59
{
_59
public function name(): string
_59
{
_59
return "Cat";
_59
}
_59
}
_59
_59
// app/Entry/Dog.php
_59
class Dog
_59
{
_59
public function name(): string
_59
{
_59
return "Dog";
_59
}
_59
}
_59
_59
// app/Entry/CatShop.php
_59
class 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
_59
class 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
_18
echo $cat_shop->getName() . "\n"; // Cat
_18
echo $dog_shop->getName() . "\n"; // Dog

添加完毕后就可以进行测试了,运行 index.php

可以看到,我们并没有写赋值 CatDog 的代码,按理使用的时候应该为 null,而 CatShopDogShop 却可以正常使用,这是因为 IoC 容器中为我们完成了赋值的工作,我们只需要关心需要使用什么而不需要关心依赖是如何来的,这样就可以很好的解耦代码,同时也简化了代码的编写。

结语

结语。。。实在不知道写什么了 ?。

从零实现一个 PHP 微框架 - IoC 容器

https://blog.ixk.me/post/implement-a-php-microframework-from-zero-3
  • 许可协议

    BY-NC-SA

  • 发布于

    2020-05-13

  • 本文作者

    Otstar Lin

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

从零实现一个 PHP 微框架 - Bootstrap 启动加载从零实现一个 PHP 微框架 - PSR & Composer