FleaPHP 是一个轻量级的 PHP MVC 框架,采用 PSR-4 命名空间标准和 Composer 自动加载机制,支持 PHP 7.4+。
- PSR 标准:PSR-11 容器、PSR-16 缓存、PSR-3 日志
- MVC 架构:模型 - 视图 - 控制器清晰分离
- 路由器:RESTful 路由、路由分组、命名路由
- 中间件:洋葱模型管道
- JWT 认证:HS256 签名
- Context 上下文:可插拔的状态管理
- TableDataGateway:简洁的数据库 CRUD
- 关联查询:HAS_ONE、HAS_MANY、BELONGS_TO、MANY_TO_MANY
- PHP: 7.4+
- Composer: 依赖管理
- 数据库: MySQL 5.0+ 或 PDO 支持的其他数据库
composer install复制示例配置文件:
cp .env.example .env编辑 .env 文件:
# 应用环境
APP_ENV=local
APP_DEBUG=true
# 数据库配置
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=your_password
DB_DATABASE=blog
# Context 配置
CONTEXT_DRIVER=session
CONTEXT_IDENTITY=session
# JWT 配置
JWT_SECRET=your-secret-key-change-this
JWT_TTL=7200
# 日志配置
LOG_ENABLED=true
LOG_LEVEL=debug
LOG_FILENAME=app.log
LOG_FILE_DIR=cache日志配置说明:
| 配置项 | 说明 | 默认值 |
|---|---|---|
LOG_ENABLED |
是否启用日志 | false |
LOG_LEVEL |
日志级别(debug/info/warning/error) | debug |
LOG_FILENAME |
日志文件名 | app.log |
LOG_FILE_DIR |
日志文件目录(相对路径或绝对路径) | cache |
注意:logFileDir 配置支持相对路径和绝对路径。使用相对路径时,建议使用 __DIR__ . '/../cache' 形式指定绝对路径,避免因工作目录不同导致路径解析失败。
mysql -u root -p < blog.sql# 在项目根目录执行
php bin/flea-cli --project-dir=demo
# 访问 http://127.0.0.1:8081/index.php框架主入口类,提供静态方法访问核心功能:
// 加载环境变量
\FLEA::loadEnv(__DIR__ . '/../.env');
// 加载应用配置
\FLEA::loadAppInf(__DIR__ . '/../App/Config.php');
// 获取配置项
$controller = \FLEA::getAppInf('defaultController');
// 获取单例对象
$container = \FLEA::getSingleton(\FLEA\Container::class);
// 启动 MVC
\FLEA::runMVC();实现 PSR-11 标准的依赖注入容器:
use FLEA\Container;
$container = Container::getInstance();
// 注册对象
$container->register(new MyService(), 'myService');
// 获取单例
$service = $container->singleton(MyService::class);
// 检查是否存在
if ($container->has('myService')) {
$service = $container->get('myService');
}配置加载顺序(优先级从高到低):
.env.{APP_ENV}环境文件.env基础配置App/Config.php应用配置FLEA\Config\Defaults框架默认配置
// 获取配置
$dbHost = \FLEA::getAppInfValue('dbDSN', 'host');
$jwtSecret = \FLEA::getAppInf('jwtSecret', '');
// 设置配置
\FLEA::setAppInfValue('dbDSN', 'host', '192.168.1.100');use FLEA\Request;
$request = Request::current();
// 请求方法
$method = $request->method();
$isPost = $request->isPost();
$isAjax = $request->isAjax();
// 获取参数
$id = $request->input('id');
$name = $request->get('name', 'default');
$data = $request->post('data');
// JSON 请求体
$json = $request->json();
$userId = $request->json('user_id');
// 请求头
$token = $request->header('Authorization');
$ip = $request->ip();
$uri = $request->uri();use FLEA\Response;
// 成功响应
Response::success([
'id' => 1,
'name' => 'John'
]);
// 错误响应
Response::error('资源未找到', 404);
// 自定义响应
Response::make()
->code(200)
->header('X-Custom', 'value')
->json($data);
// 分页响应
Response::paginate($items, $total, $page, $pageSize);统一响应结构:
// 成功
{"code": 0, "message": "ok", "data": {...}}
// 错误
{"code": -1, "message": "error message", "data": null}use FLEA\Router;
// 基本路由
Router::get('/users', 'UserController@index');
Router::post('/users', 'UserController@store');
Router::put('/users/{id}', 'UserController@update');
Router::delete('/users/{id}', 'UserController@destroy');
// 带正则参数的路由
Router::get('/users/{id:\d+}', 'UserController@show');
Router::get('/posts/{slug:[a-z-]+}', 'PostController@showBySlug');
// 命名路由
Router::get('/users', 'UserController@index')->name('users.index');
$url = Router::urlFor('users.index');
// 路由分组
Router::group('/admin', function() {
Router::get('/dashboard', 'AdminController@dashboard');
Router::get('/settings', 'AdminController@settings');
}, [new AuthMiddleware()]);
// 任何方法
Router::any('/webhook', 'WebhookController@handle');
// RESTful 资源路由(一行生成 7 条路由)
Router::resource('post', 'PostController');
// 只保留部分方法
Router::resource('post', 'PostController', ['only' => ['index', 'show']]);
// 排除部分方法
Router::resource('post', 'PostController', ['except' => ['create', 'edit']]);Router::resource() 生成的路由表:
| 方法 | URI | 处理器 | 路由名 |
|---|---|---|---|
| GET | /{name} | {controller}@index | {name}.index |
| GET | /{name}/create | {controller}@create | {name}.create |
| POST | /{name} | {controller}@store | {name}.store |
| GET | /{name}/{id} | {controller}@show | {name}.show |
| GET | /{name}/{id}/edit | {controller}@edit | {name}.edit |
| PUT | /{name}/{id} | {controller}@update | {name}.update |
| PUT | /{name}/{id} | {controller}@update | {name}.update.post (fallback) |
| DELETE | /{name}/{id} | {controller}@destroy | {name}.destroy |
| POST | /{name}/{id} | {controller}@destroy | {name}.destroy.post (fallback) |
说明:
resource()方法一行代码生成 7 条 RESTful 路由- 支持
only(白名单)和except(黑名单)选项过滤路由 - update 和 destroy 额外注册 POST fallback 路由,兼容 HTML 表单只支持 GET/POST 的限制
- 路由名格式:
{name}.{action}(如post.index、post.update.post)
namespace FLEA\Middleware;
interface MiddlewareInterface
{
public function handle(callable $next): void;
}namespace App\Middleware;
use FLEA\Middleware\MiddlewareInterface;
class CheckAdminMiddleware implements MiddlewareInterface
{
public function handle(callable $next): void
{
// 前置处理:检查用户是否为管理员
$user = flea_context()->get('user');
if (!$user || !$user['is_admin']) {
\FLEA\Response::error('权限不足', 403);
return;
}
// 调用下一个中间件或处理器
$next();
// 后置处理(如果需要)
}
}// 全局中间件(对所有请求生效)
\FLEA::middleware(new \FLEA\Middleware\CorsMiddleware());
\FLEA::middleware(new \App\Middleware\CheckAdminMiddleware());
// 路由级中间件
Router::get('/admin/dashboard', 'AdminController@dashboard', [
new \FLEA\Middleware\AuthMiddleware(),
new \App\Middleware\CheckAdminMiddleware()
]);| 中间件 | 说明 |
|---|---|
CorsMiddleware |
CORS 跨域支持 |
AuthMiddleware |
JWT 认证检查 |
RateLimitMiddleware |
请求限流 |
namespace App\Controller;
use FLEA\Controller\Action;
use App\Model\Post;
class PostController extends Action
{
protected Post $postModel;
public function __construct()
{
parent::__construct('Post');
$this->postModel = new Post();
}
// 生命周期回调
public function beforeExecute($actionMethod): void
{
// 在 action 之前执行
}
public function afterExecute($actionMethod): void
{
// 在 action 之后执行
}
}class PostController extends Action
{
// 列表页
public function actionIndex(): void
{
$posts = $this->postModel->findAll(['status' => 1]);
$this->getView()->assign('posts', $posts);
$this->getView()->display('post/index.php');
}
// 详情页
public function actionView(): void
{
$id = $this->request->input('id');
$post = $this->postModel->find($id);
if (!$post) {
\FLEA\Response::error('文章未找到', 404);
return;
}
$this->getView()->assign('post', $post);
$this->getView()->display('post/view.php');
}
// 创建(GET 显示表单,POST 处理提交)
public function actionCreate(): void
{
if ($this->request->isPost()) {
$data = [
'title' => $this->request->post('title'),
'content' => $this->request->post('content'),
];
$id = $this->postModel->create($data);
\FLEA\Response::success(['id' => $id]);
return;
}
$this->getView()->display('post/create.php');
}
// 编辑
public function actionEdit(): void
{
$id = $this->request->input('id');
$post = $this->postModel->find($id);
if ($this->request->isPost()) {
$data = [
'id' => $id,
'title' => $this->request->post('title'),
'content' => $this->request->post('content'),
];
$this->postModel->update($data);
\FLEA\Response::success();
return;
}
$this->getView()->assign('post', $post);
$this->getView()->display('post/edit.php');
}
// 删除(AJAX 方式)
public function actionDelete(): void
{
$id = $this->request->param('id');
if (!$id) {
\FLEA\Response::error('文章 ID 不能为空', 400);
return;
}
$result = $this->postModel->deletePost((int)$id);
if ($result) {
\FLEA\Response::success(null, '文章删除成功');
} else {
\FLEA\Response::error('文章删除失败', 500);
}
}
}前端 AJAX 调用示例:
function deletePost(id) {
if (!confirm('确定要删除吗?')) {
return;
}
fetch('/post/' + id + '/delete', {
method: 'POST',
headers: {
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
alert('删除成功');
location.href = '/post';
} else {
alert('删除失败:' + data.message);
}
})
.catch(err => {
alert('网络错误:' + err);
});
}class PostController extends Action
{
public function actionExample(): void
{
// 获取视图对象
$view = $this->getView();
// 获取调度器
$dispatcher = $this->getDispatcher();
// URL 生成
$url = $this->url('view', ['id' => 1]);
$url = $this->url(null, ['page' => 2]); // 当前控制器
// 重定向
$this->forward('Post', 'index');
// 判断请求类型
if ($this->isPost()) { }
if ($this->isAjax()) { }
}
}namespace App\Model;
use FLEA\Db\TableDataGateway;
class Post extends TableDataGateway
{
public string $tableName = 'posts';
public string $primaryKey = 'id';
// 自定义查询方法
public function getPublishedPosts(int $limit = 10, int $offset = 0): array
{
return $this->findAll(
['status' => 1],
'created_at DESC',
[$limit, $offset]
);
}
}$postModel = new Post();
// 查询
$post = $postModel->find(1); // 根据主键查询
$post = $postModel->find(['status' => 1], 'id DESC'); // 条件查询
$posts = $postModel->findAll(); // 查询所有
$posts = $postModel->findAll(['status' => 1], 'id DESC', [10, 0]); // 分页
$count = $postModel->findCount(['status' => 1]); // 计数
// 创建
$id = $postModel->create([
'title' => 'New Post',
'content' => 'Content here',
'status' => 1,
]);
// 更新
$postModel->update([
'id' => 1,
'title' => 'Updated Title',
]);
// 删除
$postModel->remove(1);
$postModel->removeByPkv(1);use FLEA;
$dbo = FLEA::getDBO();
// 执行原生 SQL(自动包装为 SqlStatement)
$rows = $dbo->getAll(sql_statement('SELECT * FROM posts WHERE status = 1'));
// 带参数绑定
$row = $dbo->getOne(sql_statement('SELECT COUNT(*) as cnt FROM posts'));
// SqlStatement 类
use FLEA\Db\SqlStatement;
// 只接受 string 或 PDOStatement
$stmt = new SqlStatement('SELECT * FROM users');
// 或
$pdoStmt = $pdo->prepare('SELECT * FROM users');
$stmt = new SqlStatement($pdoStmt);
// 非法类型会抛出 TypeMismatch 异常class Post extends TableDataGateway
{
public string $tableName = 'posts';
// HAS_MANY: 一篇文章有多条评论
public array $hasMany = [
[
'tableClass' => Comment::class,
'foreignKey' => 'post_id',
'mappingName' => 'comments',
],
];
// HAS_ONE: 一篇文章有一个作者
public array $hasOne = [
[
'tableClass' => User::class,
'foreignKey' => 'user_id',
'mappingName' => 'author',
],
];
}
class Comment extends TableDataGateway
{
public string $tableName = 'comments';
// BELONGS_TO: 评论属于一篇文章
public array $belongsTo = [
[
'tableClass' => Post::class,
'foreignKey' => 'post_id',
'mappingName' => 'post',
],
];
}
// 使用关联查询
$post = $postModel->find(1, null, '*', true); // true = 加载关联
$comments = $post['comments']; // HAS_MANY 关联
$author = $post['author']; // HAS_ONE 关联class User extends TableDataGateway
{
public string $tableName = 'users';
// 多对多:用户 - 角色
public array $manyToMany = [
[
'tableClass' => Role::class,
'foreignKey' => 'user_id',
'associationTable' => 'user_roles',
'associationForeignKey' => 'role_id',
'mappingName' => 'roles',
],
];
}// 控制器中
$view = $this->getView();
// 赋值
$view->assign('title', '页面标题');
$view->assign('posts', $posts);
$view->assign(['user' => $user, 'roles' => $roles]);
// 渲染
$view->display('post/index.php');
// 或获取 HTML 内容
$html = $view->fetch('post/index.php');<!-- App/View/post/index.php -->
<!DOCTYPE html>
<html>
<head>
<title><?php echo $title; ?></title>
</head>
<body>
<h1>文章列表</h1>
<?php foreach ($posts as $post): ?>
<div class="post">
<h2><?php echo htmlspecialchars($post['title']); ?></h2>
<p><?php echo nl2br($post['content']); ?></p>
</div>
<?php endforeach; ?>
</body>
</html>用于 API 等不需要视图的场景:
// Config.php
return [
'view' => \FLEA\View\NullView::class,
];
// 或控制器中
public function actionApi(): void
{
\FLEA\Response::success($data);
}use FLEA\Auth\Jwt;
// 基本签发
$token = Jwt::encode([
'user_id' => 123,
'username' => 'john',
]);
// 指定有效期(秒)
$token = Jwt::encode(['user_id' => 123], 3600);
// 完整配置
$token = Jwt::encode([
'user_id' => 123,
'exp' => time() + 3600, // 过期时间
'iat' => time(), // 签发时间
'iss' => 'my-app', // 签发者
]);use FLEA\Auth\Jwt;
use FLEA\Auth\JwtException;
try {
// 解码并验证
$payload = Jwt::decode($token);
// 或者只验证不获取 payload
if (Jwt::verify($token)) {
// Token 有效
}
} catch (JwtException $e) {
// 验证失败:Token 过期、签名无效等
\FLEA\Response::error('Token 无效', 401);
}// 路由中使用
Router::get('/api/profile', 'UserController@profile', [
new \FLEA\Middleware\AuthMiddleware()
]);
// 或全局注册
\FLEA::middleware(new \FLEA\Middleware\AuthMiddleware());class UserController extends Action
{
public function actionProfile(): void
{
// 从 Context 获取用户信息(由 AuthMiddleware 设置)
$user = flea_context()->get('user');
if (!$user) {
\FLEA\Response::error('未登录', 401);
return;
}
\FLEA\Response::success($user);
}
}Context 提供请求级别的状态管理,替代传统的 $_SESSION。
use FLEA\Context\Context;
// 通过容器获取
$context = \FLEA::getSingleton(Context::class);
// 或使用全局辅助函数
$context = flea_context();
// 存储数据
flea_context()->set('user_id', 123);
flea_context()->set('cart', ['item1', 'item2']);
// 读取数据
$user_id = flea_context()->get('user_id');
$cart = flea_context()->get('cart', []); // 带默认值
// 检查键是否存在
if (flea_context()->has('user_id')) {
// ...
}
// 删除数据
flea_context()->remove('user_id');// App/Config.php
return [
// 存储驱动:session/redis/file/database
'contextDriver' => env('CONTEXT_DRIVER', 'session'),
// 身份标识:session/jwt/api-key/request-id
'contextIdentity' => env('CONTEXT_IDENTITY', 'session'),
];# Session 驱动(默认)
CONTEXT_DRIVER=session
# Redis 驱动
CONTEXT_DRIVER=redis
CONTEXT_REDIS_HOST=127.0.0.1
CONTEXT_REDIS_PORT=6379
CONTEXT_REDIS_PASSWORD=
CONTEXT_REDIS_PREFIX=fleaphp:context:
# File 驱动
CONTEXT_DRIVER=file
CONTEXT_FILE_PATH=/path/to/context/data
# Database 驱动
CONTEXT_DRIVER=database
CONTEXT_DB_TABLE=contexts
CONTEXT_DB_FIELD_ID=context_id
CONTEXT_DB_FIELD_DATA=context_data# Session ID(默认)
CONTEXT_IDENTITY=session
# JWT 用户
CONTEXT_IDENTITY=jwt
JWT_SECRET=your-secret-key
# API Key
CONTEXT_IDENTITY=api-key
CONTEXT_API_KEY_HEADER=X-API-Key
# Request ID
CONTEXT_IDENTITY=request-id
CONTEXT_REQUEST_ID_HEADER=X-Request-IDFLEA 框架内置分布式链路追踪支持,通过 TraceContext 实现服务间调用链的自动追踪。
use FLEA\Context\TraceContext;
// 获取 TraceID(全局唯一请求 ID)
$traceId = TraceContext::getTraceId();
// 获取完整 TraceID(含 SpanID)
$fullId = TraceContext::getFullTraceId(); // 格式:trace_id-span_id
// 获取当前 SpanID
$spanId = TraceContext::getSpanId();
// 生成子 SpanID(用于下游调用)
$childSpan = TraceContext::childSpan();abc123ef456-1.2.1
│ │ │
│ │ └─ SpanID 层级(服务调用深度)
│ └─ SpanID
└─ TraceID(22 位:8 位时间戳 +6 位随机数 +8 位客户端 ID)
HttpClient 自动传递 TraceID:
当使用 HttpClient 发起服务间调用时,框架自动将 TraceID 添加到请求头:
// 服务 A 调用服务 B
$result = \FLEA\Helper\HttpClient::get('http://service-b/api/users');
// 自动添加请求头:X-Trace-Id: abc123-1被调用服务接收 TraceID:
PHP 自动将 HTTP 请求头转换为 $_SERVER 变量:
| HTTP 请求头 | PHP $_SERVER 变量 |
|---|---|
X-Trace-Id |
$_SERVER['HTTP_X_TRACE_ID'] |
Traceparent |
$_SERVER['HTTP_TRACEPARENT'] |
TraceContext 从 $_SERVER 读取外部传入的 TraceID,形成完整调用链。
调用链示例:
用户请求 → 服务 A → 服务 B → 服务 C
↓ ↓ ↓
abc123-1 abc123-1.1 abc123-1.1.1
日志自动记录 TraceID,便于问题排查:
log_message('处理用户请求', \Psr\Log\LogLevel::INFO);
// 输出:[2026-04-02 12:00:00] INFO: 处理用户请求 [trace_id=abc123-1.2]// 服务 A:接收用户请求
class OrderController extends Action
{
public function actionCreate(): void
{
// 获取当前 TraceID
$traceId = TraceContext::getTraceId();
log_message('创建订单请求'); // 自动记录 TraceID
// 调用服务 B(自动传递 TraceID)
$result = HttpClient::post('http://service-b/api/payment', [
'order_id' => 123,
'amount' => 99.99,
]);
if ($result['success']) {
Response::success($result['data']);
} else {
Response::error('支付失败', 500);
}
}
}
// 服务 B:接收服务 A 的调用
class PaymentController extends Action
{
public function actionPayment(): void
{
// TraceContext::init() 自动从 HTTP_X_TRACE_ID 读取
$traceId = TraceContext::getTraceId(); // 与服务 A 相同
$fullId = TraceContext::getFullTraceId(); // abc123-1.1
log_message('处理支付'); // 日志中记录相同 TraceID
// 处理支付逻辑...
Response::success(['payment_id' => 456]);
}
}use FLEA;
// 获取日志实例
$log = FLEA::getSingleton(FLEA\Log::class);
// 记录日志
$log->debug('调试信息', ['user_id' => 123]);
$log->info('用户登录', ['username' => 'john']);
$log->warning('警告信息');
$log->error('错误发生', ['error' => $e->getMessage()]);
$log->critical('严重错误');
// 获取 Trace ID(用于请求追踪)
$traceId = $log->getTraceId();
// 响应头中返回 Trace ID
header('X-Trace-Id: ' . $traceId);// 获取缓存
$data = FLEA::getCache('user_123');
$data = FLEA::getCache('user_123', 3600); // 指定有效期
// 写入缓存
FLEA::writeCache('user_123', $userData);
// 删除缓存
FLEA::purgeCache('user_123');// App/Config.php
return [
// 缓存驱动
'cacheProvider' => \FLEA\Cache\FileCache::class,
// 或使用 Redis
// 'cacheProvider' => \FLEA\Cache\RedisCache::class,
];your-app/
├── App/
│ ├── Config.php # 应用配置
│ ├── Controller/ # 控制器
│ │ ├── Admin/ # 后台控制器
│ │ └── Api/ # API 控制器
│ ├── Model/ # 模型
│ │ └── User.php
│ ├── Middleware/ # 自定义中间件
│ │ └── CheckAdminMiddleware.php
│ └── View/ # 视图
│ ├── layouts/ # 布局模板
│ └── user/ # 用户相关视图
├── public/
│ └── index.php # Web 入口
├── .env # 环境配置
└── vendor/ # 依赖
| 类型 | 约定 | 示例 |
|---|---|---|
| 控制器 | 复数 + Controller | UsersController |
| 模型 | 单数 | User |
| 动作方法 | action + 驼峰 | actionUserProfile() |
| 视图文件 | {controller}/{action}.php |
user/profile.php |
// 在 Config.php 中配置
return [
// 开发环境开启错误显示
'displayErrors' => env('APP_DEBUG', false),
// 生产环境使用友好错误
'friendlyErrorsMessage' => true,
];
// 或在控制器中捕获异常
public function actionShow(): void
{
try {
$post = $this->postModel->find($id);
} catch (\Exception $e) {
\FLEA\Response::error('加载失败', 500);
return;
}
}// 在模型中定义验证规则
class Post extends TableDataGateway
{
public array $validateRules = [
'title' => [
'required' => true,
'minLength' => 5,
'maxLength' => 255,
],
'content' => [
'required' => true,
'minLength' => 10,
],
];
}
// 自动验证(需在创建实例时启用)
$postModel = new Post(['autoValidating' => true]);
$postModel->create($data);检查 .env 文件中的数据库配置是否正确,确保 MySQL 服务已启动。
确保 cache/ 目录可写:
chmod -R 777 cache/运行以下命令重新生成自动加载文件:
composer dump-autoload确保使用 PHP 7.4+:
php74 -v- SPEC.md - 框架规格说明书
- README.md - 项目主页
- demo/APP_USAGE_GUIDE.md - 博客应用使用手册