开源技术小栈本文导读: 随着项目规模的扩大,管理类之间的依赖关系可能成为一项重大挑战。可以使用
IoC (Inversion of control)
容器来解决此问题。容器控制依赖项的注入,并充当一个层,您可以在需要时将使用它。几乎所有现代 PHP 框架(如 Laravel 和 Drupal)都使用 IoC 容器。本教程将教您构建 IoC 容器背后的基本概念,并向您介绍反射 PHP 中最强大的功能之一。
容器应该是单例实例,并在管理依赖项时充当单一事实来源。由于静态函数是全局的,因此它们可用于每次创建和返回相同的容器实例。
class Container
{
/**
* 获取Container的单例实例
* 这个方法确保了一个容器实例的全局访问点
* @return Container
*/
public static function instance(): ?Container
{
static $instance = null;
if ($instance === null) {
$instance = new static();
}
return $instance;
}
}
$container = Container::instance(); // creates the singleton container instance
$container = Container::instance(); // returns the same singleton container instance
在容器内部,我们有一个Binding
数组,它映射了 2 个不同的事物。
[from-namespace]
=>[to-namespace]
的映射[singleton-namespace]
=>[singleton-instance]
的映射在解析类构造函数和类方法的依赖关系或调用resolve
方法时,将检查此映射。
class Container
{
// 存储绑定的服务标识和对应的命名空间或实例
protected array $bindings = [];
/**
* 绑定一个服务标识到一个命名空间
* 这个方法允许容器知道当请求一个服务时应该实例化哪个类
* @param string $id 服务的标识符
* @param string $namespace 对应的类的命名空间
* @return Container 返回容器实例,支持链式调用
*/
public function bind(string $id, string $namespace): Container
{
$this->bindings[$id] = $namespace;
return $this;
}
/**
* 绑定一个服务标识到一个已存在的实例
* 这个方法用于当需要在整个应用中共享一个单一实例时
* @param string $id 服务的标识符
* @param object $instance 已存在的实例
* @return Container 返回容器实例,支持链式调用
*/
public function singleton(string $id, object $instance): Container
{
$this->bindings[$id] = $instance;
return $this;
}
}
PHP 框架互操作组具有一组 PHP 标准建议 (PSR),并提供了一组基本接口,您可以使用这些接口创建符合标准且可移植的代码。
PSR-11:容器接口有容器可以实现的 2 个 get() 和 has() 方法。
PSR-11: Container interface :https://www.php-fig.org/psr/psr-11
这些方法检查bindings
数组中的条目并返回绑定值,无论它是命名空间还是单例实例。
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
/**
* 获取与服务标识绑定的实例或类
* 这个方法首先检查服务是否已绑定,如果没有找到则抛出异常
* @param mixed $id 服务的标识符
* @return mixed 绑定的实例或类
* @throws Exception 当服务标识未在容器中注册时
*/
public function get($id)
{
if ($this->has($id)) {
return $this->bindings[$id];
}
throw new Exception("Container entry not found for: {$id}");
}
/**
* 检查服务标识是否已绑定到容器
* @param mixed $id 服务的标识符
* @return bool 如果服务已绑定返回true,否则返回false
*/
public function has($id): bool
{
return array_key_exists($id, $this->bindings);
}
}
使用容器可以在需要时设置、获取和更新容器绑定,从而形成一种非常动态且强大的方式来轻松清除依赖项。
$container->bind(ConfigInterface::class, PHPConfig::class);
$container->get(ConfigInterface::class);
// returns PHPConfig namespace
$container->bind(ConfigInterface::class, YAMLConfig::class);
$container->get(ConfigInterface::class);
// returns YAMLConfig namespace
$container->bind(PHPConfig::class, YAMLConfig::class);
$container->get(PHPConfig::class);
// returns YAMLConfig namespace
单一实例绑定每次都返回相同的实例。
$container->singleton(PHPConfig::class, new PHPConfig());
$container->get(PHPConfig::class);
// returns singleton PHPConfig instance
$container->get(PHPConfig::class);
// returns the same singleton PHPConfig instance
如果容器用于初始化类实例和调用类方法,则可以为容器内的绑定排除这两者的依赖关系。
class App
{
public ConfigInterface $config;
public ConfigInterface $methodConfig;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
public function handle(ConfigInterface $config)
{
$this->methodConfig = $config;
}
}
$container->bind(ConfigInterface::class, PHPConfig::class);
// App constructor and handle method will receive an instance of PHPConfig
$instance = $container->resolve(App::class);
// resolves and injects the PHPConfig instance into the constructor
$value = $container->resolveMethod(new App, 'handle');
// resolves and injects the PHPConfig instance into the class method
容器需要 2 个方法来创建解析的类实例,并使用解析的依赖项调用类方法。这些的逻辑被抽象为这些方法中的ClassResolver
和MethodResolver
。这些类将容器作为参数,以便它们访问容器绑定。
class Container implements ContainerInterface
{
/**
* 解析一个类的实例
* 这个方法通过ClassResolver类来实例化一个类,并允许传递构造函数参数
* @param string $namespace 类的命名空间
* @param array $args 构造函数参数
* @return object 实例化的对象
* @throws NotFoundExceptionInterface
* @throws \ReflectionException
* @throws ContainerExceptionInterface
*/
public function resolve(string $namespace, array $args = []): object
{
return (new ClassResolver($this, $namespace, $args))->getInstance();
}
/**
* 解析并调用对象的方法
* 这个方法通过MethodResolver类来调用对象的某个方法,并允许传递方法参数
* @param object $instance 对象实例
* @param string $method 方法名
* @param array $args 方法参数
* @return mixed 方法调用的结果
* @throws \ReflectionException
*/
public function resolveMethod(object $instance, string $method, array $args = [])
{
return (new MethodResolver($this, $instance, $method, $args))->getValue();
}
}
对于未从容器解析的参数,还可以将其他参数传递到这些方法中。
class App
{
public ConfigInterface $config;
public string $arg1;
public string $arg2;
public function __construct(ConfigInterface $config, string $arg1, string $arg2)
{
$this->config = $config;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
$app = $container->resolve(App::class, [
'arg1' => 'value1',
'arg2' => 'value2'
]);
$value = $container->resolveMethod($app, 'handle', [
'arg1' => 'value1',
'arg2' => 'value2'
]);
// sets the arg values
也可以将类实例作为参数传入,而不从容器中解析这些依赖项。
$value = $container->resolveMethod($app, 'handle', [
'config' => new PHPConfig()
'arg1' => 'value1',
'arg2' => 'value2'
]);
Reflection
是 PHP 中的一个强大工具,它允许您检查类和函数,并在初始化或调用它们之前“查看”它们需要哪些参数。类解析程序负责检查类构造函数、获取参数并将其传递给ParametersResolver
。
一旦ParametersResolver
解析了它们,就会创建一个类实例并注入解析的依赖项。
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
/**
* ClassResolver 负责解析并创建类实例。
* 它使用依赖注入容器和命名空间来实例化类。
*/
class ClassResolver
{
protected ContainerInterface $container;
protected string $namespace;
protected array $args = [];
/**
* ClassResolver 构造函数。
*
* @param ContainerInterface $container 依赖注入容器接口,用于检索绑定的实例或命名空间。
* @param string $namespace 要解析的类的命名空间。
* @param array $args 构造函数参数,默认为空数组。
*/
public function __construct(ContainerInterface $container, string $namespace, array $args = [])
{
$this->container = $container;
$this->namespace = $namespace;
$this->args = $args;
}
/**
* 获取解析的类实例。
*
* 该方法首先检查容器中是否有当前命名空间的条目,
* 如果有,则尝试从容器中获取实例;如果容器条目不是实例,
* 则将命名空间更新为容器绑定的命名空间。
* 接下来,它尝试使用 ReflectionClass 创建类的实例,
* 如果类构造函数存在且是公共的,它会解析构造函数参数并创建实例;
* 如果没有构造函数或构造函数没有参数,则直接创建实例而不调用构造函数。
*
* @return object 返回创建的类实例。
* @throws NotFoundExceptionInterface
* @throws \ReflectionException
* @throws ContainerExceptionInterface
*/
public function getInstance(): object
{
// 检查容器中是否有当前命名空间的条目
if ($this->container->has($this->namespace)) {
$binding = $this->container->get($this->namespace);
// 如果容器中有实例或单例,则直接返回
if (is_object($binding)) {
return $binding;
}
// 将命名空间设置为容器绑定的命名空间
$this->namespace = $binding;
}
// 创建反射类
$refClass = new ReflectionClass($this->namespace);
// 获取构造函数
$constructor = $refClass->getConstructor();
// 检查构造函数是否存在且可访问
if ($constructor && $constructor->isPublic()) {
// 检查构造函数是否有参数并解析它们
if (count($constructor->getParameters()) > 0) {
$argumentResolver = new ParametersResolver(
$this->container,
$constructor->getParameters(),
$this->args
);
// 解析构造函数参数
$this->args = $argumentResolver->getArguments();
}
// 使用构造函数参数创建新实例
return $refClass->newInstanceArgs($this->args);
}
// 没有参数,直接创建新实例而不调用构造函数
return $refClass->newInstanceWithoutConstructor();
}
}
parameters resolver
的工作原理是遍历传递的函数 / 构造函数反射参数列表。如果其中一个参数是类类型,则找到该类,初始化该类并将其添加到返回的要注入的参数中。值得注意的是,这也是递归的。如果参数类需要注入的参数,则这些参数将在初始化类之前被解析和注入。
use Psr\Container\ContainerInterface;
use ReflectionParameter;
class ParametersResolver
{
protected ContainerInterface $container;
protected array $parameters;
protected array $args = [];
/**
* 构造函数,初始化依赖容器、参数列表和额外参数。
*
* @param ContainerInterface $container 依赖注入容器,用于解析类实例。
* @param array $parameters 参数列表,包含反射参数对象。
* @param array $args 额外参数,用于覆盖默认参数值或注入值。
*/
public function __construct(ContainerInterface $container, array $parameters, array $args = [])
{
$this->container = $container;
$this->parameters = $parameters;
$this->args = $args;
}
/**
* 获取并解析所有参数值。
*
* @return array 包含解析后的参数值的数组。
* @throws \ReflectionException
*/
public function getArguments(): array
{
// 遍历参数列表
return array_map(
function (ReflectionParameter $param) {
// 如果额外参数中存在该参数名称,则返回该值
if (array_key_exists($param->getName(), $this->args)) {
return $this->args[$param->getName()];
}
// 如果参数是类类型,则解析并返回类实例;否则返回默认值
return $param->getType() && !$param->getType()->isBuiltin()
? $this->getClassInstance($param->getType()->getName())
: $param->getDefaultValue();
},
$this->parameters
);
}
/**
* 根据命名空间解析并返回类实例。
*
* @param string $namespace 类的命名空间。
* @return object 类的实例。
*/
protected function getClassInstance(string $namespace): object
{
return (new ClassResolver($this->container, $namespace))->getInstance();
}
}
方法resolver
的工作方式类似于类resolver
,因为它调用ParametersResolver
来获取解析的参数。然后,它使用已解析的依赖项调用类实例方法。
use Psr\Container\ContainerInterface;
use ReflectionMethod;
/**
* MethodResolver 类负责解析并执行给定对象实例上的方法。
* 它使用依赖注入来解析方法参数。
*/
class MethodResolver
{
protected ContainerInterface $container;
protected object $instance;
protected string $method;
protected array $args = [];
/**
* 构造一个 MethodResolver 实例。
*
* @param ContainerInterface $container 依赖注入容器接口,用于解析方法依赖。
* @param object $instance 要在其上执行方法的对象实例。
* @param string $method 要执行的方法名称。
* @param array $args 传递给方法的附加参数,默认为空数组。
*/
public function __construct(ContainerInterface $container, object $instance, string $method, array $args = [])
{
$this->container = $container;
$this->instance = $instance;
$this->method = $method;
$this->args = $args;
}
/**
* 执行指定的方法并返回结果。
*
* 此方法首先创建一个 ReflectionMethod 实例以反映要执行的方法,
* 然后使用 ParametersResolver 类来解析方法所需的参数。
* 最后,它使用解析出的参数调用方法并返回执行结果。
*
* @return mixed 执行方法的返回值。
* @throws \ReflectionException
*/
public function getValue()
{
// 获取类方法的反射类
$method = new ReflectionMethod(
$this->instance,
$this->method
);
// 查找并解析方法参数
$argumentResolver = new ParametersResolver(
$this->container,
$method->getParameters(),
$this->args
);
// 使用注入的参数调用方法
return $method->invokeArgs(
$this->instance,
$argumentResolver->getArguments()
);
}
}
安装
composer require tinywan/ioc
参考代码
<?php
/**
* @desc index.php 描述信息
* @author Tinywan(ShaoBo Wan)
* @date 2024/12/15 10:17
*/
declare(strict_types=1);
// 引入命名空间
use tinywan\ioc\Container;
// 引入自动加载文件
require __DIR__ . '/vendor/autoload.php';
// 定义配置接口
interface ConfigInterface
{
}
// PHP配置类实现配置接口
class PHPConfig implements ConfigInterface
{
}
// YAML配置类实现配置接口
class YAMLConfig implements ConfigInterface
{
}
// 创建并返回相同的单例容器实例
$container = Container::instance();
if (Container::instance() !== $container) {
throw new Exception();
}
// 使用接口命名空间绑定到容器
$container->bind(ConfigInterface::class, PHPConfig::class);
if ($container->get(ConfigInterface::class) !== PHPConfig::class) {
throw new Exception();
}
// 覆盖接口绑定
$container->bind(ConfigInterface::class, YAMLConfig::class);
if ($container->get(ConfigInterface::class) !== YAMLConfig::class) {
throw new Exception();
}
// 使用类命名空间绑定到容器
$container->bind(PHPConfig::class, YAMLConfig::class);
if ($container->get(PHPConfig::class) !== YAMLConfig::class) {
throw new Exception();
}
// 绑定一个单例到容器
$config = new PHPConfig();
$container->singleton(PHPConfig::class, $config);
if ($container->get(PHPConfig::class) !== $config) {
throw new Exception();
}
// 检查构造函数和方法注入参数
class App1
{
public ConfigInterface $config;
public ConfigInterface $methodConfig;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
public function handle(ConfigInterface $config)
{
$this->methodConfig = $config;
}
}
// 检查构造函数参数
$container->bind(ConfigInterface::class, PHPConfig::class);
$app1 = $container->resolve(App1::class);
if (get_class($app1->config) !== PHPConfig::class) {
throw new Exception();
}
// 检查方法参数
$container->resolveMethod($app1, 'handle');
if (get_class($app1->methodConfig) !== PHPConfig::class) {
throw new Exception();
}
// 检查额外传递的参数
class App2
{
public ConfigInterface $config;
public string $arg1;
public string $arg2;
public function __construct(ConfigInterface $config, string $arg1, string $arg2)
{
$this->config = $config;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
$app2 = $container->resolve(App2::class, [
'arg1' => 'value1',
'arg2' => 'value2'
]);
if ($app2->arg1 !== 'value1') {
throw new Exception();
}
if ($app2->arg2 !== 'value2') {
throw new Exception();
}