容器与依赖注入

依赖注入(Dependency Injection)是一种设计模式:对象不自行创建依赖,而是由外部容器注入。Viswoole 的容器(Container)负责全框架的服务绑定、依赖解析与实例生命周期管理——控制器、验证规则、路由处理器都由它创建。

本文所有 API 位于 Viswoole\Core\AppViswoole\Core\Container,依据框架源码 src/Core/App.phpsrc/Core/Container.phpsrc/helper.php 编写。

App:全局唯一的容器

App 继承自 Container,是框架全局唯一的容器实例。通过 App::factory() 获取,首次调用时自动创建:

php
use Viswoole\Core\App;

$app = App::factory(); // 全局单例,任意位置重复调用返回同一实例

首次创建时按顺序完成以下初始化(App::__constructinitialize()loadService()):

  1. 解析并定义 BASE_PATH 常量(项目根目录);
  2. 读取 app.debug 初始化调试模式(写入 Swoole 共享内存表,跨 Worker 同步);
  3. 读取 app.default_timezone 设置默认时区(默认 Asia/Shanghai);
  4. 合并框架默认服务、config/app.phpservices[]vendor/services.php 中依赖包注册的服务,依次调用各服务提供者的 register()boot()
  5. 触发 FrameworkEvent::AppInitialized 事件。

只初始化一次

初始化发生在进程启动阶段,之后整个进程生命周期内不会重新执行。修改服务提供者、配置等启动期加载的内容后必须重启服务才能生效。

魔术属性

App 通过容器实现 __get,可以直接以属性方式访问已注册的服务:

php
$config = App::factory()->config;   // 等价于 $app->make('config')
$log    = App::factory()->log;

框架预定义的服务属性(见 App 类文档注释):

属性类型说明
$app->envEnv环境变量管理
$app->configConfig配置管理
$app->consoleConsole控制台
$app->logLogManager日志
$app->eventEvent事件管理器
$app->serverServer服务管理器
$app->cacheCacheManager缓存
$app->middlewareMiddleware中间件
$app->routerRouter路由
$app->requestRequestInterface当前 HTTP 请求对象(请求级)
$app->responseResponseInterface当前 HTTP 响应对象(请求级)

容器同时支持数组式访问(实现 ArrayAccess):

php
$dbHost = $app['config']->get('db.host'); // 读取绑定的服务
$app['db'] = MyDb::class;                 // 等价于 $app->bind('db', MyDb::class)

服务绑定 bind

bind() 将一个标识(接口名、类名或自定义字符串)映射到实现:

php
use Viswoole\Core\App;

$app = App::factory();

// 1. 绑定类名:标识 → 实现类
$app->bind(PaymentInterface::class, AlipayPayment::class);

// 2. 绑定闭包:延迟到解析时才执行
$app->bind(ConnectionInterface::class, function () {
    return new MySqlConnection(config('db.channels.default.options'));
});

// 3. 绑定实例:立即作为单例存入容器
$app->bind(ClockInterface::class, new SystemClock());
参数类型默认值说明
abstractstring必填接口名、类名或自定义标识
concretestring|object必填实现类名、闭包或已有实例;字符串必须为有效类名,否则抛 TypeError

三种形态的行为差异:

  • 类名:仅记录映射关系,首次 make() 时才实例化并缓存单例;
  • 闭包:首次 make() 时执行,返回值作为单例缓存;
  • 实例:调用 bind() 的瞬间即写入单例池,之后 make() 直接返回该实例。

内置绑定

框架启动时预置了以下绑定(App::$bindings):appenvconfigconsoleeventserver,以及一个指向 Swoole 原生 Server 对象的闭包(便于注入 Swoole 服务实例)。各服务提供者还会通过 Provider::$bindings 追加更多绑定。

解析实例 make

make() 是容器最常用的解析入口:

php
// 解析绑定标识
$payment = $app->make(PaymentInterface::class); // 返回 AlipayPayment 实例

// 直接解析任意类,自动递归注入构造函数依赖
$service = $app->make(UserService::class);

// 传入构造参数,覆盖自动注入(支持命名参数或按位置)
$service = $app->make(UserService::class, ['repository' => $repo]);
参数类型默认值说明
abstractstring必填绑定标识或类名
paramsarray[]手动传入的构造参数,覆盖依赖注入

ALLOW_NEW_INSTANCE:每次新建

默认情况下 make() 对已解析的类缓存单例。若类定义了 ALLOW_NEW_INSTANCE = true 常量,则每次 make() 都创建新实例、不缓存:

php
class ReportBuilder
{
    // 声明后 make(ReportBuilder::class) 每次返回全新实例
    public const bool ALLOW_NEW_INSTANCE = true;
}

$a = $app->make(ReportBuilder::class);
$b = $app->make(ReportBuilder::class);
// $a !== $b

has / get / remove

方法签名说明
hashas(string $id): bool是否存在绑定或实例
getget(string $id): object等价于 make(),未绑定时抛 NotFoundException
removeremove(string $abstract): void移除指定标识的单例实例(含协程上下文中的)
getBindingsgetBindings(): array获取全部绑定映射
hasInstancehasInstance(string $class): bool该类是否已有单例实例

invokeClass:工厂方法优先

invokeClass() 通过反射创建类实例,自动解析构造函数依赖。特殊规则:若类存在 public static factory() 方法,则优先调用它创建实例,否则使用 __construct

php
class Connection
{
    public static function factory(): static
    {
        return new static(config('db.channels.default.options'));
    }

    private function __construct(private array $options) {}
}

// 容器优先调用 Connection::factory() 而非反射构造函数
$conn = $app->invokeClass(Connection::class);

App 自身正是利用这一约定实现单例:App::factory()。框架内 EventLogManager 等管理器也遵循同样的模式。

方法调用与参数注入

容器提供一组 invoke* 方法,反射分析参数类型后自动注入:

方法说明
invoke(callable|string|array $callable, array $params = [])统一入口,按传入结构自动分发
invokeMethod(array|callable $method, array $params = [])调用 [对象/类名, 方法]'类名::方法名'
invokeFunction(string|Closure $fn, array $params = [])调用函数或闭包
invokeClass(string $class, array $params = [])实例化类并注入构造参数
php
class OrderService
{
    public function create(
        UserRepository $repo,   // 类型注入:容器自动解析
        int $userId,            // 标量参数:从 $params 按名匹配
        string $orderNo = '',   // 有默认值:可省略
    ): array {
        return ['user' => $repo->find($userId), 'orderNo' => $orderNo];
    }
}

$order = $app->invoke([new OrderService(), 'create'], ['userId' => 1, 'orderNo' => 'A001']);

invoke() 的分发规则:

text
闭包                  → invokeFunction
[类/对象, 方法] 数组   → invokeMethod
'类名::方法名' 字符串  → invokeMethod
类名字符串            → invokeClass
函数名字符串          → invokeFunction

参数注入能力

容器对方法/构造参数的处理覆盖以下场景(依据 Container::injectParams()):

能力说明
命名参数$params 按参数名匹配
位置参数$params 按索引匹配
可变参数支持 ...$args,逐项校验
默认值有默认值的参数可省略
可空类型?Type 允许传入 null
类类型自动递归解析依赖(含接口绑定)
前置注入注解实现 PreInjectInterface 的 Attribute 在注入前改写参数值
校验规则注解BaseValidateRule 子类 Attribute 在注入时自动校验(详见验证器

类型不匹配或校验失败时抛出 Viswoole\Core\Exception\ValidateException,debug 模式下错误消息附带参数位置与方法上下文。

解析钩子

类被 invokeClass 创建后可触发回调,用于统一装饰实例(如注入日志代理):

php
// 为所有新创建的实例统一打点
$app->addHook('*', function (object $instance, Container $container): void {
    // 对 $instance 做额外处理
});

// 仅监听某个类
$id = $app->addHook(UserService::class, function (UserService $service): void { /* ... */ });
$app->removeHook(UserService::class, $id); // 移除指定钩子

请求级协程隔离

这是 Viswoole 容器最重要的设计,值得单独解释。

Swoole 下每个 HTTP 请求运行在独立的根协程中(请求处理期间创建的子协程共享同一根)。容器将解析出的单例存入根协程上下文Viswoole\Core\Coroutine\Context),以 Coroutine::getTopId()(请求根协程 ID)为隔离键,键名为 __container_singleton_ 前缀 + 类名:

text
Worker 进程(常驻)
├── 请求 A(根协程 #1)
│   ├── make(UserService::class) → 实例 A₁(存入协程 #1 上下文)
│   └── 子协程中 make(UserService::class) → 命中单例,返回 A₁
├── 请求 B(根协程 #2)
│   └── make(UserService::class) → 实例 B₁(存入协程 #2 上下文,与 A₁ 互不可见)
└── 请求结束 → 根协程销毁,其上下文中的全部单例随之释放

由此带来的行为:

  • 每请求隔离:两个并发请求各自的 make(UserController::class) 得到不同实例,互不污染;
  • 链路内共享:同一请求内(含子协程)多次 make 同一类返回同一实例;
  • 自动销毁:请求结束随协程上下文释放,无需手动清理,不存在跨请求的状态残留;
  • 非协程环境(CLI、PHPUnit):单例退化为普通的进程级实例池,行为与传统容器一致。

设计意图

容器把「协程隔离」这个底层细节收敛在内部:业务代码照常声明依赖,不必关心自己跑在哪个协程。同理,数据库事务状态(ConnectManager)也按协程上下文隔离,事务之间天然互不干扰。

助手函数

框架在 src/helper.php 提供全局函数,日常开发中无需 use 导入即可使用:

函数说明
app(?string $name = null)无参返回容器实例;传标识则解析对应服务
make(string $abstract, array $params = [])等价于 App::factory()->make()
bind(string $abstract, object|string $concrete)等价于 App::factory()->bind()
invoke(array|callable|string $callable, array $params = [])等价于 App::factory()->invoke()
config(?string $name = null, mixed $default = null)读取配置,支持点号多级键
env(?string $key = null, mixed $default = null)读取环境变量
cache(?string $key = null, mixed $value = null)读取缓存;无参返回 CacheManager
isDebug() / app_debug()是否处于调试模式
dump(mixed $data, ...)打印变量(带调用源定位)
echo_log(string|int $message, ...)输出一条控制台文本日志
getRootPath() / getVendorPath() / getConfigPath() / getAppPath() / getEnvPath()各目录路径
getVersion()框架版本号
php
// 典型的日常用法
bind(PaymentInterface::class, AlipayPayment::class); // 绑定
$payment = make(PaymentInterface::class);            // 解析
$debug   = config('app.debug', false);               // 配置
$result  = invoke([$service, 'handle'], ['id' => 1]); // 注入调用

下一步