高级特性
自动配置、标签服务、惰性代理、循环依赖与错误处理。
目录
自动配置
自动配置可根据服务实现的接口或携带的属性,自动为其添加标签和配置——类似 Symfony 的自动配置。
声明式:#[AutoconfigureTag]
在接口或自定义属性上放置 #[AutoconfigureTag],自动标记所有实现/装饰的类。
在接口上
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
#[AutoconfigureTag('command.handler')]
interface CommandHandlerInterface
{
public function __invoke(object $command): void;
}
// 自动标记为 'command.handler'
class CreateUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
class DeleteUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
在自定义属性上
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
#[Attribute(Attribute::TARGET_CLASS)]
#[AutoconfigureTag('scheduler.task')]
class AsScheduled {}
// 自动标记为 'scheduler.task'
#[AsScheduled]
class DailyReportTask
{
public function run(): void { /* ... */ }
}
#[AsScheduled]
class CleanupTask
{
public function run(): void { /* ... */ }
}
编程式:registerForAutoconfiguration()
需要更多控制时(生命周期、lazy 模式、多标签):
$builder->registerForAutoconfiguration(EventListenerInterface::class)
->tag('event.listener')
->singleton()
->lazy();
scan() 过程中找到的任何实现 EventListenerInterface 的类都会自动:
- 获得
event.listener标签 - 配置为单例
- 使用惰性代理
也适用于自定义属性:
$builder->registerForAutoconfiguration(AsScheduled::class)
->tag('scheduler.task')
->transient();
CQRS 示例
使用自动配置的完整命令/查询总线设置:
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
// 定义带自动标记的处理器接口
#[AutoconfigureTag('command.handler')]
interface CommandHandlerInterface
{
public function __invoke(object $command): void;
}
#[AutoconfigureTag('query.handler')]
interface QueryHandlerInterface
{
public function __invoke(object $query): mixed;
}
// 命令处理器——自动标记
class CreateUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
class DeleteUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
// 查询处理器——自动标记
class GetUserHandler implements QueryHandlerInterface
{
public function __invoke(object $query): mixed { /* ... */ }
}
构建并使用:
$builder = new ContainerBuilder(projectDir: __DIR__);
$builder->scan(__DIR__ . '/src');
$container = $builder->build();
// 所有命令处理器
foreach ($container->getTagged('command.handler') as $handler) {
// CreateUserHandler, DeleteUserHandler
}
// 所有查询处理器
foreach ($container->getTagged('query.handler') as $handler) {
// GetUserHandler
}
自动配置的接口不受歧义自动绑定检查影响。CommandHandlerInterface 的多个实现不会报错——它们应通过标签检索。
标签服务
标签将服务分组以便集体检索。对事件分发、中间件链和插件系统等模式至关重要。
添加标签
通过属性:
use AsceticSoft\Wirebox\Attribute\Tag;
#[Tag('event.listener')]
class OrderCreatedListener { /* ... */ }
#[Tag('event.listener')]
class UserCreatedListener { /* ... */ }
#[Tag('event.listener')]
#[Tag('audit')]
class AuditListener { /* ... */ }
通过 fluent API:
$builder->register(OrderCreatedListener::class)->tag('event.listener');
$builder->register(AuditListener::class)->tag('event.listener', 'audit');
检索标签服务
// 所有事件监听器
foreach ($container->getTagged('event.listener') as $listener) {
$listener->handle($event);
}
// 所有审计服务
foreach ($container->getTagged('audit') as $service) {
// ...
}
事件分发器示例
class EventDispatcher
{
/** @var iterable<EventListenerInterface> */
private iterable $listeners;
public function __construct(ContainerInterface $container)
{
// 惰性——监听器在迭代前不会实例化
$this->listeners = $container->getTagged('event.listener');
}
public function dispatch(object $event): void
{
foreach ($this->listeners as $listener) {
$listener->handle($event);
}
}
}
惰性代理
惰性代理将服务构造延迟到实际使用时。基于 PHP 8.4 原生 ReflectionClass::newLazyProxy()。
工作原理
- 请求惰性服务时,立即返回代理对象
- 代理是类的真实实例(通过
instanceof检查) - 访问任何属性或调用方法时,构造真实实例
- 后续访问使用已构造的实例
默认 Lazy 模式
默认情况下,ContainerBuilder 启用 lazy 模式。所有服务都是惰性的,除非:
- 显式标记
#[Eager] - 通过 fluent API 的
->eager()配置
// 此容器中所有服务默认为惰性
$builder = new ContainerBuilder(projectDir: __DIR__);
$builder->scan(__DIR__ . '/src');
$container = $builder->build();
禁用:
$builder->defaultLazy(false);
// 现在只有带 #[Lazy] 的服务才是惰性的
性能优势
惰性代理在以下情况特别有价值:
- 服务有昂贵的构造函数(数据库连接、HTTP 客户端)
- 服务众多但每个请求只用到少数
- 服务有复杂的依赖链且并非总是需要
#[Lazy]
class ElasticsearchClient
{
public function __construct()
{
// 昂贵:建立连接、检查集群健康
$this->client = ClientBuilder::create()
->setHosts(['elasticsearch:9200'])
->build();
}
}
// 代理立即返回——未建立连接
$client = $container->get(ElasticsearchClient::class);
// 此时才建立连接
$client->search(['index' => 'products', 'body' => [...]]);
循环依赖
Wirebox 在构建时检测循环依赖——在容器使用之前。
安全的循环
循环依赖只有当循环中所有服务都是惰性单例时才安全:
#[Lazy]
class ServiceA
{
public function __construct(public readonly ServiceB $b) {}
}
#[Lazy]
class ServiceB
{
public function __construct(public readonly ServiceA $a) {}
}
$container = $builder->build(); // OK——两者都是惰性单例
$a = $container->get(ServiceA::class);
assert($a->b->a === $a); // 同一个代理实例
原理: 代理在真实实例化之前缓存到单例存储中。当依赖链回环时,它找到缓存中的代理,而非重新进入构造。
不安全的循环
| 场景 | 结果 |
|---|---|
| 所有服务都是惰性单例 | 安全 — 代理在实例化前缓存 |
| 任何服务是 eager | 不安全 — Autowirer 两次访问同一类 |
| 任何服务是惰性 transient | 不安全 — 代理未缓存,无限递归 |
不安全的循环会给出清晰的错误消息:
Circular dependency detected: ServiceA -> ServiceB -> ServiceA.
All services in a circular dependency must be lazy singletons.
Unsafe: ServiceB (not lazy)
遇到循环依赖错误时,解决方案通常是:
- 让循环中所有服务成为惰性单例(最简单)
- 重构以打破循环(提取共享依赖)
- 使用 setter 注入延迟其中一个依赖
工厂定义(register(..., fn() => ...))在循环分析中被跳过,因为无法静态确定其依赖。
Setter 注入
配置服务创建后要调用的方法:
$builder->register(Mailer::class)
->call('setTransport', [SmtpTransport::class])
->call('setLogger', [FileLogger::class]);
容器从容器中解析类名参数,标量参数原样传递。多次 call() 按顺序执行。
使用场景
- 不应放在构造函数中的可选依赖
- 服务在创建后配置的框架集成
- 通过延迟一条边来打破循环依赖
自注册
容器将自身注册为 Psr\Container\ContainerInterface。可在任何服务中通过类型提示获取:
use Psr\Container\ContainerInterface;
class ServiceLocator
{
public function __construct(
private ContainerInterface $container,
) {
}
public function getService(string $id): object
{
return $this->container->get($id);
}
}
注入容器本身是服务定位器反模式。优先使用具体依赖的构造函数注入。仅在确实需要动态服务解析时使用自注册(如插件加载器、命令总线)。
错误处理
Wirebox 抛出特定的、描述性的异常:
| 异常 | 触发条件 |
|---|---|
NotFoundException |
服务未找到且无法自动装配 |
AutowireException |
无法解析构造函数参数(无类型提示、不可解析的类型) |
CircularDependencyException |
构建时检测到不安全的循环依赖 |
ContainerException |
通用容器错误(歧义绑定、无效配置) |
所有异常实现 Psr\Container\ContainerExceptionInterface。
示例:捕获错误
use AsceticSoft\Wirebox\Exception\CircularDependencyException;
use AsceticSoft\Wirebox\Exception\ContainerException;
use Psr\Container\NotFoundExceptionInterface;
try {
$container = $builder->build();
} catch (CircularDependencyException $e) {
// "Circular dependency detected: A -> B -> A. ..."
error_log($e->getMessage());
} catch (ContainerException $e) {
// "Ambiguous auto-binding for PaymentInterface: StripePayment, PayPalPayment"
error_log($e->getMessage());
}
try {
$service = $container->get('NonExistentService');
} catch (NotFoundExceptionInterface $e) {
// 服务未找到
}
所有异常包含详细消息和完整依赖路径,便于诊断问题。