ThinkPHP 3.1.3 Full完整版实战指南:部署、开发与排坑 简介ThinkPHP 3.1.3完整版压缩包面向PHP初学者、Web开发者及维护旧项目的工程师提供一套可直接运行的经典PHP MVC框架源码。虽然版本较老但核心设计——模型、控制器、视图分离、动态路由、内置模板引擎与数据库操作接口等——仍有很强的学习价值可作为理解PHP框架演进和快速搭建中小型应用的参考基底。资源包大小约1.37MB资源页暂未展示文件总数与类型明细。目前已有280人学习适合需要对照源码研究框架结构、梳理早期版本特性或搭建本地开发环境的读者。解压后即可配置运行便于在真实环境中练习控制器与模型交互、自定义路由规则、使用缓存与日志机制同时对于接手老旧ThinkPHP项目的人员也是一份便于查阅的原始框架素材。1. 项目概述ThinkPHP 3.1.3 Full完整版到底是个什么来头将近十年前的老框架到现在还有人在问、在找、在下载这事本身就说明了一些问题。ThinkPHP 3.1.3 的 Full完整版简而言之就是官方发布的包含完整核心库、全部扩展类库、内置模板引擎、中文文档和示例代码的全量压缩包。相比普通标准版Full版把平时需要单独通过下载或在线获取的功能模块全部提前打包好解压即用不用再去东拼西凑找扩展。那时候国内PHP生态远没有现在这么繁荣Composer还没普及ThinkPHP 3.1.3发布时Composer还只是个雏形装个框架全靠下载压缩包往服务器一丢。Full版的存在本质上解决了一个非常实际的问题很多开发者的服务器环境是内网隔离的根本没有外网权限去拉依赖。Full完整版把所有家当都装在一个包里这对当时的项目部署来说简直是救命的存在。到现在虽然ThinkPHP已经迭代到8.x版本但3.1.3这个老版本依然活跃在一大堆历史遗留项目和早期的CMS系统里。我接触过不少做系统维护的同行手头或多或少都有一两个跑在ThinkPHP 3.1.3上的老项目不敢动、不能动、也动不了。所以这篇博文把3.1.3 Full完整版从环境部署到实战开发到排坑经验全部过一遍希望帮到那些被迫维护老项目的朋友也给刚开始接触这个框架的新手一条相对顺畅的学习路径。1.1 Full完整版和标准版的核心差异先把这个最基础的问题说清楚。标准版Standard精简了很多额外功能核心框架能跑但很多便利组件需要自己额外引入。Full完整版则是在标准版基础上补齐了完整的扩展类库目录Extend/Library包含了ORG类库、COM类库等大量工具类全部驱动数据库驱动、缓存驱动、日志驱动、Session驱动完整的中文文档和API手册官方示例代码和开发规范说明内置的RBAC权限控制模块所需的基础代码选择Full版的核心逻辑其实很简单开发阶段省事部署阶段省心。你不需要纠结某个功能用的类库在不在环境里直接拿过来就能用。代价是压缩包体积大一些但在那个网速和硬盘都不是瓶颈的时代这压根不算什么问题。2. 环境准备与安装部署2.1 版本兼容性一步踩错后面全崩ThinkPHP 3.1.3时代对应的PHP版本主流是5.2到5.4我在生产环境里跑过PHP 5.3和5.4都很稳定。这里必须提醒一句如果你现在还在用ThinkPHP 3.1.3务必确认PHP版本不能太高。PHP 5.6开始有部分函数废弃警告到了PHP 7.0以上直接就是一堆致命错误砸脸。原因很好理解。3.1.3的核心代码大量使用mysql_*系列函数虽然它内部做了驱动封装这些函数在PHP 7.0被彻底移除。另外老版本框架里常见的each()、create_function()等语法在PHP 8.0里也已经删除。所以如果你接手的是一个跑在PHP 7.2以上的环境里的3.1.3老项目能跑起来多半是之前有人做过兼容性修补别轻易动那些补丁代码。推荐的环境组合组件推荐版本备注PHP5.4.x最稳零警告MySQL5.5 - 5.7兼容性最好Web服务器Apache 2.2/2.4 Nginx 1.6都支持URL重写规则略有差异操作系统CentOS 6/7、Ubuntu 14.04/16.04当年的主流服务器环境2.2 部署过程与目录权限3.1.3的部署步骤非常简单比现在动不动就Composer install的流程亲切太多解压Full完整包到Web根目录比如/var/www/html/thinkphp313。确保Runtime目录有写权限存放编译模板、缓存文件、日志文件。浏览器访问http://你的域名/thinkphp313/index.php如果能看到ThinkPHP默认欢迎页说明基础环境没问题。这里有一个关键细节入口文件是index.php默认通过PATHINFO模式解析URL。如果你用的是Apache且没开mod_rewriteURL会变成index.php?mHomecIndexaindex这种带参数的形式也能正常跑。但如果想用美观的伪静态URL就需要在根目录放.htaccess文件IfModule mod_rewrite.c RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] /IfModuleNginx下对应的rewrite规则是location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php/$1 last; } }我那时候踩过的坑是Nginx配置里少了if (!-e $request_filename)这个判断导致静态资源如CSS、JS文件也被重写到index.php上页面样式全丢排查了整整一个下午。3. 核心架构与关键机制解析3.1 MVC目录结构与模块化设计解开Full包之后目录结构是这样的ThinkPHP3.1.3_Full/ ├── index.php # 入口文件 ├── ThinkPHP/ # 框架核心目录 │ ├── Common/ # 核心公共函数 │ ├── Conf/ # 框架默认配置 │ ├── Extend/ # 扩展目录 │ ├── Lang/ # 语言包 │ ├── Lib/ # 核心类库 │ │ ├── Core/ # 核心类Think、Controller、Model等 │ │ └── Driver/ # 驱动 │ ├── Mode/ # 模式扩展 │ └── Tpl/ # 默认模板 ├── App/ # 应用目录默认生成 │ ├── Common/ # 公共函数目录 │ ├── Conf/ # 应用配置目录 │ ├── Home/ # 默认Home模块 │ ├── Admin/ # 后台管理模块通常自己创建 │ └── Runtime/ # 运行时缓存目录 └── Public/ # 公共资源目录这个结构的精髓在于入口文件index.php通过define(APP_PATH, ./App/)指定应用目录框架会自动根据URL中的分组信息加载对应的模块。默认分组是Home前台你可以在应用配置里绑定默认模块也可以手动创建Admin模块目录对应后台管理。模块化的好处在当时非常明显前台和后台共用一套核心框架但各自有独立的控制器、模型、配置和模板互不干扰。遇到需要给后台单独设置访问权限的场景直接在入口文件里做一次环境判断就行不用像单入口单模块的框架那样折腾路由白名单。3.2 路由解析与URL模式3.1.3支持四种URL模式在配置文件中通过URL_MODEL参数控制0普通模式index.php?mHomecUseraaddid11PATHINFO模式index.php/Home/User/add/id/12REWRITE模式去掉index.php的PATHINFO需要伪静态规则配合3兼容模式index.php?s/Home/User/add/id/1默认配置是PATHINFO模式URL_MODEL1这也是我推荐大多数项目使用的模式。它兼顾了URL可读性和配置简单度不需要服务器额外支持。一个容易被忽略的细节是PATHINFO模式下参数传递的规则index.php/模块/控制器/操作/参数名/参数值/参数名2/参数值2。比如index.php/Home/User/detail/id/23在detail方法里用$_GET[id]或I(get.id)拿到的就是23。这种参数绑定方式比传统的?id23更加规整也方便做URL语义化。3.3 数据库操作与ORM实现ThinkPHP 3.1.3内嵌的ORM模型是很多人选择它的核心理由。它实现了ActiveRecord模式这意味着你可以用非常直观的链式操作来和数据库打交道// 查询单条记录 $user M(User)-where(id23)-find(); // 条件查询 排序 分页 $list M(Article)-where(status1 AND category_id5) -order(create_time DESC) -limit(10) -select(); // 新增记录 $data array(username test, password md5(123456)); $newId M(User)-add($data); // 更新 M(User)-where(id23)-save(array(last_login_time time())); // 删除 M(User)-where(id23)-delete();M()方法实例化一个基础模型对应数据表名的驼峰转换规则是M(User)对应think_user表、M(UserInfo)对应think_user_info表。如果你需要自定义表名前缀在配置里改DB_PREFIX即可默认还是think_。这里要特别提醒一下老版本ORM的一个坑find()和select()返回的数据结构不同前者是以为键的一维数组后者是二维数组。新手经常犯的错误是把select()的返回结果当一维数组去取字段结果打印出来一堆Undefined index错误。4. 实操过程从零搭建一个带后台的新闻模块4.1 数据库设计与模型定义开发一个新模块我习惯先从数据库设计入手。假设我们要做一个新闻发布系统最简单的表结构如下CREATE TABLE think_news ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 标题, content text NOT NULL COMMENT 内容, category_id int(11) NOT NULL DEFAULT 0 COMMENT 分类ID, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态0隐藏 1显示, create_time int(11) NOT NULL DEFAULT 0 COMMENT 创建时间, update_time int(11) NOT NULL DEFAULT 0 COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT新闻表;在App/Home/Model/目录下创建NewsModel.class.php?php class NewsModel extends Model { // 自定义字段映射 protected $_map array( title title, content content, ); // 自动验证标题必填、标题长度限制 protected $_validate array( array(title, require, 标题不能为空, 1), array(title, 2,100, 标题长度在2到100个字符之间, 1, length), ); // 自动完成创建时间、更新时间 protected $_auto array( array(create_time, time, 1, function), // 新增时写入 array(update_time, time, 2, function), // 更新时写入 ); }_map字段映射、_validate自动验证、_auto自动完成是3.1.3模型层最强大的三个特性。特别是自动验证配合表单提交能少写大量if判断。只有当你用过这些特性后才会明白为什么当年那么多人愿意用ThinkPHP而不去裸写原生SQL。4.2 控制器逻辑与数据交互控制器放在App/Home/Controller/NewsController.class.php?php class NewsController extends Controller { // 新闻列表 public function index() { $News M(News); $count $News-where(status1)-count(); $Page new Page($count, 10); $show $Page-show(); $list $News-where(status1) -order(id DESC) -limit($Page-firstRow . , . $Page-listRows) -select(); $this-assign(list, $list); $this-assign(page, $show); $this-display(); } // 新闻详情 public function detail() { $id I(get.id, 0, intval); $info M(News)-where(id . $id)-find(); if (!$info || $info[status] ! 1) { $this-error(文章不存在或已下架); } $this-assign(info, $info); $this-display(); } // 搜索新闻按标题模糊搜索 public function search() { $keyword I(get.keyword, , trim); $where array(status 1); if ($keyword ! ) { $where[title] array(like, % . $keyword . %); } $list M(News)-where($where)-select(); $this-assign(list, $list); $this-assign(keyword, $keyword); $this-display(); } }I()方法是我在ThinkPHP里最喜欢的函数之一其封装了$_GET、$_POST、$_REQUEST等超全局变量的安全获取逻辑第二个参数是默认值第三个参数是过滤函数。用I()代替直接操作$_GET[id]至少能规避掉一半的SQL注入风险。有个实操细节值得说一下列表查询里我用了$Page-firstRow . , . $Page-listRows的limit写法这是当年ThinkPHP分页类的标准用法。如果你写成limit(10)不分页数据量少没问题一旦上了千条记录页面直接卡死。4.3 模板渲染与标签库模板文件放在App/Home/Tpl/News/index.html用ThinkPHP内置模板引擎!DOCTYPE html html head meta charsetutf-8 title新闻列表/title /head body ul volist namelist iditem li a href{:U(News/detail, array(id $item[id]))} {$item.title} /a span{$item.create_time|dateY-m-d,###}/span /li /volist /ul div classpage{$page}/div /body /html模板里的volist标签是ThinkPHP的循环输出标签等价于PHP的foreach。{$item.create_time|dateY-m-d,###}是模板引擎的变量修饰符写法把时间戳用date()函数格式化后输出其中###表示变量本身。模板继承已经支持但3.1.3更常用的还是布局模板layout和包含标签include file./App/Home/Tpl/Public/header.html /。如果页面头部和底部在多个模板里重复用include是最简单直接的方案。5. 常见问题与排查技巧实录5.1 PHP高版本兼容性老框架遇上新时代这是3.1.3使用者最头疼的问题。如果在PHP 7.0以上环境运行通常会出现mysql_connect(): The mysql extension is deprecated and will be removed in the future这类错误甚至直接白屏。两个解决方向一是改数据库驱动。3.1.3的数据库驱动位于ThinkPHP/Lib/Driver/Db/目录下默认DbMysql.class.php用的是mysql_*函数把它替换为DbMysqli.class.php的实现同样位置有DbMysqli.class.php用的是MySQLi扩展。然后在配置里改DB_TYPE mysqli,二是对框架核心代码打补丁把mysql_*函数替换为PHP 7支持的等价写法。这个工作量较大而且后期维护困难不太建议除非你项目里数据库交互层已经被改得面目全非。另外一个容易踩的坑是PHP 7.2以后each()函数被移除。如果你在旧代码里见过while (list($key, $value) each($array)) {请务必改成foreach ($array as $key $value) {搜索一下整个项目里的each(能省去线上环境半夜被叫起来的痛苦。5.2 缓存与调试白屏了怎么办白屏是最让人抓狂的问题。排查思路要按优先级来打开调试模式。在App/Conf/config.php里设置SHOW_ERROR_MSG true, SHOW_PAGE_TRACE true,打开后页面底部会显示调试信息包含SQL语句、运行时间、文件加载情况这是最直接的诊断工具。清空Runtime缓存。3.1.3会把编译后的模板、配置文件缓存到App/Runtime/有时候修改了配置不生效十有八九是缓存没清。最快的办法就是删除Runtime目录下的所有文件刷新页面即可。检查PHP错误日志。如果什么错误信息都不显示看Runtime/Logs/目录下的日志文件框架会把错误记录在里面。常见白屏原因分布大致是这样的模板语法错误约占四成、数据库连接失败约占三成、PHP版本不兼容约占两成、其他占一成。照着这个方向排查基本能覆盖大多数场景。5.3 模板乱码与Web服务器配置老用户可能遇到过模板页面输出乱码的情况。原因一般是文件编码和页面声明编码不一致比如模板文件用UTF-8保存但页面没加meta charsetutf-8。也有一种情况是模板文件本身被以GBK编码保存内容里又混入了特殊字符这种只能重新保存文件解决。Nginx环境还有一个常见问题PATHINFO模式下如果配置不当会返回404。原因是Nginx默认不支持PATHINFO路径解析必须在配置里加上location ~ \.php($|/) { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_split_path_info ^(.\.php)(/.)$; fastcgi_param PATH_INFO $fastcgi_path_info; }我自己曾被这个问题坑过一次。当时在测试环境用Apache一切正常上了生产Nginx服务器后所有列表页全部404排查了快两个钟头才定位到是fastcgi_split_path_info没配置。从此以后我部署ThinkPHP 3.1.3到Nginx时第一步就是检查PATH_INFO配置。6. 安全加固与性能优化心得6.1 防SQL注入与XSS的基础操作3.1.3本身有一些内置的安全机制比如自动转义POST数据、I()方法默认会做htmlspecialchars处理。但在实际项目中还需要做到以下几点才能放心上线所有数据库查询尽量走模型和M()方法不要拼接SQL。如果确实需要复杂查询用$Model-query()时必须做参数绑定不要直接拼$_GET值进去。后台的权限控制不要只做菜单隐藏要在每个控制器方法的开头校权。3.1.3的RBAC实现里有个RBAC::AccessDecision()方法可以在公共控制器里统一调用。上传文件必须校验MIME类型和文件后缀老版本的上传类UploadFile有默认允许的后缀列表但建议在配置里显式限定为jpg,jpeg,png,gif等图片格式其他一概不认。6.2 性能优化的三板斧开启缓存。3.1.3支持文件缓存、Memcache、Redis等多种方式。如果服务器上装了Memcache在配置里加DATA_CACHE_TYPE Memcache, MEMCACHE_HOST 127.0.0.1, MEMCACHE_PORT 11211,开启字段缓存。在配置里设置DB_FIELDS_CACHE true框架会把数据表字段缓存起来避免每次查询都去读表结构数据量大时提升非常明显。使用静态页面缓存。对于访问量高的新闻列表页可以直接在控制器方法里调用S()方法缓存查询结果if (!$list S(news_list_ . $page)) { $list $News-where(status1)-order(id DESC)-limit(10)-select(); S(news_list_ . $page, $list, 60); // 缓存60秒 }6.3 老项目迁移时的注意事项如果你的任务不是从零开发而是维护一个遗留的3.1.3项目我强烈建议按以下顺序梳理代码把所有mysql_*函数查出来能替换成M()方法或mysqli驱动就换。检查所有控制器基类确认公共逻辑有没有在父类_initialize()方法里做统一处理——这是3.1.3的惯例所有控制器的初始化操作都放在这里而不是构造函数里。确认配置文件中APP_DEBUG是否关闭。生产环境开着调试会导致每次请求额外生成调试日志磁盘和性能都会被拖垮。留意数据表前缀迁移数据库时如果改了前缀记得同步修改配置里的DB_PREFIX否则所有查询都会报Table not found。7. 写在最后的一些个人体会说实话ThinkPHP 3.1.3放到今天来看很多设计确实显得老了。它没有现代框架的依赖注入容器、没有PSR规范、没有Composer生态调试信息也是一股原始气息。但在它活跃的那个年代它给中国PHP开发者带来的便利是实实在在的。我自己职业生涯的前三个正式项目都是基于3.1.x开发的那个框架教会我的MVC分层思维、ActiveRecord操作方式、模板引擎的变量输出逻辑直到现在写其他语言和框架时依然受用。如果你现在接触3.1.3是因为维护老项目多说一句在做任何改动之前先把原代码完整备份一份最好把数据库也导一份。老项目的坑往往不在代码本身而在你根本不知道线上跑了多少年、改过多少次、有没有人偷偷往里面塞过黑盒逻辑。稳字当头比什么都重要。最后再分享一个小技巧3.1.3的项目调试时如果觉得页面报错信息不够直观可以在App/Common/common.php里加一个自定义错误处理函数把异常信息写入日志文件的同时输出到浏览器。这在排查Ajax接口问题时特别有用——很多次接口返回500错误但页面毫无提示就是因为默认配置掩盖了真实报错。本文还有配套的精品资源点击获取