⭐ 推荐:社区规则条款 V1.0

PHP 依赖注入容器手写实战:100 行实现 PSR-11 兼容容器

玄墨染
玄墨染 正式会员正式会员认证极客认证极客
发布于 2026-09-23 01:44 ·74 浏览 ·0 回复
内容摘要

介绍如何用约 100 行代码手写 PSR-11 兼容的 PHP 依赖注入容器,先通过 Composer 安装 psr/container 获取 ContainerInterface 和两个异常接口,并说明异常接口用于区分找不到与构建失败、无 Composer 时可手写接口,进而围绕构造函数自动装配、单例缓存和循环依赖检测完成容器实现。

学完这篇,你能在自己项目里落地一个 100 行上下、真正实现 Psr\Container\ContainerInterface 的依赖注入容器,并且搞明白构造函数自动装配、单例缓存和循环依赖检测是怎么写出来的。

第一步:装上 PSR-11 接口

容器不自己造接口标准,直接用官方的:

composer require psr/container

会得到三个东西:ContainerInterface(只有 get($id) 和 has($id) 两个方法)、NotFoundExceptionInterface、ContainerExceptionInterface。两个异常接口是空标记接口,作用是让调用方可以精确地区分「找不到」和「构建失败」,所以你必须实现它们,不能随便抛个 RuntimeException 了事。

注意:如果你的老项目跑不起来 Composer,直接建三个文件手写接口内容也行,PSR-11 接口本身只有几行,不算侵权也不算投机。

第二步:定义异常 + 容器骨架

新建 src/Container.php:

<?php
declare(strict_types=1);

namespace App;

use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Container\ContainerExceptionInterface;

class NotFoundException extends \RuntimeException implements NotFoundExceptionInterface {}
class ContainerException extends \RuntimeException implements ContainerExceptionInterface {}

class Container implements ContainerInterface
{
    /** @var array<string, mixed> 注册的绑定 */
    private array $definitions = [];
    /** @var array<string, mixed> 已构建的单例 */
    private array $instances = [];
    /** @var array<string, bool> 解析中标记,用于循环依赖检测 */
    private array $resolving = [];

    /** 注册:$concrete 可以是类名、闭包或现成对象 */
    public function set(string $id, $concrete): void
    {
        $this->definitions[$id] = $concrete;
    }

    public function has(string $id): bool
    {
        return isset($this->definitions[$id]) || isset($this->instances[$id]);
    }

    public function get(string $id)
    {
        if (isset($this->instances[$id])) {
            return $this->instances[$id];      // 命中单例缓存
        }
        if (isset($this->resolving[$id])) {
            throw new ContainerException("检测到循环依赖:{$id}");
        }
        $this->resolving[$id] = true;
        try {
            $obj = $this->build($id);
        } finally {
            unset($this->resolving[$id]);      // 无论成败都要清理,否则下次同样的 ID 会被误判
        }
        return $this->instances[$id] = $obj;
    }

    private function build(string $id)
    {
        $concrete = $this->definitions[$id] ?? $id;

        if ($concrete instanceof \Closure) {
            return $concrete($this);           // 闭包工厂:$this 就是容器,方便内部再取依赖
        }
        if (is_object($concrete)) {
            return $concrete;                  // 直接塞进来的实例
        }
        if (is_string($concrete) && class_exists($concrete)) {
            return $this->autowire($concrete);
        }
        throw new NotFoundException("无法解析:{$id}");
    }
}

这里两个细节值得停一下看清楚:finally 里的 unset 不能省,否则一次失败会让这个 ID 永久被标记为「正在解析」;has() 故意不返回 class_exists($id),PSR-11 规定 has() 返回 true 就必须能 get() 成功,把类名算进 has() 容易给出误导性答案。

第三步:用反射做构造函数自动装配

补上 autowire() 方法:

    private function autowire(string $class)
    {
        $ref = new \ReflectionClass($class);

        if (!$ref->isInstantiable()) {
            throw new ContainerException("{$class} 是抽象类或接口,无法实例化");
        }

        $ctor = $ref->getConstructor();
        if ($ctor === null) {
            return new $class();               // 无构造函数,直接 new
        }

        $args = [];
        foreach ($ctor->getParameters() as $param) {
            $type = $param->getType();

            // 只处理类类型提示(ReflectionNamedType 且非内置类型)
            if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
                $args[] = $this->get($type->getName());   // 递归解析依赖
                continue;
            }
            if ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();      // 标量参数走默认值
                continue;
            }
            throw new ContainerException(
                "{$class} 的参数 \${$param->getName()} 无法自动注入,请显式注册"
            );
        }

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

注意:自动装配只能处理类类型提示。__construct(string $dsn) 这种标量参数反射不出来,必须要么给默认值,要么在 set() 里用闭包把配置传进去。这是新手最常见的报错来源。另外 PHP 8 的联合类型(Foo|Bar)是 ReflectionUnionType,会被上面的判断跳过,同样需要显式注册。

第四步:跑起来验证

interface MailerInterface {}
class SmtpMailer implements MailerInterface {}

class UserService
{
    public function __construct(private MailerInterface $mailer) {}
}

$c = new Container();
$c->set(MailerInterface::class, SmtpMailer::class);   // 接口 → 实现
$c->set('dsn', fn() => 'mysql:host=127.0.0.1');

$svc = $c->get(UserService::class);
var_dump($svc instanceof UserService);          // true
var_dump($c->get(UserService::class) === $svc); // true,单例

UserService 没注册过,容器靠反射发现它需要 MailerInterface,回头查绑定表拿到 SmtpMailer,这就是所谓「自动装配」。全部代码加起来 90 行左右。

小结

  • PSR-11 只有 get/has 两个方法,加两个异常标记接口,先 composer require psr/container。
  • 解析顺序:单例缓存 → 循环依赖检查 → 绑定表 → 闭包/实例/类名 → 反射自动装配。
  • 循环依赖必须靠「解析中标记 + finally 清理」来拦,否则会栈溢出且报错信息极难读。
  • 标量参数、联合类型、抽象类这四种情况反射搞不定,一律显式 set() 注册。
  • 想让某个服务每次 get 都新建实例(transient),就在 get() 里改成不入 $this->instances,加个 $shared 开关即可。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-566.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~