dfer/df-php

由Df打造的php版的Mvc框架,结构简洁,使用方便

Maintainers

Package info

gitcode.com/dofun333/dfphp.dfer.site

Homepage

Language:JavaScript

Type:project

pkg:composer/dfer/df-php

Transparency log

Statistics

Installs: 22

Dependents: 0

Suggesters: 0


README

轻量级 PHP MVC 框架,遵循传统 MVC 结构,沿袭 ASP.NET MVC 的项目风格,借鉴了微擎、ThinkPHP 的部分设计,以实用为出发点,适合小型网站与接口的快速开发。

  • 作者:Dfer · www.dfer.site
  • 工作 QQ:3504725309 · QQ 群:76673820
  • 当前版本:8.x(基于 PHP 8.0)

目录

  1. 框架简介
  2. 快速开始
  3. 运行环境
  4. 目录结构
  5. 路由与控制器
  6. 模板引擎
  7. TP8 兼容模板标签
  8. 数据库操作
  9. 页面静态化
  10. 多站合一
  11. API 接口
  12. 命令行工具
  13. 前端技术栈
  14. 伪静态配置
  15. 注意事项

框架简介

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}.htmpublic/view/{area}/{ctrl}/{action}.htm
布局模板areas/{area}/public/{layout}.htmpublic/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.phpadmin_url(默认 df
  • area:模块目录名(admin / homepage / 自定义)
  • ctrl:控制器类名前缀(ArticleControllerarticle
  • action:方法名(listpublic 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.phpHTTP_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 CSSPlay CDN所有样式
Alpine.js3.13.5交互(勿升级到 3.14.1)
Tabulator5.5.4数据表格
Font Awesome6.5.1 Free图标
jQuery3.7.1Ajax / 表单
umeditor1.2.2富文本编辑器
WebUploader0.1.5文件上传
layer.min.js本地弹窗

Alpine.js 已知 BUG

/node_modules/@df_business/tools-js/extends/js.ext.jsArray.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> 表达式中含 > < 时用 gt lt 别名,或在控制器预计算布尔值

©2017–2025 Dfer.Site