容器与依赖注入
依赖注入(Dependency Injection)是一种设计模式:对象不自行创建依赖,而是由外部容器注入。Viswoole 的容器(Container)负责全框架的服务绑定、依赖解析与实例生命周期管理——控制器、验证规则、路由处理器都由它创建。
本文所有 API 位于
Viswoole\Core\App与Viswoole\Core\Container,依据框架源码src/Core/App.php、src/Core/Container.php、src/helper.php编写。
App:全局唯一的容器
App 继承自 Container,是框架全局唯一的容器实例。通过 App::factory() 获取,首次调用时自动创建:
use Viswoole\Core\App;
$app = App::factory(); // 全局单例,任意位置重复调用返回同一实例首次创建时按顺序完成以下初始化(App::__construct → initialize() → loadService()):
- 解析并定义
BASE_PATH常量(项目根目录); - 读取
app.debug初始化调试模式(写入 Swoole 共享内存表,跨 Worker 同步); - 读取
app.default_timezone设置默认时区(默认Asia/Shanghai); - 合并框架默认服务、
config/app.php的services[]与vendor/services.php中依赖包注册的服务,依次调用各服务提供者的register()与boot(); - 触发
FrameworkEvent::AppInitialized事件。
只初始化一次
初始化发生在进程启动阶段,之后整个进程生命周期内不会重新执行。修改服务提供者、配置等启动期加载的内容后必须重启服务才能生效。
魔术属性
App 通过容器实现 __get,可以直接以属性方式访问已注册的服务:
$config = App::factory()->config; // 等价于 $app->make('config')
$log = App::factory()->log;框架预定义的服务属性(见 App 类文档注释):
| 属性 | 类型 | 说明 |
|---|---|---|
$app->env | Env | 环境变量管理 |
$app->config | Config | 配置管理 |
$app->console | Console | 控制台 |
$app->log | LogManager | 日志 |
$app->event | Event | 事件管理器 |
$app->server | Server | 服务管理器 |
$app->cache | CacheManager | 缓存 |
$app->middleware | Middleware | 中间件 |
$app->router | Router | 路由 |
$app->request | RequestInterface | 当前 HTTP 请求对象(请求级) |
$app->response | ResponseInterface | 当前 HTTP 响应对象(请求级) |
容器同时支持数组式访问(实现 ArrayAccess):
$dbHost = $app['config']->get('db.host'); // 读取绑定的服务
$app['db'] = MyDb::class; // 等价于 $app->bind('db', MyDb::class)服务绑定 bind
bind() 将一个标识(接口名、类名或自定义字符串)映射到实现:
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());| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
abstract | string | 必填 | 接口名、类名或自定义标识 |
concrete | string|object | 必填 | 实现类名、闭包或已有实例;字符串必须为有效类名,否则抛 TypeError |
三种形态的行为差异:
- 类名:仅记录映射关系,首次
make()时才实例化并缓存单例; - 闭包:首次
make()时执行,返回值作为单例缓存; - 实例:调用
bind()的瞬间即写入单例池,之后make()直接返回该实例。
内置绑定
框架启动时预置了以下绑定(App::$bindings):app、env、config、console、event、server,以及一个指向 Swoole 原生 Server 对象的闭包(便于注入 Swoole 服务实例)。各服务提供者还会通过 Provider::$bindings 追加更多绑定。
解析实例 make
make() 是容器最常用的解析入口:
// 解析绑定标识
$payment = $app->make(PaymentInterface::class); // 返回 AlipayPayment 实例
// 直接解析任意类,自动递归注入构造函数依赖
$service = $app->make(UserService::class);
// 传入构造参数,覆盖自动注入(支持命名参数或按位置)
$service = $app->make(UserService::class, ['repository' => $repo]);| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
abstract | string | 必填 | 绑定标识或类名 |
params | array | [] | 手动传入的构造参数,覆盖依赖注入 |
ALLOW_NEW_INSTANCE:每次新建
默认情况下 make() 对已解析的类缓存单例。若类定义了 ALLOW_NEW_INSTANCE = true 常量,则每次 make() 都创建新实例、不缓存:
class ReportBuilder
{
// 声明后 make(ReportBuilder::class) 每次返回全新实例
public const bool ALLOW_NEW_INSTANCE = true;
}
$a = $app->make(ReportBuilder::class);
$b = $app->make(ReportBuilder::class);
// $a !== $bhas / get / remove
| 方法 | 签名 | 说明 |
|---|---|---|
has | has(string $id): bool | 是否存在绑定或实例 |
get | get(string $id): object | 等价于 make(),未绑定时抛 NotFoundException |
remove | remove(string $abstract): void | 移除指定标识的单例实例(含协程上下文中的) |
getBindings | getBindings(): array | 获取全部绑定映射 |
hasInstance | hasInstance(string $class): bool | 该类是否已有单例实例 |
invokeClass:工厂方法优先
invokeClass() 通过反射创建类实例,自动解析构造函数依赖。特殊规则:若类存在 public static factory() 方法,则优先调用它创建实例,否则使用 __construct:
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()。框架内 Event、LogManager 等管理器也遵循同样的模式。
方法调用与参数注入
容器提供一组 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 = []) | 实例化类并注入构造参数 |
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() 的分发规则:
闭包 → invokeFunction
[类/对象, 方法] 数组 → invokeMethod
'类名::方法名' 字符串 → invokeMethod
类名字符串 → invokeClass
函数名字符串 → invokeFunction参数注入能力
容器对方法/构造参数的处理覆盖以下场景(依据 Container::injectParams()):
| 能力 | 说明 |
|---|---|
| 命名参数 | $params 按参数名匹配 |
| 位置参数 | $params 按索引匹配 |
| 可变参数 | 支持 ...$args,逐项校验 |
| 默认值 | 有默认值的参数可省略 |
| 可空类型 | ?Type 允许传入 null |
| 类类型 | 自动递归解析依赖(含接口绑定) |
| 前置注入注解 | 实现 PreInjectInterface 的 Attribute 在注入前改写参数值 |
| 校验规则注解 | BaseValidateRule 子类 Attribute 在注入时自动校验(详见验证器) |
类型不匹配或校验失败时抛出 Viswoole\Core\Exception\ValidateException,debug 模式下错误消息附带参数位置与方法上下文。
解析钩子
类被 invokeClass 创建后可触发回调,用于统一装饰实例(如注入日志代理):
// 为所有新创建的实例统一打点
$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_ 前缀 + 类名:
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() | 框架版本号 |
// 典型的日常用法
bind(PaymentInterface::class, AlipayPayment::class); // 绑定
$payment = make(PaymentInterface::class); // 解析
$debug = config('app.debug', false); // 配置
$result = invoke([$service, 'handle'], ['id' => 1]); // 注入调用