控制反转与依赖注入 (3) - 构建最小容器实现PSR-11规范

未匹配的标注

手握设计模式宝典 - 控制反转与依赖注入 (3) - 构建最小容器实现PSR-11规范

Design-Pattern - Inversion-Of-Control and Dependency-Injection - Building a Minimal Container Implementing the PSR-11 Specification

  • Title: 《手握设计模式宝典》 之 控制反转与依赖注入 (3) - 构建最小容器实现PSR-11规范
  • Tag: Design-PatternInversion-Of-ControlDependency-InjectionIoCDI设计模式依赖注入PSR-11
  • Author: Tacks
  • Create-Date: 2023-10-19
  • Update-Date: 2023-10-19

大纲

0、Series

0、REF

0、Keyword

  • Object Oriented 面向对象
  • Dependency Inversion Principle 依赖倒置原则 DIP
  • Simple Factory Pattern 简单工厂模式
  • Inversion of Control 控制反转 IoC
  • Dependency Injection 依赖注入 DI
  • Dependent Class 依赖类
  • Dependency 依赖项
  • Container 容器
  • Inversion of Control Container IOC 容器
  • Server Container 服务容器
  • PHP Standard Recommendations PSR 标准规范
  • PHP Framework Interop Group PHP-FIG 组织

1、PSR

PSR(PHP Standards Recommendations,PHP标准推荐)是 PHP 社区为 PHP 开发者制定的一系列技术规范和标准。

PSR 的制定是由 PHP-FIG(PHP Framework Interop Group)组织负责,该组织由许多主要的 PHP 框架和库的核心开发者组成。PHP-FIG 致力于制定共同的接口标准,以促进不同项目之间的代码共享和互操作性。

1.1 PSR-11

PSR-11PHP-FIG 提出的一项标准规范,旨在定义容器接口的共同规范。

PSR-11 规范主要围绕容器的概念,容器是一种用于管理和解决对象依赖关系的机制。它允许开发人员将对象的创建解析逻辑与应用程序的其他部分分离开来,提供了一种灵活且可扩展的方式来管理对象之间的依赖关系。

PSR-11 规范⚙

  • Psr\Container\ContainerInterface 容器接口
    • public function get(string $id)
      • 必传参数:唯一标识符 $id
      • 返回对象:可以是任意类型的值
      • 异常抛出:如果没有对应标识符的值应该抛出一个 NotFoundExceptionInterface
      • 连续调用相同标识符获取的对象不一定相同,具体看 容器 的实现类,(比如可能每次获取都是 new 一个新的对象)
    • public function has(string $id): bool
      • 必传参数:唯一标识符 $id
      • 返回类型:bool (存在对应标识符的对象就返回 true ,否则返回 false)
  • Psr\Container\ContainerExceptionInterface 容器异常接口
    • Psr\Container\NotFoundExceptionInterface 如果 get($id) 获取不存在标识符的时候必须抛出该异常

1.2 PSR-11 规范接口

  • 在项目中引入 PSR-11
$ composer require psr/container
declare(strict_types=1);

namespace Psr\Container;

/**
 * 描述容器的接口,该接口公开用于读取其项的方法。
 */
interface ContainerInterface
{
    /**
     * 根据容器的标识符查找容器的条目并返回该条目
     *
     * @param string $id 要查找条目的标识符
     *
     * @throws NotFoundExceptionInterface 
     * @throws ContainerExceptionInterface
     *
     * @return mixed Entry.
     */
    public function get(string $id);

    /**
     * 容器中是否存在对应标识符的条目
     *
     * @param string $id 要查找条目的标识符
     *
     * @return bool
     */
    public function has(string $id): bool;
}

1.3 PSR-11 的目标

  • 为什么 PSR-11 容器接口定义这么简单?

    • 最小化

      PSR-11 提供一个最小化的容器接口,来实现简洁性和互操作性。

    • 通用性

      get($id) 获取给定标识符对应的对象实例,容器中最最最常用的功能。
      has($id) 判断容器中是否存在标识符对应的对象实例。

    • 灵活性

      虽然 PSR-11 只规定了这两个方法,但是容器的具体实现却可以更加复杂,更加强大,不必拘于这两个方法。

  • 为什么 PSR-11 容器接口没有定义 set() 之类设置依赖关系的方法 ?

“容器 PSR 设定的目标是标准化框架和库如何使用容器来获取对象和参数 。”
—> PSR-11

简而言之:PSR-11 容器接口的目标受众是库或框架的作者,而不是具体应用程序的作者,其中应用程序本身负责填充容器。

  • 为什么 PSR-11 没有告诉我们容器如何去保存对象 ?

对象在容器中怎么保存,如何在容器中配置标识符和对象,这些不是 PSR 规范的范围。

比如有些容器配置依赖 PHP 回调,有些依赖配置文件,有些依赖自动装载,等等。

PSR 这里只规范了如何从容器中获取对象 get($id)

1.4 不同种类容器的类库

下面是一些实现 PSR-11 的部分类库,可以参考一下具体实现方式

2、📦最小容器

2.1 明确什么是 “容器” ?

在 PHP 中,容器也是一个对象,它负责管理其他对象的生命周期、依赖关系和创建过程。通常会保存一个标识符和对应的对象之间的关联关系。当需要使用某个对象时,我们可以通过向容器提供标识符来获取相应的对象实例。

容器可以用不同的名称来称呼,例如 IoC 容器、DI 容器、服务容器,但在实际应用中,通常是表示相同概念。 通过使用容器,我们可以实现对象之间的解耦和灵活性,将对象的创建和依赖关系的处理交给容器来管理。

📌目标

  • 实现 PSR-11 规范
  • 保存/获取 对象

2.2 先实现基础目标

2.2.1 简单容器类 SimpleContainer

  • 主要实现 PSR-11 中的容器接口 Psr\Container\ContainerInterface
namespace App\More\Di\Psr;

use Psr\Container\ContainerInterface;

/**
 * 简单容器,实现 PSR-11
 */
class SimpleContainer implements ContainerInterface 
{
    /* 保存标识符 => 对应的对象 */
    protected $binds = [];

    /**
     * 将对象实例绑定到容器中的一个标识符
     *
     * @param string $id 标识符
     * @param object $service 对象类型
     */
    public function bind(string $id, object $service)
    {
        $this->binds[$id] = $service;
    }

    /**
     * 从容器中获取标识符对应的对象实例
     *
     * @param string $id 标识符
     * @return mixed 
     * @throws NotFoundException
     */
    public function get(string $id)
    {
        if($this->has($id)) {
            return $this->binds[$id];
        }
        throw new NotFoundException("Object not found in container, id:{$id}");
    }

    /**
     * 检查容器中是否存在指定的标识符
     *
     * @param string $id 标识符
     * @return boolean
     */
    public function has(string $id): bool
    {
        return isset($this->binds[$id]);
    }

}

2.2.2 容器异常相关类 NotFoundException

  • 主要实现 PSR-11 中的容器异常接口 Psr\Container\NotFoundExceptionInterface
namespace App\More\Di\Psr;

use Psr\Container\NotFoundExceptionInterface;

class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
}

2.2.3 一些测试类

  • CoffeeMachineInterface
namespace App\More\Di\Psr\Coffee;

/**
 * 咖啡机-接口定义
 */
interface CoffeeMachineInterface {
    public function makeCoffee(string $coffeeBeans);
}
  • AmericanoCoffee
namespace App\More\Di\Psr\Coffee;

/**
 * 美式咖啡机
 */
class AmericanoCoffee implements CoffeeMachineInterface {

    public function makeCoffee(string $beans)
    {
        echo sprintf("[%s] %s ===> 制作({%s})美式 <===", date('Y-m-d H:i:s'), __CLASS__, $beans) . PHP_EOL;
    }
}
  • LatteCoffee
namespace App\More\Di\Psr\Coffee;

/**
 * 拿铁咖啡机
 */
class LatteCoffee implements CoffeeMachineInterface {

    public function makeCoffee(string $beans)
    {
        echo sprintf("[%s] %s ===> 制作({%s})拿铁 <===", date('Y-m-d H:i:s'), __CLASS__, $beans) . PHP_EOL;
    }
}
  • User
namespace App\More\Di\Psr\Coffee;

class User
{
    protected $coffeeMachine;

    public function __construct(CoffeeMachineInterface $coffeeMachine)
    {
        $this->coffeeMachine = $coffeeMachine;
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] %s ===> 开喝! <===", date('Y-m-d H:i:s'), __CLASS__) . PHP_EOL;
    }
}
  • UserA
namespace App\More\Di\Psr\Coffee;

class UserA
{
    protected $coffeeMachine;

    public function __construct(LatteCoffee $coffeeMachine)
    {
        $this->coffeeMachine = $coffeeMachine;
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] %s ===> 开喝! <===", date('Y-m-d H:i:s'), __CLASS__) . PHP_EOL;
    }
}
  • UserB
class UserB
{
    protected $coffeeMachine;
    protected $beans;

    public function __construct(CoffeeMachineInterface $coffeeMachine, string $beans)
    {
        $this->coffeeMachine = $coffeeMachine;
        $this->beans = $beans;
    }

    public function drinkCoffee()
    {
        $this->coffeeMachine->makeCoffee($this->beans);
        echo sprintf("[%s] %s ===> 开喝! <===", date('Y-m-d H:i:s'), __CLASS__) . PHP_EOL;
    }
}

2.2.4 容器使用

use App\More\Di\Psr\Coffee\AmericanoCoffee;
use App\More\Di\Psr\Coffee\LatteCoffee;
use App\More\Di\Psr\Coffee\User;
use App\More\Di\Psr\SimpleContainer;

main();

function main()
{
    try {

        // 容器实例化
        $container = new SimpleContainer();

        // 绑定咖啡机器
        // $container->bind(AmericanoCoffee::class, new AmericanoCoffee());
        $container->bind(LatteCoffee::class, new LatteCoffee());

        // 容器中没有用户类的时候
        if(!$container->has(User::class)) {
            // 绑定用户-构造函数中注入依赖对象
            $container->bind(User::class, new User($container->get(LatteCoffee::class)));
        }

        // 获取用户
        $user = $container->get(User::class);
        $user->drinkCoffee("SOE");

        // 两次获取对象,结果相等,是因为容器中保存的是同一个对象
        $user2 = $container->get(User::class);
        if($user === $user2) {
            echo '$user === $user2' .PHP_EOL;
        } else {
            echo '$user !== $user2' .PHP_EOL;
        }

        $container->get(AmericanoCoffee::class);

    } catch (Exception $th) {
        echo sprintf("[Error] : %s ", $th->getMessage()) , PHP_EOL ;
    }
}
  • 执行结果
[2023-10-18 08:08:08] App\More\Di\Psr\Coffee\LatteCoffee ===> 制作({SOE})拿铁 <===
[2023-10-18 08:08:08] App\More\Di\Psr\Coffee\User ===> 开喝! <===
$user === $user2
[Error] : Object not found in container: [App\More\Di\Psr\Coffee\AmericanoCoffee] 

3、🛒增强容器

📌基础

  • 实现 PSR-11 规范

📌目标

  • 实例化对象 New
  • 单例对象 Singleton
  • 反射解决对象之间的依赖关系 Reflection
  • 自动装配 Auto-Wiring
  • 支持闭包 Closure
  • 接口绑定 bind
  • 解析规则 rule

📌注意

  • 本容器类的实现,只为学习使用,不能用于生成环境,但你可以从这个类再去改进

3.1 容器的实现

  • $entries 以键值对的方式跟踪容器中的注册条目
  • $instances 以键值对方式存储单例的对象,从而每次获取都确保是同一个
  • $rules 一些解析规则
    • 例如,接口绑定到具体实现类上
    • 例如,配置某些类是以单例方式获取
    • 例如,配置某些实现类中构造函数所需参数,例如数据库连接之类的可以放置到配置文件中
  • get($id)
    • 实现 PSR-11 容器接口的方法
    • 先判断是否存在容器中,不存在就进行绑定添加
    • 然后判断是否是闭包的方式
    • 然后判断是否是单例的方式
    • 然后进行正常的解析获取对象
  • has($id)
    • 实现 PSR-11 容器接口的方法
    • 通过判断 $entries 数组中是否有对应标识符的值来返回布尔值
  • bind(string $abstract, $concrete = null)
    • 绑定接收两个参数
    • 参数1:就是标识符
    • 参数2:是完整闭包或者类名
  • resolve($alias)
    • 解析具体的类:也即是有魔法的地方,利用反射来帮助我们解析类,完成实例化
    • 利用 ReflectionClass 获取反射
    • 获取类是否是一个接口
      • 如果是接口,就去解析有没有对应的实体类 resolveInterface($ReflectionClass)
    • 获取类的构造函数
      • 如果无法实例化,则抛出异常
      • 如果没有构造函数,直接就能实例化
      • 如果有构造函数,看如果有参数需要再去获取对应的参数解析 getArguments()
    • 完成类的实例化存储到容器中
  • 接口的解析 resolveInterface
    • 通过判断 $rules 规则中提前判断是否有 substitute 属性规则,如果有就实例化对应具体的类
    • 如果没有解析规则,就判断一下系统中存在的类,是否对应当前接口,然后返回
  • 参数的获取 getArguments()
    • 通过 ReflectionMethod 构造函数中获取 getParameters() 参数列表
    • 循环进行解析
      • 如果参数类型是具体的类名=>直接实例化 (但是这块实现上有点问题,如果类构造函数还有参数呢,就需要递归解析了,还有一些父类啥的问题,这里简单处理)
      • 如果解析规则中有配置 变量映射,直接读取配置
      • 如果参数类型是某个接口 => 并且配置的有映射 => 实例化
      • 如果参数有默认值 => 直接获取默认值
    • 最后拼接所需要的参数
    • 这块稍微复杂一些考虑的情况要多一些,具体可以参考 Laravel 的源码实现。
namespace App\More\Di\Psr;

use Psr\Container\ContainerInterface;

/**
 * 简单容器,实现 PSR-11
 */
class SuperContainer implements ContainerInterface
{
    /**
     * 存储容器中的对象实例或闭包函数
     */
    protected $entries = [];

    /**
     * 存储单例对象实例
     */
    protected $instances = [];

    /**
     * 解析规则
     */
    protected $rules = [];



    /**
     * 获取对象
     *
     * @param string $id
     * @return mixed
     */
    public function get(string $id)
    {
        if (!$this->has($id)) {
            $this->bind($id);
        }

        // 闭包调用
        if ($this->entries[$id] instanceof \Closure || is_callable($this->entries[$id])) {
            return $this->entries[$id]($this);
        }

        // 走单例类
        if (isset($this->rules['shared']) &&  isset($this->rules['shared'][$id]) && $this->rules['shared'][$id] === true) {
            return $this->singleton($id);
        }

        // 正常解析
        return $this->resolve($id);
    }


    /**
     * 是否存在容器中
     *
     * @param string $id
     * @return boolean
     */
    public function has(string $id): bool
    {
        return isset($this->entries[$id]);
    }

    /**
     * 获取单例
     *
     * @param string $alias
     * @return mixed
     */
    public function singleton($alias)
    {
        if (!isset($this->instances[$alias])) {
            $this->instances[$alias] = $this->resolve($alias);
        }
        return $this->instances[$alias];
    }



    /**
     * 绑定到容器中
     *
     * @param  string  $abstract 标识符 id
     * @param  \Closure|string|null  $concrete 闭包或完整的类名
     * @return void
     */
    public function bind(string $abstract, $concrete = null)
    {
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        $this->entries[$abstract] = $concrete;
    }


    /**
     * 配置解析规则
     *
     * @param array $config
     * @return SuperContainer
     */
    public function configure(array $config)
    {
        $this->rules = array_merge($this->rules, $config);
        return $this;
    }

    /**
     * 获取配置规则
     *
     * @return array
     */
    public function getRules()
    {
        return $this->rules;
    }

    /**
     * 解析出真正的对象
     *
     * @param string $alias
     * @return mixed
     */
    protected function resolve($alias)
    {
        // 获取反射
        try {
            $reflector = (new \ReflectionClass($alias));
        } catch (\ReflectionException $e) {
            throw new NotFoundException(
                $e->getMessage(),
                $e->getCode()
            );
        }

        // 是否是接口
        if ($reflector->isInterface()) {
            // 根据类型提示接口解析具体的类
            return $this->resolveInterface($reflector);
        }

        // 无法实例化类-直接异常
        if (!$reflector->isInstantiable()) {
            throw new ContainerException("{$reflector->getName()} cannot be instantiated, id:{$alias}");
        }

        // 获取构造函数
        $constructor = $reflector->getConstructor();

        // 没有构造函数,可以直接实例化
        if (null === $constructor) {
            return $reflector->newInstance();
        }

        // 有构造函数,就获取全部参数再实例化
        $args = $this->getArguments($alias, $constructor);

        return $reflector->newInstanceArgs($args);
    }


    /**
     * 解析接口类型
     *
     * @param \ReflectionClass $reflector
     * @return void
     */
    protected function resolveInterface(\ReflectionClass $reflector)
    {
        if (isset($this->rules['substitute'][$reflector->getName()])) {
            return $this->get(
                $this->rules['substitute'][$reflector->getName()]
            );
        }

        // 返回全部的类
        $classes = get_declared_classes();


        // 循环判断是否有符合的接口实现类
        foreach ($classes as $class) {
            $rf = new \ReflectionClass($class);
            if ($rf->implementsInterface($reflector->getName())) {
                return $this->get($rf->getName());
            }
        }
        throw new NotFoundException(
            "Class {$reflector->getName()} not found",
            404
        );
    }


    /**
    * 获取构造函数的参数
    *
    * @param string $alias
    * @param \ReflectionMethod $constructor
    * @return array
    */
    protected function getArguments($alias, \ReflectionMethod $constructor)
    {
        $args = [];
        $parameters = $constructor->getParameters();
        foreach ($parameters as $param) {

            // 获取参数类型
            $parameterType = $param->getType();
            assert($parameterType instanceof \ReflectionNamedType);
            // 参数类型名
            $parameterTypeName  = $parameterType->getName();
            // 形参名称
            $parameterName = $param->getName();

            if (class_exists($parameterTypeName)) {
                // 如果类存在
                $args[] = new $parameterTypeName();

            }  elseif (isset($this->rules[$alias][$parameterName])) {
                // 查看解析规则中是否有-变量名映射
                $args[] = $this->rules[$alias][$parameterName];

            } elseif (isset($this->rules[$alias][$parameterTypeName])) {
                // 查看解析规则中是否有-类名映射
                $args[] = $this->rules[$alias][$parameterTypeName];

            } elseif ($param->isDefaultValueAvailable()) {
                // 如果有默认值
                $args[] = $param->getDefaultValue();

            } elseif (interface_exists($parameterTypeName)) {
                // 如果接口存在
                $args[] = $this->resolveInterface(new \ReflectionClass($parameterTypeName));
            }
        }
        return $args;
    }

}

3.2 容器的使用

  • 测试1
/**
 * 支持接口绑定
 * 支持自动装配
 * 支持自动解析依赖参数
 * 支持规则配置
 *
 * @return void
 */
function func1()
{
    echo "=======================================================>" , __FUNCTION__ , PHP_EOL;
    // 容器实例化
    $container = new SuperContainer();

    // 定义解析规则
    $container->configure([
        'substitute' => [
            CoffeeMachineInterface::class => LatteCoffee::class,
        ],
        UserB::class => [
            'coffeeMachine' => $container->get(AmericanoCoffee::class),
            'beans'         => 'SOE',
        ],
    ]);

    // 获取用户-构造函数注入-接口类型提前配置 substitute 明确具体实例化的类
    $user = $container->get(User::class);
    $user->drinkCoffee("SOE");

    // 获取用户-构造函数注入-已知类型直接实例化
    $user = $container->get(UserA::class);
    $user->drinkCoffee("瑰夏");

    // 获取用户-构造函数注入-多个参数需要同时配置-可以再配置文件侧填写
    $user = $container->get(UserB::class);
    $user->drinkCoffee();


    $rules = $container->getRules();
    var_dump($rules);
    echo "<=======================================================",  __FUNCTION__ , PHP_EOL;
}
  • 测试2
/**
 * 支持单例
 *
 * @return void
 */
function func2()
{
    echo "=======================================================>",  __FUNCTION__ , PHP_EOL;
    // 容器实例化
    $container = new SuperContainer();

    // 接口绑定实现类对象
    $container->bind(CoffeeMachineInterface::class, $container->get(AmericanoCoffee::class));

    // 直接获取
    $user1 = $container->get(User::class);
    $user1->drinkCoffee("埃塞俄比亚");

    $user2 = $container->get(User::class);
    $user2->drinkCoffee("埃塞俄比亚");

    if ($user1 === $user2) {
        echo '$user1 === $user2', PHP_EOL;
    } else {
        echo '$user1 !== $user2', PHP_EOL; // 不相等
    }

    // 单例获取
    $user3 = $container->singleton(User::class);
    $user3->drinkCoffee("埃塞俄比亚");

    $user4 = $container->singleton(User::class);
    $user4->drinkCoffee("埃塞俄比亚");

    if ($user3 === $user4) {
        echo '$user3 === $user4', PHP_EOL; // 相等
    } else {
        echo '$user3 !== $user4', PHP_EOL;
    }

    echo "<=======================================================",  __FUNCTION__ , PHP_EOL;
}
  • 测试3
/**
 * 支持闭包
 *
 * @return void
 */
function func3()
{
    echo "=======================================================>",  __FUNCTION__ , PHP_EOL;

    // 闭包获取
    $container = new SuperContainer();
    $container->bind(User::class, function () {
        return new User(new LatteCoffee());
    });

    $user = $container->get(User::class);
    $user->drinkCoffee("埃塞俄比亚");

    echo "<=======================================================",  __FUNCTION__ , PHP_EOL;
}

4、👓总结一下

本身实现 PSR-11 容器接口规范很简单,甚至你用一个数组来保存对象都行。当然本文还是以学习的角度去看待 IoC 容器的作用。但是在具体应用程序中,还需要考虑多种因素,实现起来也有不同方式,但只要实现了 PSR-11 接口,就能标准化容器获取对象的方法,从而也可以兼容不同框架,不同类库,用户也能去选择他们喜欢的容器。

PSR-11 容器接口规范,统一 get($id) 来获取对象。更重要的是面向框架和类库的开发者,从而让应用提供一种通用获取对象的方式。

为了成为一个更为成熟的 IoC 容器,一般会有实例化对象、单例对象、依赖注入、闭包、自动装配、接口绑定、生命周期管理、解析配置管理、对象别名、延迟实例化、工厂模式等多种能力。具体可以参考 Laravel 服务容器和服务提供商的概念。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~