fiberphp / container
🚀 FiberPHP IoC 服务容器 —— PSR-11 实现,支持绑定/单例/上下文绑定、延迟加载、构造参数自动装配与 #[Inject] 属性注入。
Requires
- php: >=8.3
- psr/container: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
None
Provides
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-11 11:01:25 UTC
README
FiberPHP IoC 服务容器,实现 PSR-11。支持绑定与单例、上下文绑定、延迟绑定、构造参数自动装配、#[Inject] 属性注入、服务装饰与标签,并内置部署前依赖图静态检查器。
环境要求
- PHP >= 8.3
- psr/container ^2.0
安装
composer require fiberphp/container
快速开始
use FiberPHP\Container\Container;
$container = new Container();
$container->singleton(LoggerInterface::class, FileLogger::class);
$userService = $container->get(UserService::class);
UserService 无需任何注册,其构造参数中的类依赖会被自动装配。
绑定与单例
bind() — 工厂绑定
// 类名:解析时自动装配 Impl 的构造参数
$container->bind(LoggerInterface::class, FileLogger::class);
// 闭包:自定义构造过程,闭包接收容器与透传参数
$container->bind(Redis::class, fn (Container $c, array $params = [])
=> new Redis($params['host'] ?? '127.0.0.1'));
// 自绑定(省略 concrete):仅为该类注册一个绑定条目
$container->bind(Service::class);
// 共享实例请用 singleton()
$container->singleton(Service::class);
singleton() — 单例绑定
工厂只解析一次,后续始终返回同一实例:
$container->singleton(CacheInterface::class, fn (Container $c) => new RedisCache());
实例直接注册
把已创建的对象直接登记为已解析实例:
$container->bind(ClockInterface::class, new SystemClock());
bind() / singleton() 返回容器自身,可链式调用。重新绑定同一抽象会清除其已缓存的实例。
解析
get() — PSR-11 入口
$logger = $container->get(LoggerInterface::class);
未绑定的类执行自动构建;条目不存在或构建失败时抛出异常(见异常)。
getOrNull() — 失败返回 null
$logger = $container->getOrNull(LoggerInterface::class) ?? new NullLogger();
has() — 是否存在条目
if ($container->has(LoggerInterface::class)) {
// 已绑定、已解析或已注册延迟绑定
}
make() — 透传构造参数 / 强制新建
// 位置参数或命名参数均可
$service = $container->make(Service::class, ['host' => 'redis://x', 1]);
// fresh=true:跳过单例缓存,强制创建新实例
$another = $container->make(Service::class, fresh: true);
传入构造参数(即使是 singleton 绑定)会绕过单例缓存,本次解析返回独立实例;无参解析才命中共享缓存。
自动装配规则
未显式传入的构造参数,按参数声明处理:
| 参数声明 | 处理方式 |
|---|---|
单一类类型 Foo、可空类类型 ?Foo | 从容器解析该类型 |
多类联合 Foo\|Bar | 需有默认值(回退)或显式绑定,否则报错 |
交叉类型 Foo&Bar | 需有默认值或显式绑定,否则报错 |
| 标量、无类型参数 | 需有默认值,否则报错 |
变长参数 ...$items | 不自动解析,仅接受 make() 显式传入(数组自动展开) |
有默认值的类依赖还承担两个兜底场景:依赖类型正在构建链上(自引用/互相引用,如 ?Node $parent = null),以及未绑定的接口/抽象类——此时回退默认值而非报错。
属性注入
实例属性标注 #[Inject],对象构建完成后自动注入。支持两种写法:
use FiberPHP\Container\Inject;
class Service
{
// 1. 按属性类型注入(要求属性有明确类型)
#[Inject]
protected LoggerInterface $logger;
// 2. 显式指定服务键(属性可不加类型)
#[Inject(CacheInterface::class)]
protected $cache;
}
static 属性不注入;readonly 属性、无类型且未显式指定 class 的属性会在构建期抛出异常。建议优先使用构造注入,属性注入仅用于基类层级。
上下文绑定
同一接口在不同消费类中使用不同实现:
$container->addContextual(OrderService::class, LoggerInterface::class, OrderLogger::class);
$container->addContextual(UserService::class, LoggerInterface::class, UserLogger::class);
查询某个消费类中的绑定(未定义返回 null):
$impl = $container->contextualBinding(OrderService::class, LoggerInterface::class);
服务装饰 extend()
在已有服务外层追加包装(如缓冲、缓存),装饰闭包接收原实例与容器:
$container->extend(LoggerInterface::class,
fn ($logger, Container $c) => new BufferedLogger($logger));
- 已解析的实例:立即装饰并替换;
- 已绑定的服务:包装其工厂,保留原有单例/工厂语义;
- 未绑定的类:按瞬态(bind)注册,需要共享时应先
singleton()再extend()。
服务标签
把一组服务归入同一标签,再按组批量解析:
// 单标签或多标签、单服务或多服务均可
$container->tag([CollectorA::class, CollectorB::class], 'collectors');
$container->tag(CollectorC::class, ['collectors', 'metrics']);
foreach ($container->tagged('collectors') as $collector) {
$collector->run();
}
tagged() 返回解析后的实例列表,顺序与打标顺序一致;同一标签内重复打标只保留一次;标签未定义时返回空数组。
延迟绑定
服务仅在首次被请求时才执行注册逻辑,适合加载成本高的服务:
$container->setDeferredProvider('heavy.service', function (Container $c) {
$c->singleton('heavy.service', fn () => new HeavyService());
});
// 此时才触发上面的加载闭包,且仅触发一次
$service = $container->get('heavy.service');
has() 对已注册的延迟绑定返回 true。批量移除延迟条目:
$container->removeDeferredProviders(['heavy.service', 'other.service']);
方法调用 call()
调用任意可调用对象,未显式传入的参数按自动装配规则注入:
// 闭包
$result = $container->call(fn (LoggerInterface $logger) => $logger->getName());
// [对象, 方法名],命名参数显式传入
$result = $container->call([$controller, 'show'], ['id' => 1]);
// [类名, 方法名]:接收对象自动从容器解析
$result = $container->call([ReportGenerator::class, 'run'], ['format' => 'pdf']);
全局访问点
容器可作为进程级单例访问:
$container = Container::getInstance();
Container::setInstance($customContainer);
Container::reset(); // 清除单例(测试隔离用)
依赖图静态检查
DependencyChecker 通过反射遍历入口类的完整构造依赖图,不实例化任何类,供部署流水线在运行前发现问题:
use FiberPHP\Container\DependencyChecker;
$checker = new DependencyChecker($container);
$errors = $checker->check([
UserController::class,
AuthMiddleware::class,
]);
返回值为错误列表,每条包含可读依赖链路与错误原因;返回空数组即检查通过:
[
'path' => [UserController::class, '$userService', '...'],
'message' => "No binding found for interface 'LoggerInterface'",
]
覆盖的问题:类不存在、类不可实例化(接口/抽象类)、未绑定接口、不可解析的标量参数、圈依赖,以及非法 #[Inject] 属性。
检查边界:
- 有默认值、变长参数的依赖不纳入检查(运行期可自行兜底);
- 显式绑定(含上下文绑定、延迟绑定)视为可解析,不展开闭包内部。
异常
| 异常 | 实现接口 | 抛出场景 |
|---|---|---|
ClassNotFoundException | PSR NotFoundExceptionInterface | 类不存在;接口存在但未绑定实现时提示 No binding found for interface |
ContainerException | PSR ContainerExceptionInterface | 不可实例化、圈依赖、参数无法解析、非法属性注入等一切构建错误 |
两者均位于 FiberPHP\Container\Exception,且 ClassNotFoundException 继承自 ContainerException,按需选择 catch 粒度。