dfer / df-php
由Df打造的php版的Mvc框架,结构简洁,使用方便
Package info
gitcode.com/dofun333/dfphp.dfer.site
Language:JavaScript
Type:project
pkg:composer/dfer/df-php
Requires
- dfer/df-php-core: >=1.0.0
- dfer/df-php-root: >=1.0.0
This package is auto-updated.
Last update: 2026-06-23 08:34:00 UTC
README
轻量级 PHP MVC 框架,遵循传统 MVC 结构,沿袭 ASP.NET MVC 的项目风格,借鉴了微擎、ThinkPHP 的部分设计,以实用为出发点,适合小型网站与接口的快速开发。
- 作者:Dfer · www.dfer.site
- 工作 QQ:3504725309 · QQ 群:76673820
- 当前版本:8.x(基于 PHP 8.0)
目录
框架简介
DfPHP 是 dfer 在学习 PHP 时独立设计的框架,整体架构独立于 ThinkPHP 等主流框架。核心功能通过自研 Composer 组件实现:
| 组件 | 路径 | 说明 |
|---|---|---|
| df-php-root | /vendor/dfer/df-php-root/ | 框架基础结构同步 |
| df-php-core | /vendor/dfer/df-php-core/ | 框架核心功能 |
| tools | /vendor/dfer/tools/ | 常用工具类集成 |
/extend— 自定义拓展类目录/extend/command/— 命令行调用类/df— 命令行入口脚本
快速开始
composer create-project dfer/df-php
创建 df-php 文件夹,包含完整项目源码。
运行环境
- PHP 8.0(已安装 Redis 扩展)
- MySQL 5.7+
- Nginx / Apache
- 服务器最低配置:1 CPU · 1 GB RAM · 1 Mbps
指定 PHP 版本运行(Windows)
"G:\Program Files\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe" "C:\ProgramData\ComposerSetup\bin\composer.phar" update
"G:\Program Files\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe" df
"D:\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe" "D:\Program Files\composer\composer.phar" update
"D:\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe" df
指定 PHP 版本运行(Linux)
/www/server/php/80/bin/php /usr/bin/composer update
/www/server/php/80/bin/php df
目录结构
/ ← 项目根目录(Web 根目录之外)
├── areas/ # 模块目录(HTTP 不可直接访问)
│ ├── admin/ # 后台模块
│ │ ├── controller/ # 控制器
│ │ ├── model/ # 数据模型
│ │ ├── validate/ # 验证器
│ │ ├── public/ # 布局模板(common.htm、icon_share.htm 等)
│ │ └── view/ # 内容模板
│ └── homepage/ # 前台模块
│ ├── controller/
│ ├── public/ # 布局模板
│ └── view/
├── extend/ # 自定义拓展类
│ └── command/ # 命令行类
├── public/ # ← Nginx document root
│ ├── index.php # Web 入口
│ └── view/
│ └── {area}/public/assets/ # 静态资源(CSS/JS/图片)
├── data/
│ ├── cache/ # 模板缓存(可删除)
│ └── config/ # 配置文件
├── vendor/ # Composer 依赖
└── df # 命令行入口
视图查找规则
框架优先在 areas/ 中查找,找不到再降级到 public/view/:
| 类型 | 优先路径 | 降级路径 |
|---|---|---|
| 内容模板 | areas/{area}/view/{ctrl}/{action}.htm | public/view/{area}/{ctrl}/{action}.htm |
| 布局模板 | areas/{area}/public/{layout}.htm | public/view/{area}/public/{layout}.htm |
| 静态资源 | — | public/view/{area}/public/assets/ |
安全说明
Nginx document root 指向 public/,areas/ 完全在 Web 根目录之外,模板 .htm 文件无法被 HTTP 直接访问。
路由与控制器
URL 规则
http://domain/{admin_url}/{area}/{ctrl}/{action}/{param}
↓
http://example.com/df/article/list/3
↑ ↑ ↑ ↑
admin area ctrl action/param
admin_url:后台入口前缀,配置于data/config/web.php→admin_url(默认df)area:模块目录名(admin/homepage/ 自定义)ctrl:控制器类名前缀(ArticleController→article)action:方法名(list→public function list($param))param:传入$param参数
控制器
namespace areas\admin\controller;
class ArticleController extends BaseController
{
public function index($param)
{
$list = ArticleModel::select();
$this->view(compact('list')); // 渲染视图
}
public function save()
{
$id = post('id');
$dt = post('data');
$ret = ArticleModel::where($id)->update($dt);
if ($ret) StaticHtml::flush('article'); // 刷新静态缓存
$this->jumpPrompt($ret, 'index'); // 跳转提示
}
}
常用助手函数
post('key') // 读取 POST 数据(整个 data[] 或指定 key)
get('key') // 读取 GET 参数
session_get('key') // 读取 session
session_set('key', v) // 写入 session
split_url('area/ctrl/action') // 生成 URL
to_url('ctrl/action') // 跳转(同 area 内)
config('key', default) // 读取 web.php 配置
视图渲染
// 方式一:传变量数组
$this->view(get_defined_vars());
$this->view(compact('list', 'total'));
// 方式二:链式 assign
$this->assign('list', $list)->assign('total', 10)->view();
// 方式三:指定布局模板
$this->view(get_defined_vars(), 'iconShare'); // 使用 icon_share.htm 布局
// 跳转提示页
$this->jumpPrompt($status, 'index'); // 成功/失败后跳转
$this->jumpPrompt(0, 'index', '错误信息');
模板引擎
布局标签
<!-- 注入到公共模板的 <head> 区域 -->
<df-header> ... </df-header>
<!-- 注入到公共模板的主体区域 -->
<df-body> ... </df-body>
<!-- 注入到公共模板的 </body> 前 -->
<df-footer> ... </df-footer>
<!-- 在 common.htm 中引用对应区域 -->
<df-header />
<df-body />
<df-footer />
输出变量
<!-- 输出 PHP 表达式(会 htmlspecialchars) -->
{:$var}
{:date('Y-m-d')}
{:json_encode($arr)}
<!-- 循环内输出(已转义) -->
{$var}
<!-- 循环内原始输出(不转义,支持 HTML) -->
{::var}
条件语句
支持直接写 PHP 表达式,也支持 gt/lt/eq/neq/egt/elt/heq/nheq 别名代替 > < == != >= <= === !==,避免 > < 被模板正则误截断:
<df-if $status eq 1>
已发布
<df-elif $status eq 2>
处理中
<df-else>
草稿
</df-if>
<!-- 也可直接写表达式(不含 > < 时) -->
<df-if $count != 0>有数据</df-if>
<!-- 推荐:控制器预计算布尔值 -->
<df-if $has_data>有数据</df-if>
循环
<df-each $list>
<p>{::title}</p>
<p>{::content}</p>
</df-each>
执行 PHP 代码
<df-code>
use areas\admin\model\{ArticleModel};
$data = ArticleModel::select();
</df-code>
JS 内嵌 PHP 值
<script>
var url = /*code {:$url} code*/;
var delay = /*code{:$delay * 1000}code*/;
</script>
TP8 兼容模板标签
以下标签与原有 <df-*> 标签并存,可混合使用。
原样输出 {literal}
{literal}
这里的 {$var} 和 {:expr} 不会被解析
{/literal}
变量赋值 {assign}
{assign name="title" value="DfPHP" /}
执行 PHP {php}
{php}
$list = ArticleModel::select();
{/php}
条件语句 {if}(同样支持 gt/lt/eq 等别名)
{if $status eq 1}
已发布
{elseif $status eq 0 /}
草稿
{else /}
未知
{/if}
遍历 {foreach} / {volist}
{foreach $list as $k => $v}
{$v.title}
{/foreach}
<!-- volist 支持 offset / length 分片 -->
{volist name="list" id="vo" key="k" offset="0" length="5"}
{$vo.title}
{/volist}
数值循环 {for}
{for start="1" end="10" step="1" name="i"}
第 {$i} 项
{/for}
空值判断 {empty} / {notempty}
{empty name="list"}暂无数据{/empty}
{notempty name="list"}共 {$list|count} 条{/notempty}
比较标签
{eq name="status" value="1"}已启用{/eq}
{gt name="score" value="90"}优秀{/gt}
{elt name="level" value="3"}初级{/elt}
IN 判断
{in name="role" value="1,2,3"}管理员{/in}
{notin name="role" value="0"}普通用户{/notin}
点语法 & 管道修饰符
{$user.name}
{$config.layout.title}
{$name|htmlspecialchars}
{$content|strip_tags|substr=0,100}
数据库操作
引用模型
use areas\admin\model\{ArticleModel, LinkModel, MusicModel, MessageModel, NotepadModel};
基础 CRUD
// 查询
$list = ArticleModel::select();
$list = ArticleModel::order(['id', 'desc'])->select();
$row = ArticleModel::where(3)->find();
$row = ArticleModel::where(['key' => 'layout'])->first();
$val = ConfigModel::where(['key' => 'layout'])->value('val');
// 新增(返回新 id)
$id = ArticleModel::insert(['title' => '标题', 'content' => '内容']);
// 修改
ArticleModel::where(3)->update($dt);
ArticleModel::where(['key' => 'layout'])->update(['val' => $dt]);
// 删除
ArticleModel::where(3)->del();
ArticleModel::where(['type' => 3])->del();
ArticleModel::del(); // 清空表
模型属性配置
class ArticleModel extends Model {
protected $pk = 'id';
protected $table = 'my_article'; // 完整表名(覆盖自动推导)
protected $autoWriteTimestamp = true; // false | true | 'datetime' | 'int'
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
protected $deleteTime = 'delete_time'; // 软删除字段(null 则不启用)
protected $json = ['extra']; // JSON 字段自动序列化
protected $jsonAssoc = true; // 取出时转为数组
protected $siteScoped = true; // 多站隔离(自动 WHERE site_id=当前站)
protected $type = [
'status' => 'int',
'price' => 'float',
];
}
TP8 风格静态方法
$id = ArticleModel::create(['title' => '标题']);
$row = ArticleModel::get(3);
ArticleModel::destroy(3);
ArticleModel::destroy([1, 2, 3]);
高级查询
ArticleModel::where('status', 1)->select();
ArticleModel::where('id', '>', 10)->select();
ArticleModel::whereOr('type', 2)->select();
ArticleModel::whereIn('id', [1, 2, 3])->select();
ArticleModel::whereBetween('id', [5, 20])->select();
ArticleModel::whereNull('delete_time')->select();
ArticleModel::whereLike('title', '%PHP%')->select();
ArticleModel::whereRaw('status = 1 AND id > 10')->select();
// 聚合
ArticleModel::count();
ArticleModel::max('id');
ArticleModel::sum('views');
// 分页(页码从 1 开始)
ArticleModel::page(2, 15)->select();
// 分页结构(返回 data / total / page / size / pages)
$result = ArticleModel::paginate(10, 2);
// 某列值数组
ArticleModel::column('title'); // ['标题A', '标题B', ...]
ArticleModel::column('title', 'id'); // [1=>'标题A', ...]
// JOIN
ArticleModel::alias('a')
->leftJoin('roles r', 'a.role = r.id')
->field(['a.title', 'r.nm'])
->select();
// GROUP BY / HAVING / DISTINCT
ArticleModel::group('type')->having('count(*) > 1')->select();
ArticleModel::distinct()->column('type');
// 自增/自减
ArticleModel::where('id', 1)->increment('views');
ArticleModel::where('id', 1)->decrement('stock', 2);
// 批量插入
ArticleModel::insertAll([
['title' => 'A'],
['title' => 'B'],
]);
// 分批处理
ArticleModel::chunk(100, function($rows) {
foreach ($rows as $row) { /* 处理 */ }
});
// 获取 SQL 字符串(不执行)
$sql = ArticleModel::where('id', 1)->buildSql();
软删除
ArticleModel::where('id', 3)->del(); // 软删除(写时间戳)
ArticleModel::withTrashed()->select(); // 包含软删除
ArticleModel::onlyTrashed()->select(); // 仅软删除
ArticleModel::onlyTrashed()->where('id', 3)->restore(); // 恢复
页面静态化
将动态页面预渲染为静态 HTML,前台直接返回静态文件,提升速度、降低数据库压力、利于 SEO。
工作原理
| 环节 | 实现 |
|---|---|
| 配置驱动 | data/config/static.php 声明路由、标签 |
| 内部渲染 | StaticHtml::render() 用 ob 缓冲渲染,不走 HTTP 抓取 |
| 产出 | public/static/{area}/{ctrl}/{action}[/param].html |
| 透明调用 | 前台 GET 命中静态文件直接返回(X-Static-Cache: HIT) |
| 增量再生 | 按标签重建受影响页面 |
配置 data/config/static.php
return [
'enable' => true,
'dir' => 'static',
'expire' => 0, // 0=永不过期
'base_url' => 'http://dfphp.dfer.site.local',
'routes' => [
'homepage/home/index' => ['tags' => ['home', 'article', 'link', 'music', 'layout']],
'homepage/home/readme' => ['tags' => ['article']],
// 动态路由:provider 返回参数列表
'homepage/home/article' => [
'tags' => ['article'],
'provider' => fn() => \areas\admin\model\ArticleModel::column('id'),
],
],
];
三种刷新方式
php df static # 查看各路由状态
php df static build # 全量重建
php df static build homepage # 只重建指定站点
php df static flush article # 按标签增量重建
php df static clear # 清空全部
控制器内自动失效:
$ret = ArticleModel::where($id)->update($dt);
if ($ret) StaticHtml::flush('article');
Nginx 直出(可选)
location / {
try_files $uri /static$uri.html @dfphp;
}
location @dfphp {
rewrite ^/(.*)$ /index.php?s=$1;
}
多站合一
一套框架 + 一个数据库 + 一个公共后台,托管多个完全独立的网站。每个 area 即一个站点,内容按 site_id 隔离。
工作机制
| 环节 | 说明 |
|---|---|
| 站点注册 | site 表登记 area / 域名 / 展示配置(JSON);后台「站点管理」增删改,自动重写 data/config/sites.php |
| 域名路由 | Web.php 按 HTTP_HOST 定位 area;后台 /df 可从任意域名进入 |
| 内容隔离 | 模型声明 $siteScoped=true,查询自动加 WHERE site_id=当前站,写入自动填充 site_id |
| 当前站点 | 前台按域名、后台按顶栏切换器(session)、CLI 按构建目标;未命中回落主站(id=1) |
| 静态部署 | 各站产出 public/static/{area}/...;nginx 按域名 try_files 到对应目录 |
新增站点步骤
# 1) 后台「站点管理 → 新增站点」:填 name / area / 域名 / 展示配置
# 2) 建立前端 area
mkdir areas/{area}/controller areas/{area}/view/home
# 3) data/config/static.php 增加该站路由
# 4) 生成静态
php df static build {area}
# 5) nginx 增加 server 块(root 仍为 public/)
# 6) 本地调试:hosts 加 127.0.0.1 {域名}
模板/控制器助手
current_site() // 当前站点行(site 表一行)
site_config('title') // 当前站展示配置(config JSON 字段)
site_id() // 当前站 id
已有库升级
php df upgrade # 幂等迁移:补 site 表 / site_id 列 / 主站种子 / 菜单
API 接口
框架内置 ApiTrait,通过 Authorization: Bearer {token} header 区分 API 请求和普通网页请求,同一个控制器可同时支持两种访问方式。
判断逻辑
请求进来
├─ 有 Authorization: Bearer header → API 模式 → 返回 JSON
│ ├─ $apiPublic = true → 跳过 token 验证(公开接口)
│ └─ $apiPublic = false(默认) → 验证 token → 通过则执行
└─ 无 header → 网页模式 → 渲染模板(session 鉴权)
公开接口(无需任何权限)
namespace areas\admin\controller;
class OpenController extends BaseController
{
protected bool $apiPublic = true; // 跳过 token 验证
public function articles($param)
{
$list = ArticleModel::select();
$this->view(compact('list'));
// 浏览器访问 → 渲染模板
// 带 Authorization header → 直接返回 JSON
}
}
curl http://example.com/df/open/articles/ \
-H "Authorization: Bearer any"
# {"code":0,"msg":"ok","data":{"list":[...]}}
Token 接口
class ApiController extends BaseController
{
// 不声明 $apiPublic,默认走 token 验证
public function articles($param)
{
// $this->apiUser 包含 token 里存的用户信息
$list = ArticleModel::select();
$this->view(compact('list'));
}
}
curl http://example.com/df/api/articles/ \
-H "Authorization: Bearer abc123..."
Token 生命周期
// 颁发(登录成功后调用)
$token = $this->apiTokenIssue(['id' => $user->id, 'nm' => $user->nm], 86400);
// 吊销(退出时)
$this->apiTokenRevoke($token);
// 吊销用户所有 token
$this->apiTokenRevokeAll($userId);
独立 API Area
如需完全独立于 admin 的 API 区域:
// areas/api/controller/BaseController.php
namespace areas\api\controller;
use Dfer\Tools\Modules\Controller;
require_once ROOT . '/extend/ApiTrait.php';
abstract class BaseController extends Controller
{
use \ApiTrait;
public function initialize() {}
}
访问地址:http://domain/api/xxx/yyy/
命令行工具
php df # 查看所有命令
php df static # 查看静态化状态
php df static build # 全量构建静态页
php df static build {area} # 构建指定站点
php df static flush {tag} # 按标签增量重建
php df static clear # 清空所有静态文件
php df static sites # 列出已注册站点
php df upgrade # 多站架构幂等迁移
php df dev:root # 同步基础结构到 df-php-root 项目
php df dev:core # 同步核心代码到 df-php-core 项目
自定义命令
在 extend/command/ 新建类:
// extend/command/MyCmd.php
class MyCmd {
public function run($args) {
echo "hello\n";
}
}
在 data/config/console.php 注册:
'mycmd' => [
'class' => 'command\MyCmd',
'remark' => '我的命令',
],
php df mycmd
前端技术栈(后台)
| 库 | 版本 | 用途 |
|---|---|---|
| Tailwind CSS | Play CDN | 所有样式 |
| Alpine.js | 3.13.5 | 交互(勿升级到 3.14.1) |
| Tabulator | 5.5.4 | 数据表格 |
| Font Awesome | 6.5.1 Free | 图标 |
| jQuery | 3.7.1 | Ajax / 表单 |
| umeditor | 1.2.2 | 富文本编辑器 |
| WebUploader | 0.1.5 | 文件上传 |
| layer.min.js | 本地 | 弹窗 |
Alpine.js 已知 BUG
/node_modules/@df_business/tools-js/extends/js.ext.js 将 Array.prototype.toString 重写为换行分隔,导致 Alpine 内部解析失败。
修复(已在 common.htm 和 icon_share.htm 中加入):
<script>Array.prototype.toString = function() { return Array.prototype.join.call(this, ','); };</script>
伪静态配置
Nginx
location / {
index index.php index.html index.htm;
if (!-e $request_filename) {
rewrite ^/(.*)$ /index.php?s=$1;
}
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; }
location ~ .*\.(js|css)?$ { expires 1h; }
Apache
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?s=$1
注意事项
- 上传到服务器后需给所有文件添加访问权限:
chmod -R 777 * - 页首
<df-code>中不要添加注释,以免格式化工具破坏 PHP 结构 - 默认开启 SSL,降级到 HTTP 后需重启浏览器才能生效
- 更高版本的 PHP 代码审核更严格,无效代码分支应用
die拦截 - 文件名尽量使用英文,避免 Linux 中文兼容问题
- 修改视图后需清除模板缓存:
rm -rf data/cache/* <df-if>表达式中含><时用gtlt别名,或在控制器预计算布尔值
©2017–2025 Dfer.Site