项目结构介绍

本文解释 Viswoole 项目的目录组织方式与请求的完整生命周期,帮助你理解「框架在什么时候做了什么」——这是在常驻内存(Persistent Memory)环境中写出正确代码的前提。

目录总览

composer create-project 创建的应用为例:

text
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/日志、缓存、路由缓存等动态产物需保证运行用户可读写
viswooleCLI 入口,所有 php viswoole ... 命令由此启动定义 BASE_PATH 并引导 App::factory()

CLI 入口文件

viswoole 文件是框架的命令行入口,内容极简:

php
<?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 后:

text
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 进程就绪

三个要点:

  1. 服务提供者在进程启动时注册一次,之后常驻内存,因此修改服务提供者后必须完整重启服务(见 生产环境配置)。
  2. app.debug 默认值为 trueconfig/app.php),生产环境务必通过 .env 设为 false
  3. 懒加载配置在 AppInitialized 事件后才加载,启动早期不可访问,详见 配置文件

请求处理阶段

每个 HTTP 请求由 Swoole 的 onRequest 回调进入框架(Viswoole\HttpServer\HttpEventHandle::onRequest):

text
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 属性在请求之间不会重置。常驻内存的完整红线清单见 核心概念

全局共享的只有两类对象:进程级全局服务(logcachedbrouterconfig 等)与只读元数据。数据库与 Redis连接由连接池(Connection Pool)管理并自动做协程隔离,业务代码不要手动缓存连接。

配置体系概览

Viswoole 的配置分两层:

载体特点
环境变量.env 文件承载随环境变化的值(密码、开关),env() 读取
应用配置config/ 目录承载结构性配置,文件名为一级键,config() 点号多级读取

典型用法是在 config/ 中用 env() 引用环境变量,实现「一套代码,多环境部署」:

php
// config/database.php(节选)
'default' => env('DATABASE_DEFAULT', 'default'),

详细规则见 环境变量配置文件

从 FPM 迁移的典型误区

最后列出三个从传统 PHP-FPM 迁移过来时最容易踩的坑,它们都源于「常驻内存」:

  1. 在请求中缓存跨请求状态到 static 属性——Worker 进程不随请求销毁,static 属性会一直存活并被后续请求读到脏数据;
  2. 手动管理数据库 / Redis 连接——连接由连接池按协程调度,手动 new 或长期持有连接会破坏池化并引发并发错乱;
  3. 期望改完代码立即生效——代码已加载进内存,需要 server:reload 或完整重启,本地开发可用文件监控脚本辅助自动重启。

理解这些差异后,日常开发体验与 FPM 并无二致——框架已经在协程层面完成了上下文隔离。

下一步