PHP 属性

使用 PHP 8.4 原生属性进行声明式服务配置。

目录

概览

Wirebox 提供一组 PHP 属性,直接在类定义中声明式配置服务,无需外部配置文件。

属性 目标 说明
#[Singleton] 每个容器一个实例(默认)
#[Transient] 每次 get() 新实例
#[Lazy] 通过代理延迟实例化
#[Eager] 禁用默认 lazy 模式
#[Tag] 分组检索标签(可重复)
#[Inject] 参数 覆盖类型提示的服务
#[Param] 参数 注入环境变量
#[Exclude] 扫描时跳过
#[AutoconfigureTag] 按接口或属性自动标记

所有属性在 AsceticSoft\Wirebox\Attribute 命名空间下。


#[Singleton]

将类标记为单例。由于这是默认行为,主要用于明确表达意图:

use AsceticSoft\Wirebox\Attribute\Singleton;

#[Singleton]
class DatabaseConnection
{
    public function __construct(
        #[Param('DB_HOST')] private string $host,
    ) {
    }
}

等效 Fluent API:

$builder->register(DatabaseConnection::class)->singleton();

#[Transient]

每次 get() 调用创建新实例:

use AsceticSoft\Wirebox\Attribute\Transient;

#[Transient]
class RequestContext
{
    private float $startTime;

    public function __construct()
    {
        $this->startTime = microtime(true);
    }
}

每次调用 $container->get(RequestContext::class) 返回新实例。

等效 Fluent API:

$builder->register(RequestContext::class)->transient();

对持有请求特定状态的服务使用 #[Transient],如请求上下文、表单数据或 DTO。


#[Lazy]

立即返回轻量级代理;只有在首次访问属性或方法时才创建真实实例。使用 PHP 8.4 原生惰性对象 (ReflectionClass::newLazyProxy):

use AsceticSoft\Wirebox\Attribute\Lazy;

#[Lazy]
class HeavyReportGenerator
{
    public function __construct(
        private PDO $db,
        private CacheInterface $cache,
    ) {
        // 昂贵的初始化 —— 只在实际需要时执行
    }
}

关键特性:

  • 代理是类的真实实例(通过 instanceof 检查)
  • 创建延迟到首次属性或方法访问
  • 编译容器完全支持
  • 可与 #[Transient] 组合,每次 get() 获取新的惰性代理

等效 Fluent API:

$builder->register(HeavyReportGenerator::class)->lazy();

defaultLazy 启用时(默认),所有服务都是惰性的,除非标记了 #[Eager]#[Lazy] 属性仅在 defaultLazy 关闭时需要。


#[Eager]

当容器默认 lazy 模式启用时,禁用惰性实例化:

use AsceticSoft\Wirebox\Attribute\Eager;

#[Eager]
class AppConfig
{
    public function __construct()
    {
        // 即使 defaultLazy 开启,也始终立即创建
    }
}

适用于:

  • 必须尽早初始化的服务(配置、事件订阅者)
  • 构造函数有副作用的服务
  • 需要在启动时验证设置的服务

等效 Fluent API:

$builder->register(AppConfig::class)->eager();

#[Tag]

为类添加标签以进行分组检索。属性可重复 —— 一个类可以有多个标签:

use AsceticSoft\Wirebox\Attribute\Tag;

#[Tag('event.listener')]
#[Tag('audit')]
class UserCreatedListener
{
    public function handle(object $event): void
    {
        // ...
    }
}

检索特定标签的所有服务:

foreach ($container->getTagged('event.listener') as $listener) {
    $listener->handle($event);
}

等效 Fluent API:

$builder->register(UserCreatedListener::class)->tag('event.listener', 'audit');

标签非常适合插件系统、事件分发器、中间件链和命令/查询总线模式。


#[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
    {
        // ...
    }
}

在自定义属性上

所有被该属性装饰的类获得标签:

use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;

#[Attribute(Attribute::TARGET_CLASS)]
#[AutoconfigureTag('scheduler.task')]
class AsScheduled {}

// 扫描时自动标记为 'scheduler.task'
#[AsScheduled]
class DailyReportTask
{
    public function run(): void { /* ... */ }
}

多个标签

属性可重复:

#[AutoconfigureTag('command.handler')]
#[AutoconfigureTag('auditable')]
interface CommandHandlerInterface {}

带有 #[AutoconfigureTag] 的接口不受歧义自动绑定检查影响 —— 多个实现是预期行为。

另见自动配置了解编程式配置。


#[Inject]

覆盖特定构造函数参数的类型提示服务:

use AsceticSoft\Wirebox\Attribute\Inject;

class NotificationService
{
    public function __construct(
        #[Inject(SmtpMailer::class)]
        private MailerInterface $mailer,
    ) {
    }
}

没有 #[Inject],Wirebox 会通过容器绑定解析 MailerInterface。有了 #[Inject],无论绑定如何都始终注入 SmtpMailer

使用场景:

  • 某个服务需要特定实现,其他地方需要另一个
  • 接口没有全局绑定
  • 需要为特定消费者覆盖全局绑定

#[Param]

直接将环境变量的标量值注入构造函数参数:

use AsceticSoft\Wirebox\Attribute\Param;

class DatabaseService
{
    public function __construct(
        #[Param('DB_HOST')] private string $host,
        #[Param('DB_PORT')] private int $port,
        #[Param('APP_DEBUG')] private bool $debug = false,
    ) {
    }
}

类型转换根据参数类型提示自动进行:

PHP 类型 环境值 结果
string "localhost" "localhost"
int "5432" 5432
float "1.5" 1.5
bool "true" / "1" true
bool "false" / "0" / "" false

#[Param] 直接从环境变量读取(使用三级优先级系统)。这是注入配置值的最简方式。

详见环境变量


#[Exclude]

目录扫描时排除类的自动注册:

use AsceticSoft\Wirebox\Attribute\Exclude;

#[Exclude]
class InternalHelper
{
    // 不会注册到容器中
}

使用场景:

  • 不应成为服务的辅助/工具类
  • 不用于直接实例化的基类
  • 意外出现在扫描目录中的测试替身

#[Exclude] 仅影响目录扫描。仍可通过 $builder->register() 手动注册被排除的类。


组合属性

属性可自由组合:

use AsceticSoft\Wirebox\Attribute\{Lazy, Singleton, Tag, Param};

#[Singleton]
#[Lazy]
#[Tag('event.listener')]
class OrderEventListener
{
    public function __construct(
        #[Param('NOTIFICATION_EMAIL')]
        private string $notifyEmail,
    ) {
    }

    public function onOrderCreated(OrderCreated $event): void
    {
        // ...
    }
}

属性 vs Fluent API

每个属性都有等效的 fluent API 调用,选择你喜欢的风格:

属性 Fluent API
#[Singleton] ->singleton()
#[Transient] ->transient()
#[Lazy] ->lazy()
#[Eager] ->eager()
#[Tag('x')] ->tag('x')
#[Exclude] $builder->exclude(...)

属性适合与类定义一起的配置。Fluent API 适合外部或条件配置(如不同环境使用不同绑定)。