项目结构介绍
本文解释 Viswoole 项目的目录组织方式与请求的完整生命周期,帮助你理解「框架在什么时候做了什么」——这是在常驻内存(Persistent Memory)环境中写出正确代码的前提。
目录总览
以 composer create-project 创建的应用为例:
project/
├── app/ # 应用层:业务代码(PSR-4,命名空间 App\)
│ └── Controller/ # 控制器,注解路由的扫描目录
├── config/ # 配置层:所有应用配置
│ ├── app.php # 应用配置(调试、时区、服务提供者、命令)
│ ├── server.php # Swoole 服务定义(监听地址、端口、进程选项)
│ ├── router.php # 路由全局配置(含路由缓存、API 文档)
│ ├── database.php # 数据库通道配置
│ ├── cache.php # 缓存存储配置
│ ├── log.php # 日志通道配置
│ ├── task.php # 异步任务配置
│ ├── listens.php # 事件监听注册入口
│ ├── lazy/ # 懒加载配置(AppInitialized 后才加载)
│ └── route/
│ └── route.php # 编程式路由定义文件
├── public/ # 可公开访问的静态资源
├── runtime/ # 运行时产物:日志、缓存、路由缓存等(不入版本库)
├── vendor/ # Composer 依赖(含框架源码 viswoole/framework)
├── viswoole # CLI 入口文件
└── .env # 环境变量文件(不入版本库)不要修改 vendor 中的框架代码
框架核心 viswoole/framework 由 Composer 管理,直接修改会在更新时丢失。定制能力应通过服务提供者(Provider)、中间件或事件机制实现。
关键目录说明
| 目录/文件 | 职责 | 边界 |
|---|---|---|
app/ | 业务代码,控制器需位于 app/Controller 供注解路由扫描 | 遵循 App\ 命名空间 |
config/ | 应用配置,文件名为一级配置键 | 支持 php/yml/ini/json,见 配置文件 |
config/lazy/ | 懒加载配置与应用初始化后才执行的注册逻辑 | 不可放启动必需的核心配置 |
runtime/ | 日志、缓存、路由缓存等动态产物 | 需保证运行用户可读写 |
viswoole | CLI 入口,所有 php viswoole ... 命令由此启动 | 定义 BASE_PATH 并引导 App::factory() |
CLI 入口文件
viswoole 文件是框架的命令行入口,内容极简:
<?php
declare(strict_types=1);
use Viswoole\Core\App;
ini_set('display_errors', 'on');
ini_set('memory_limit', '1G');
// 定义项目根路径,App 由此推断 config/、app/ 等目录位置
!defined('BASE_PATH') && define('BASE_PATH', __DIR__);
require __DIR__ . '/vendor/autoload.php';
// 创建全局唯一的应用容器,并运行控制台命令
(function () {
App::factory()->console->run();
})();请求生命周期
理解请求生命周期(Request Lifecycle)的关键在于区分两个阶段:进程启动阶段(只发生一次)与请求处理阶段(每个请求都发生)。
进程启动阶段
执行 php viswoole server:start 后:
php viswoole server:start
└─ App::factory() # ① 创建全局唯一容器
├─ 定义 BASE_PATH,绑定 app/env/config/console/event/server 标识
├─ 读取 app.debug / app.default_timezone
├─ loadService() # ② 注册并启动服务提供者
│ ├─ 框架默认服务:Log / Cache / Middleware / Router / Http / Db
│ ├─ config/app.php 的 services[](用户服务)
│ └─ vendor/services.php(依赖包注册的服务)
│ 每个服务依次执行 register(),全部注册完再依次执行 boot()
└─ 触发 AppInitialized 事件 # ③ Config 此时加载 config/lazy/
└─ Swoole Server 启动,Master/Manager/Worker 进程就绪三个要点:
- 服务提供者在进程启动时注册一次,之后常驻内存,因此修改服务提供者后必须完整重启服务(见 生产环境配置)。
app.debug默认值为true(config/app.php),生产环境务必通过.env设为false。- 懒加载配置在
AppInitialized事件后才加载,启动早期不可访问,详见 配置文件。
请求处理阶段
每个 HTTP 请求由 Swoole 的 onRequest 回调进入框架(Viswoole\HttpServer\HttpEventHandle::onRequest):
Swoole onRequest
├─ ① 封装对象:make(RequestInterface) / make(ResponseInterface)
│ 将 Swoole 原始请求/响应包装为框架对象,每个请求新建
├─ ② 路由分发:$app->router->dispatch(path, method, host)
│ 匹配路由 → 组装中间件管道 → 容器调用控制器方法(参数自动注入)
├─ ③ 响应输出:
│ 返回 ResponseInterface → 直接发送
│ 返回数组/对象 → 自动 JSON
│ 返回其他标量 → 转字符串发送
└─ ④ 异常兜底:交由 config/server.php 中 exception_handle 渲染协程上下文隔离
Swoole 是常驻内存的,但 Viswoole 的容器单例按请求(根协程)隔离:
- 协程环境下,
make()产生的单例存入当前请求根协程的上下文(Coroutine Context); - 同一请求内多次解析得到同一实例,不同请求之间互不可见;
- 请求结束后上下文自动销毁,无需手动清理。
这意味着你可以在服务中安全地持有请求级状态,只要该服务是每请求从容器解析的;反过来,禁止用类的 static 属性保存请求级状态——Worker 进程常驻,static 属性在请求之间不会重置。常驻内存的完整红线清单见 核心概念。
全局共享的只有两类对象:进程级全局服务(log、cache、db、router、config 等)与只读元数据。数据库与 Redis连接由连接池(Connection Pool)管理并自动做协程隔离,业务代码不要手动缓存连接。
配置体系概览
Viswoole 的配置分两层:
| 层 | 载体 | 特点 |
|---|---|---|
| 环境变量 | .env 文件 | 承载随环境变化的值(密码、开关),env() 读取 |
| 应用配置 | config/ 目录 | 承载结构性配置,文件名为一级键,config() 点号多级读取 |
典型用法是在 config/ 中用 env() 引用环境变量,实现「一套代码,多环境部署」:
// config/database.php(节选)
'default' => env('DATABASE_DEFAULT', 'default'),从 FPM 迁移的典型误区
最后列出三个从传统 PHP-FPM 迁移过来时最容易踩的坑,它们都源于「常驻内存」:
- 在请求中缓存跨请求状态到 static 属性——Worker 进程不随请求销毁,static 属性会一直存活并被后续请求读到脏数据;
- 手动管理数据库 / Redis 连接——连接由连接池按协程调度,手动
new或长期持有连接会破坏池化并引发并发错乱; - 期望改完代码立即生效——代码已加载进内存,需要
server:reload或完整重启,本地开发可用文件监控脚本辅助自动重启。
理解这些差异后,日常开发体验与 FPM 并无二致——框架已经在协程层面完成了上下文隔离。
