前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >如何使用PHP构建IoC容器,实现依赖注入!

如何使用PHP构建IoC容器,实现依赖注入!

作者头像
Tinywan
发布2024-12-19 18:47:02
发布2024-12-19 18:47:02
10700
代码可运行
举报
文章被收录于专栏:开源技术小栈开源技术小栈
运行总次数:0
代码可运行

开源技术小栈本文导读: 随着项目规模的扩大,管理类之间的依赖关系可能成为一项重大挑战。可以使用 IoC (Inversion of control) 容器来解决此问题。容器控制依赖项的注入,并充当一个层,您可以在需要时将使用它。几乎所有现代 PHP 框架(如 Laravel 和 Drupal)都使用 IoC 容器。本教程将教您构建 IoC 容器背后的基本概念,并向您介绍反射 PHP 中最强大的功能之一。

单例容器模式

容器应该是单例实例,并在管理依赖项时充当单一事实来源。由于静态函数是全局的,因此它们可用于每次创建和返回相同的容器实例。

代码语言:javascript
代码运行次数:0
复制
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 方法时,将检查此映射。

代码语言:javascript
代码运行次数:0
复制
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;
    }
}

PSR 容器接口

PHP 框架互操作组具有一组 PHP 标准建议 (PSR),并提供了一组基本接口,您可以使用这些接口创建符合标准且可移植的代码。

PSR-11:容器接口有容器可以实现的 2 个 get() 和 has() 方法。

PSR-11: Container interface :https://www.php-fig.org/psr/psr-11

这些方法检查bindings 数组中的条目并返回绑定值,无论它是命名空间还是单例实例。

代码语言:javascript
代码运行次数:0
复制
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);
    }
}

检索绑定

使用容器可以在需要时设置、获取和更新容器绑定,从而形成一种非常动态且强大的方式来轻松清除依赖项。

代码语言:javascript
代码运行次数:0
复制
$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

单一实例绑定每次都返回相同的实例。

代码语言:javascript
代码运行次数:0
复制
$container->singleton(PHPConfig::class, new PHPConfig());
$container->get(PHPConfig::class); 
// returns singleton PHPConfig instance

$container->get(PHPConfig::class); 
// returns the same singleton PHPConfig instance

依赖关系注入

如果容器用于初始化类实例和调用类方法,则可以为容器内的绑定排除这两者的依赖关系。

代码语言:javascript
代码运行次数:0
复制
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 个方法来创建解析的类实例,并使用解析的依赖项调用类方法。这些的逻辑被抽象为这些方法中的ClassResolverMethodResolver。这些类将容器作为参数,以便它们访问容器绑定。

代码语言:javascript
代码运行次数:0
复制
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();
    }
}

对于未从容器解析的参数,还可以将其他参数传递到这些方法中。

代码语言:javascript
代码运行次数:0
复制
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

也可以将类实例作为参数传入,而不从容器中解析这些依赖项。

代码语言:javascript
代码运行次数:0
复制
$value = $container->resolveMethod($app, 'handle', [
    'config' => new PHPConfig()
    'arg1' => 'value1', 
    'arg2' => 'value2'
]);

创建类解析程序

Reflection 是 PHP 中的一个强大工具,它允许您检查类和函数,并在初始化或调用它们之前“查看”它们需要哪些参数。类解析程序负责检查类构造函数、获取参数并将其传递给ParametersResolver

一旦ParametersResolver 解析了它们,就会创建一个类实例并注入解析的依赖项。

代码语言:javascript
代码运行次数:0
复制
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 的工作原理是遍历传递的函数 / 构造函数反射参数列表。如果其中一个参数是类类型,则找到该类,初始化该类并将其添加到返回的要注入的参数中。值得注意的是,这也是递归的。如果参数类需要注入的参数,则这些参数将在初始化类之前被解析和注入。

代码语言:javascript
代码运行次数:0
复制
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 的工作方式类似于类resolver ,因为它调用ParametersResolver 来获取解析的参数。然后,它使用已解析的依赖项调用类实例方法。

代码语言:javascript
代码运行次数:0
复制
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()
        );
    }
}

使用

安装

代码语言:javascript
代码运行次数:0
复制
composer require tinywan/ioc

参考代码

代码语言:javascript
代码运行次数:0
复制
<?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();
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 单例容器模式
  • 设置容器绑定
  • PSR 容器接口
  • 检索绑定
  • 依赖关系注入
  • 解析依赖关系
    • 创建类解析程序
    • 创建参数解析器
    • 创建方法 resolver
  • 使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档