深度解析:ThinkPHP 3.2框架的核心架构与安全实践

55 阅读20分钟

ThinkPHP 3.2

1. 标准目录结构

www  WEB部署目录(或者子目录)
├─index.php       入口文件
├─README.md       README文件
├─Application     应用目录
├─Public          资源文件目录
└─ThinkPHP        框架目录

2. 框架目录结构

├─ThinkPHP 框架系统目录(可以部署在非web目录下面)
│  ├─Common       核心公共函数目录
│  ├─Conf         核心配置目录 
│  ├─Lang         核心语言包目录
│  ├─Library      框架类库目录
│  │  ├─Think     核心Think类库包目录
│  │  ├─Behavior  行为类库目录
│  │  ├─Org       Org类库包目录
│  │  ├─Vendor    第三方类库目录
│  │  ├─ ...      更多类库目录
│  ├─Mode         框架应用模式目录
│  ├─Tpl          系统模板目录
│  ├─LICENSE.txt  框架授权协议文件
│  ├─logo.png     框架LOGO文件
│  ├─README.txt   框架README文件
│  └─ThinkPHP.php 框架入口文件

3. 入口文件

入口文件主要完成:

  • 定义框架路径、项目路径(可选)
  • 定义调试模式和应用模式(可选)
  • 定义系统相关常量(可选)
  • 载入框架入口文件(必须)

默认情况下,框架已经自带了一个应用入口文件(以及默认的目录结构),内容如下:

define('APP_PATH','./Application/');
require './ThinkPHP/ThinkPHP.php';

如果你改变了项目目录(例如把Application更改为Apps),只需要在入口文件更改APP_PATH常量定义即可:

define('APP_PATH','./Apps/');
require './ThinkPHP/ThinkPHP.php';

入口文件中的其他定义

一般不建议在入口文件中做过多的操作,但可以重新定义一些系统常量,入口文件中支持定义(建议)的一些系统常量包括:

常量描述
THINK_PATH框架目录
APP_PATH应用目录
RUNTIME_PATH应用运行时目录(可写)
APP_DEBUG应用调试模式 (默认为false)
STORAGE_TYPE存储类型(默认为File)
APP_MODE应用模式(默认为common)

例如,我们可以在入口文件中重新定义相关目录并且开启调试模式:

// 定义应用目录
define('APP_PATH','./Apps/');
// 定义运行时目录
define('RUNTIME_PATH','./Runtime/');
// 开启调试模式
define('APP_DEBUG',True);
// 更名框架目录名称,并载入框架入口文件
require './Think/ThinkPHP.php';

4. 控制器

我们可以在自动生成的Application/Home/Controller目录下面找到一个 IndexController.class.php 文件,这就是默认的Index控制器文件。

控制器类的命名方式是:控制器名(驼峰法,首字母大写)+Controller 控制器文件的命名方式是:类名+class.php(类文件后缀)

默认的欢迎页面其实就是访问的Home模块下面的Index控制器类的index操作方法 我们修改默认的index操作方法如下:

namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        echo 'hello,world!';
    }
}

再次运行应用入口文件,浏览器会显示:hello,world!。

我们再来看下控制器类,IndexController控制器类的开头是命名空间定义:

namespace Home\Controller;

这是系统的规范要求,表示当前类是Home模块下的控制器类,命名空间和实际的控制器文件所在的路径是一致的,也就是说: Home\Controller\IndexController类 对应的控制器文件位于应用目录下面的 Home/Controller/IndexController.class.php,如果你改变了当前的模块名,那么这个控制器类的命名空间也需要随之修改。

5. URL 模式

入口文件是应用的单一入口,对应用的所有请求都定向到应用入口文件,系统会从URL参数中解析当前请求的模块、控制器和操作:

http://serverName/index.php/模块/控制器/操作

这是3.2版本的标准URL格式。

可以通过设置模块绑定或者域名部署等方式简化URL地址中的模块及控制器名称。

如果我们直接访问入口文件的话,由于URL中没有模块、控制器和操作,因此系统会访问默认模块(Home)下面的默认控制器(Index)的默认操作(index),因此下面的访问是等效的:

http://serverName/index.php http://serverName/index.php/Home/Index/index

这种URL模式就是系统默认的PATHINFO模式,不同的URL模式获取模块和操作的方法不同,ThinkPHP支持的URL模式有四种:普通模式、PATHINFO、REWRITE和兼容模式,可以设置URL_MODEL参数改变URL模式。

普通模式

普通模式也就是传统的GET传参方式来指定当前访问的模块和操作,例如: http://localhost/?m=home&c=user&a=login&var=value

m参数表示模块,c参数表示控制器,a参数表示操作(当然这些参数都是可以配置的),后面的表示其他GET参数。

如果默认的变量设置和你的应用变量有冲突的话,你需要重新设置系统配置,例如改成下面的:

'VAR_MODULE'            =>  'module',     // 默认模块获取变量
'VAR_CONTROLLER'        =>  'controller',    // 默认控制器获取变量
'VAR_ACTION'            =>  'action',    // 默认操作获取变量

上面的访问地址则变成: http://localhost/?module=home&controller=user&action=login&var=value

PATHINFO 模式

PATHINFO模式是系统的默认URL模式,提供了最好的SEO支持,系统内部已经做了环境的兼容处理,所以能够支持大多数的主机环境。对应上面的URL模式,PATHINFO模式下面的URL访问地址是: http://localhost/index.php/home/user/login/var/value/

PATHINFO地址的前三个参数分别表示模块/控制器/操作。

不过,PATHINFO模式下面,依然可以采用普通URL模式的参数方式,例如: http://localhost/index.php/home/user/login?var=value 依然是有效的

PATHINFO模式下面,URL是可定制的,例如,通过下面的配置:

// 更改PATHINFO参数分隔符
'URL_PATHINFO_DEPR'=>'-',

我们还可以支持下面的URL访问: http://localhost/index.php/home-user-login-var-value

REWRITE 模式

REWRITE模式是在PATHINFO模式的基础上添加了重写规则的支持,可以去掉URL地址里面的入口文件index.php,但是需要额外配置WEB服务器的重写规则。

如果是Apache则需要在入口文件的同级添加.htaccess文件,内容如下:

<IfModule mod_rewrite.c>
 RewriteEngine on
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>

接下来,就可以用下面的URL地址访问了: http://localhost/home/user/login/var/value

兼容模式

兼容模式是用于不支持PATHINFO的特殊环境,URL地址是: http://localhost/?s=/home/user/login/var/value

可以更改兼容模式变量的名称定义,例如:

'VAR_PATHINFO'          =>  'path'

PATHINFO参数分隔符对兼容模式依然有效,例如:

// 更改PATHINFO参数分隔符
'URL_PATHINFO_DEPR'=>'-',

使用以上配置的话,URL访问地址可以变成: http://localhost/?path=/home-user-login-var-value

兼容模式配合Web服务器重写规则的定义,可以达到和REWRITE模式一样的URL效果。

6. 启用路由

要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情况下不支持路由功能),并且在应用(或者模块)配置文件中开启路由:

// 开启路由
'URL_ROUTER_ON'  
  => true,

路由功能可以针对模块,也可以针对全局,针对模块的路由则需要在模块配置文件中开启和设置路由,如果是针对全局的路由,则是在公共模块的配置文件中开启和设置(后面我们以模块路由定义为例)。

然后就是配置路由规则了,在模块的配置文件中使用URL_ROUTE_RULES参数进行配置,配置格式是一个数组,每个元素都代表一个路由规则,例如:

'URL_ROUTE_RULES'=>array(
    'news/:year/:month/:day' => array('News/archive', 'status=1'),
    'news/:id'               => 'News/read',
    'news/read/:id'          => '/news/:1',
),

系统会按定义的顺序依次匹配路由规则,一旦匹配到的话,就会定位到路由定义中的控制器和操作方法去执行(可以传入其他的参数),并且后面的规则不会继续匹配。

7. 操作绑定到类

ThinkPHP3.2版本提供了把每个操作方法定位到一个类的功能,可以让你的开发工作更细化,可以设置参数ACTION_BIND_CLASS,例如:

'ACTION_BIND_CLASS'    =>    True,

设置后,我们的控制器定义有所改变,以URL访问为 http://serverName/Home/Index/index为例,原来的控制器文件定义位置为:

Application/Home/Controller/IndexController.class.php

控制器类的定义如下:

namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller{
    public function index(){
        echo '执行Index控制器的index操作';
    }
}

可以看到,实际上我们调用的是 Home\Controller\IndexController 类的index方法。

设置后,控制器文件位置改为:

Application/Home/Controller/Index/index.class.php

控制器类的定义如下:

namespace Home\Controller\Index;
use Think\Controller;
class index extends Controller{
    public function run(){
        echo '执行Index控制器的index操作';
    }
}

现在,我们调用的其实是 Home\Controller\Index\index 类的run方法。

run方法依旧可以支持传入参数和进行Action参数绑定操作,但不再支持A方法实例化和R方法远程调用,我们建议R方法不要进行当前访问控制器的远程调用。

8. 重定向

Controller类的redirect方法可以实现页面的重定向功能。

redirect方法的参数用法和U函数的用法一致(参考URL生成部分),例如:

//重定向到New模块的Category操作
$this->redirect('New/category', array('cate_id' => 2), 5, '页面跳转中...');

上面的用法是停留5秒后跳转到New模块的category操作,并且显示页面跳转中字样,重定向后会改变当前的URL地址。

如果你仅仅是想重定向要一个指定的URL地址,而不是到某个模块的操作方法,可以直接使用redirect函数重定向,例如:

//重定向到指定的URL地址
redirect('/New/category/cate_id/2', 5, '页面跳转中...');

Redirect函数的第一个参数是一个URL地址。

控制器的redirect方法和redirect函数的区别在于前者是用URL规则定义跳转地址,后者是一个纯粹的URL地址。

9. 获取变量

I方法是ThinkPHP用于更加方便和安全的获取系统输入变量,可以用于任何地方,用法格式如下:

I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])

我们以GET变量类型为例,说明下I方法的使用:

echo I('get.id'); // 相当于 $_GET['id']
echo I('get.name'); // 相当于 $_GET['name']

支持默认值:

echo I('get.id',0); // 如果不存在$_GET['id'] 则返回0
echo I('get.name',''); // 如果不存在$_GET['name'] 则返回空字符串

采用方法过滤:

// 采用htmlspecialchars方法对$_GET['name'] 进行过滤,如果不存在则返回空字符串
echo I('get.name','','htmlspecialchars');

支持直接获取整个变量类型,例如:

// 获取整个$_GET 数组
I('get.');

用同样的方式,我们可以获取post或者其他输入类型的变量,例如:

I('post.name','','htmlspecialchars'); // 采用htmlspecialchars方法对$_POST['name'] 进行过滤,如果不存在则返回空字符串
I('session.user_id',0); // 获取$_SESSION['user_id'] 如果不存在则默认为0
I('cookie.'); // 获取整个 $_COOKIE 数组
I('server.REQUEST_METHOD'); // 获取 $_SERVER['REQUEST_METHOD']

param变量类型是框架特有的支持自动判断当前请求类型的变量获取方式,例如:

echo I('param.id');

如果当前请求类型是GET,那么等效于 $_GET['id'],如果当前请求类型是POST或者PUT,那么相当于获取 $_POST['id'] 或者 PUT参数id。

由于param类型是I函数默认获取的变量类型,因此事实上param变量类型的写法可以简化为:

I('id'); // 等同于 I('param.id')
I('name'); // 等同于 I('param.name')

path类型变量可以用于获取URL参数(必须是PATHINFO模式参数有效,无论是GET还是POST方式都有效),例如: 当前访问URL地址是 http://serverName/index.php/New/2013/06/01

那么我们可以通过

echo I('path.1'); // 输出2013
echo I('path.2'); // 输出06
echo I('path.3'); // 输出01

data类型变量可以用于获取不支持的变量类型的读取,例如:

I('data.file1','','',$_FILES);

变量过滤

如果你没有在调用I函数的时候指定过滤方法的话,系统会采用默认的过滤机制(由DEFAULT_FILTER配置),事实上,该参数的默认设置是:

// 系统默认的变量过滤机制
'DEFAULT_FILTER'        => 'htmlspecialchars'

也就说,I方法的所有获取变量如果没有设置过滤方法的话都会进行htmlspecialchars过滤,那么:

// 等同于 htmlspecialchars($_GET['name'])
I('get.name');

同样,该参数也可以设置支持多个过滤,例如:

'DEFAULT_FILTER'        => 'strip_tags,htmlspecialchars'

设置后,我们在使用:

// 等同于 htmlspecialchars(strip_tags($_GET['name']))
I('get.name');

如果我们在使用I方法的时候 指定了过滤方法,那么就会忽略DEFAULT_FILTER的设置,例如:

// 等同于 strip_tags($_GET['name'])
echo I('get.name','','strip_tags');

I方法的第三个参数如果传入函数名,则表示调用该函数对变量进行过滤并返回(在变量是数组的情况下自动使用array_map进行过滤处理),否则会调用PHP内置的filter_var方法进行过滤处理,例如:

I('post.email','',FILTER_VALIDATE_EMAIL);

表示 会对$_POST['email'] 进行 格式验证,如果不符合要求的话,返回空字符串。 (关于更多的验证格式,可以参考 官方手册的filter_var用法。) 或者可以用下面的字符标识方式:

I('post.email','','email');

可以支持的过滤名称必须是filter_list方法中的有效值(不同的服务器环境可能有所不同),可能支持的包括:

  • int
  • boolean
  • float
  • validate_regexp
  • validate_url
  • validate_email
  • validate_ip
  • string
  • stripped
  • encoded
  • special_chars
  • unsafe_raw
  • email
  • url
  • number_int
  • number_float
  • magic_quotes
  • callback

还可以支持进行正则匹配过滤,例如:

// 采用正则表达式进行变量过滤
I('get.name','','/^[A-Za-z]+$/');
I('get.id',0,'/^\d+$/');

如果正则匹配不通过的话,则返回默认值。

在有些特殊的情况下,我们不希望进行任何过滤,即使DEFAULT_FILTER已经有所设置,可以使用:

// 下面两种方式都不采用任何过滤方法
I('get.name','','');
I('get.id','',false);

一旦过滤参数设置为空字符串或者false,即表示不再进行任何的过滤。

变量修饰符

最新版本的I函数支持对变量使用修饰符功能,可以更方便的通过类型过滤变量。

用法如下: I('变量类型.变量名/修饰符')

例如:

I('get.id/d'); // 强制变量转换为整型
I('post.name/s'); // 强制转换变量为字符串类型
I('post.ids/a'); // 强制变量转换为数组类型

10. Cookie 设置

cookie('name','value'); //设置cookie
cookie('name','value',3600); // 指定cookie保存时间

还可以支持参数传入的方式完成复杂的cookie赋值,下面是对cookie的值设置3600秒有效期,并且加上cookie前缀think_

cookie('name','value',array('expire'=>3600,'prefix'=>'think_'))

数组参数可以采用query形式参数

cookie('name','value','expire=3600&prefix=think_')

和上面的用法等效。

后面的参数支持prefix,expire,path,domainhttponly3.2.2版本新增)五个索引参数,如果没有传入或者传入空值的话,会默认取COOKIE_PREFIXCOOKIE_EXPIRECOOKIE_PATHCOOKIE_DOMAINCOOKIE_HTTPONLY五个配置参数。如果只传入个别参数,那么也会和默认的配置参数合并。

支持给cookie设置数组值(采用JSON编码格式保存),例如:

cookie('name',array('value1','value2'));

Cookie 获取

获取cookie很简单,无论是怎么设置的cookie,只需要使用:

$value = cookie('name');

如果没有设置cookie前缀的话 相当于 $value = $_COOKIE['name'];

如果设置了cookie前缀的话,相当于 $value = $_COOKIE['前缀+name'];

如果要获取所有的cookie,可以使用:

$value = cookie();

该用法相当于 $value = $_COOKIE;

注意,该用法会返回所有的cookie而无论是否当前的前缀。

Cookie 删除

删除某个cookie的值,使用:

cookie('name',null);

要删除所有的Cookie值,可以使用:

cookie(null); // 清空当前设定前缀的所有cookie值
cookie(null,'think_'); //  清空指定前缀的所有cookie值

11. 上传操作

ThinkPHP文件上传操作使用Think\Upload类。假设前面的表单提交到当前控制器的upload方法,我们来看下upload方法的实现代码:

public function upload(){
    $upload = new \Think\Upload();// 实例化上传类
    $upload->maxSize   =     3145728 ;// 设置附件上传大小
    $upload->exts      =     array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
    $upload->rootPath  =     './Uploads/'; // 设置附件上传根目录
    $upload->savePath  =     ''; // 设置附件上传(子)目录
    // 上传文件 
    $info   =   $upload->upload();
    if(!$info) {// 上传错误提示错误信息
        $this->error($upload->getError());
    }else{// 上传成功
        $this->success('上传成功!');
    }
}

上传类对图片文件的上传安全做了支持,如果企图上传非法的图像文件,系统会提示 非法图像文件。 为了更好的使用上传功能,建议你的服务器开启finfo模块支持。

上传参数

在上传操作之前,我们可以对上传的属性进行一些设置。Upload类支持的属性设置包括:

属性描述
maxSize文件上传的最大文件大小(以字节为单位),0为不限大小
rootPath文件上传保存的根路径
savePath文件上传的保存路径(相对于根路径)
saveName上传文件的保存规则,支持数组和字符串方式定义
saveExt上传文件的保存后缀,不设置的话使用原文件后缀
replace存在同名文件是否是覆盖,默认为false
exts允许上传的文件后缀(留空为不限制),使用数组或者逗号分隔的字符串设置,默认为空
mimes允许上传的文件类型(留空为不限制),使用数组或者逗号分隔的字符串设置,默认为空
autoSub自动使用子目录保存上传文件 默认为true
subName子目录创建方式,采用数组或者字符串方式定义
hash是否生成文件的hash编码 默认为true
callback检测文件是否存在回调,如果存在返回文件信息数组

上面的属性可以通过两种方式传入:

1. 实例化传入

我们可以在实例化的时候直接传入参数数组,例如:

$config = array(
    'maxSize'    =>    3145728,
    'rootPath'   =>    './Uploads/',
    'savePath'   =>    '',
    'saveName'   =>    array('uniqid',''),
    'exts'       =>    array('jpg', 'gif', 'png', 'jpeg'),
    'autoSub'    =>    true,
    'subName'    =>    array('date','Ymd'),
);
$upload = new \Think\Upload($config);// 实例化上传类

关于saveNamesubName的使用后面我们会有详细的描述。

2. 动态赋值

支持在实例化后动态赋值上传参数,例如:

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 3145728;
$upload->rootPath = './Uploads/';
$upload->savePath = '';
$upload->saveName = array('uniqid','');
$upload->exts     = array('jpg', 'gif', 'png', 'jpeg');
$upload->autoSub  = true;
$upload->subName  = array('date','Ymd');

上面的设置和实例化传入的效果是一致的。

上传文件信息

设置好上传的参数后,就可以调用Think\Upload类的upload方法进行附件上传,如果失败,返回false,并且用getError方法获取错误提示信息;如果上传成功,就返回成功上传的文件信息数组。

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize   =     3145728 ;// 设置附件上传大小
$upload->exts      =     array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath  =      './Uploads/'; // 设置附件上传根目录
$upload->savePath  =      ''; // 设置附件上传(子)目录
// 上传文件 
$info   =   $upload->upload();
if(!$info) {// 上传错误提示错误信息
    $this->error($upload->getError());
}else{// 上传成功 获取上传文件信息
    foreach($info as $file){
        echo $file['savepath'].$file['savename'];
    }
}

每个文件信息又是一个记录了下面信息的数组,包括:

属性描述
key附件上传的表单名称
savepath上传文件的保存路径
name上传文件的原始名称
savename上传文件的保存名称
size上传文件的大小
type上传文件的MIME类型
ext上传文件的后缀类型
md5上传文件的md5哈希验证字符串 仅当hash设置开启后有效
sha1上传文件的sha1哈希验证字符串 仅当hash设置开启后有效

文件上传成功后,就可以使用这些文件信息来进行其他的数据操作,例如保存到当前数据表或者单独的附件数据表。

例如,下面表示把上传信息保存到数据表的字段:

$model = M('Photo');
// 取得成功上传的文件信息
$info = $upload->upload();
// 保存当前数据对象
$data['photo'] = $info['photo']['savename'];
$data['create_time'] = NOW_TIME;
$model->add($data);

单文件上传

upload方法支持多文件上传,有时候,我们只需要上传一个文件,就可以使用Upload类提供的uploadOne方法上传单个文件,例如:

public function upload(){
    $upload = new \Think\Upload();// 实例化上传类
    $upload->maxSize   =     3145728 ;// 设置附件上传大小
    $upload->exts      =     array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
    $upload->rootPath  =      './Uploads/'; // 设置附件上传根目录
    // 上传单个文件 
    $info   =   $upload->uploadOne($_FILES['photo1']);
    if(!$info) {// 上传错误提示错误信息
        $this->error($upload->getError());
    }else{// 上传成功 获取上传文件信息
         echo $info['savepath'].$info['savename'];
    }
}

uploadOne方法上传成功后返回的文件信息和upload方法的区别是只有单个文件信息的一维数组。

上传文件的命名规则

上传文件的命名规则(saveName)用于确保文件不会产生冲突或者覆盖的情况。命名规则的定义可以根据你的业务逻辑来调整,不是固定的。例如,如果你采用时间戳的方式来定义命名规范,那么在同时上传多个文件的时候可能产生冲突(因为同一秒内可以上传多个文件),因此你需要根据你的业务需求来设置合适的上传命名规则。这里顺便来说下saveName参数的具体用法。

1. 采用函数方式

如果传入的字符串是一个函数名,那么表示采用函数动态生成上传文件名(不包括文件后缀),例如:

// 采用时间戳命名
$upload->saveName = 'time';
// 采用GUID序列命名
$upload->saveName = 'com_create_guid';

也可以采用用户自定义函数

// 采用自定义函数命名
$upload->saveName = 'myfun';

默认的命名规则设置是采用uniqid函数生成一个唯一的字符串序列。

saveName的值支持数组和字符串两种方式,如果是只有一个参数或者没有参数的函数,直接使用字符串设置即可,如果需要传入额外的参数,可以使用数组方式,例如:

// 采用date函数生成命名规则 传入Y-m-d参数
$upload->saveName = array('date','Y-m-d');
// 如果有多个参数需要传入的话 可以使用数组
$upload->saveName = array('myFun',array('__FILE__','val1','val2'));

如果需要使用上传的原始文件名,可以采用__FILE__传入,所以上面的定义规则,最终的结果是 myFun('上传文件名','val1','val2')执行的结果。

2. 直接设置上传文件名

如果传入的参数不是一个函数名,那么就会直接当做是上传文件名,例如:

$upload->saveName = time().'_'.mt_rand();

表示上传的文件命名采用时间戳加一个随机数的组合字符串方式。

当然,如果觉得有必要,你还可以固定设置一个上传文件的命名规则,用于固定保存某个上传文件。

$upload->saveName = 'ThinkPHP';

3. 保持上传文件名不变

如果你想保持上传的文件名不变,那么只需要设置命名规范为空即可,例如:

$upload->saveName = '';

一般来说不建议保持不变,因为会导致相同的文件名上传后被覆盖的情况。

子目录保存

saveName只是用于设置文件的保存规则,不涉及到目录,如果希望对上传的文件分子目录保存,可以设置autoSubsubName参数来完成,例如:

// 开启子目录保存 并以日期(格式为Ymd)为子目录
$upload->autoSub = true;
$upload->subName = array('date','Ymd');

可以使用自定义函数来保存,例如:

// 开启子目录保存 并调用自定义函数get_user_id生成子目录
$upload->autoSub = true;
$upload->subName = 'get_user_id';

saveName参数一样,subName的定义可以采用数组和字符串的方式。

注意:如果get_user_id函数未定义的话,会直接以get_user_id字符串作为子目录的名称保存。

子目录保存和文件命名规则可以结合使用。

上传驱动

上传类可以支持不同的环境,通过相应的上传驱动来解决,默认情况下使用本地(Local)上传驱动,当然,你还可以设置当前默认的上传驱动类型,例如:

'FILE_UPLOAD_TYPE'    =>    'Ftp',
'UPLOAD_TYPE_CONFIG'  =>    array(        
        'host'     => '192.168.1.200', //服务器
        'port'     => 21, //端口
        'timeout'  => 90, //超时时间
        'username' => 'ftp_user', //用户名
        'password' => 'ftp_pwd', //密码 ),

表示当前使用Ftp作为上传类的驱动,上传的文件会通过FTP传到指定的远程服务器。

也可以在实例化上传类的时候指定,例如:

$config = array(
    'maxSize'    =    3145728,
    'rootPath'   =    './Uploads/',
    'savePath'   =    '',
    'saveName'   =    array('uniqid',''),
    'exts'       =    array('jpg', 'gif', 'png', 'jpeg'),
    'autoSub'    =    true,
    'subName'    =    array('date','Ymd'),
);
$ftpConfig     =    array(        
        'host'     => '192.168.1.200', //服务器
        'port'     => 21, //端口
        'timeout'  => 90, //超时时间
        'username' => 'ftp_user', //用户名
        'password' => 'ftp_pwd', //密码
);
$upload = new \Think\Upload($config,'Ftp',$ftpConfig);// 实例化上传类

12. 查询方式

ThinkPHP可以支持直接使用字符串作为查询条件,但是大多数情况推荐使用数组或者对象来作为查询条件,因为会更加安全。

使用字符串作为查询条件

这是最传统的方式,但是安全性不高,例如:

$User = M("User"); // 实例化User对象
$User->where('type=1 AND status=1')->select();

最后生成的SQL语句是: SELECT * FROM think_user WHERE type=1 AND status=1

采用字符串查询的时候,我们可以配合使用字符串条件的安全预处理机制。

使用数组作为查询条件

这种方式是最常用的查询方式,例如:

$User = M("User"); // 实例化User对象
$condition['name'] = 'thinkphp';
$condition['status'] = 1;
// 把查询条件传入查询方法
$User->where($condition)->select();

最后生成的SQL语句是: SELECT * FROM think_user WHERE name='thinkphp' AND status=1

如果进行多字段查询,那么字段之间的默认逻辑关系是 逻辑与 AND,但是用下面的规则可以更改默认的逻辑判断,通过使用 _logic 定义查询逻辑:

$User = M("User"); // 实例化User对象
$condition['name'] = 'thinkphp';
$condition['account'] = 'thinkphp';
$condition['_logic'] = 'OR';
// 把查询条件传入查询方法
$User->where($condition)->select();

最后生成的SQL语句是: SELECT * FROM think_user WHERE name='thinkphp' OR account='thinkphp'

使用对象方式来查询

这里以stdClass内置对象为例:

$User = M("User"); // 实例化User对象
// 定义查询条件
$condition = new stdClass(); 
$condition->name = 'thinkphp'; 
$condition->status= 1; 
$User->where($condition)->select();

最后生成的SQL语句和上面一样: SELECT * FROM think_user WHERE name='thinkphp' AND status=1

使用对象方式查询和使用数组查询的效果是相同的,并且是可以互换的,大多数情况下,我们建议采用数组方式更加高效。

在使用数组和对象方式查询的时候,如果传入了不存在的查询字段是会被自动过滤的,例如:

$User = M("User"); // 实例化User对象
$condition['name'] = 'thinkphp';
$condition['status'] = 1;
$condition['test'] = 'test';
// 把查询条件传入查询方法
$User->where($condition)->select();

因为数据库的test字段是不存在的,所以系统会自动检测并过滤掉$condition['test'] = 'test'这一查询条件。 如果是3.2.2版本以上,当开启调试模式的话,则会抛出异常,显示:错误的查询条件。

13. 原生 SQL 查询

ThinkPHP内置的ORM和ActiveRecord模式实现了方便的数据存取操作,而且新版增加的连贯操作功能更是让这个数据操作更加清晰,但是ThinkPHP仍然保留了原生的SQL查询和执行操作支持,为了满足复杂查询的需要和一些特殊的数据操作,SQL查询的返回值因为是直接返回的Db类的查询结果,没有做任何的处理。

主要包括下面两个方法:

QUERY 方法

query方法用于执行SQL查询操作,如果数据非法或者查询错误则返回false,否则返回查询结果数据集(同select方法)。

使用示例:

$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->query("select * from think_user where status=1");

如果你当前采用了分布式数据库,并且设置了读写分离的话,query方法始终是在读服务器执行,因此query方法对应的都是读操作,而不管你的SQL语句是什么。

可以在query方法中使用表名的简化写法,便于动态更改表前缀,例如:

$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->query("select * from __PREFIX__user where status=1");
// 3.2.2版本以上还可以直接使用
$Model->query("select * from __USER__ where status=1");

和上面的写法等效,会自动读取当前设置的表前缀。

EXECUTE 方法

execute用于更新和写入数据的sql操作,如果数据非法或者查询错误则返回false ,否则返回影响的记录数。

使用示例:

$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->execute("update think_user set name='thinkPHP' where status=1");

如果你当前采用了分布式数据库,并且设置了读写分离的话,execute方法始终是在写服务器执行,因此execute方法对应的都是写操作,而不管你的SQL语句是什么。

也可以在execute方法中使用表名的简化写法,便于动态更改表前缀,例如:

$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->execute("update __PREFIX__user set name='thinkPHP' where status=1");
// 3.2.2版本以上还可以直接使用
$Model->execute("update __USER__ set name='thinkPHP' where status=1");

和上面的写法等效,会自动读取当前设置的表前缀。

14. 查询条件预处理

where方法使用字符串条件的时候,支持预处理(安全过滤),并支持两种方式传入预处理参数,例如:

$Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx))->select();
// 或者
$Model->where("id=%d and username='%s' and xx='%f'",$id,$username,$xx)->select();

模型的queryexecute方法 同样支持预处理机制,例如:

$model->query('select * from user where id=%d and status=%d',$id,$status);
//或者
$model->query('select * from user where id=%d and status=%d',array($id,$status));

execute方法用法同query方法。

15. 输入过滤

使用 I 函数过滤

使用系统内置的I函数是避免输入数据出现安全隐患的重要手段。I函数默认的过滤方法是htmlspecialchars [cite: 13]。如果我们需要采用其他的方法进行安全过滤,有两种方式:

  1. 全局过滤 如果是全局的过滤方法,那么可以设置DEFAULT_FILTER,例如:

    'DEFAULT_FILTER'        =>  'strip_tags',
    

    设置了DEFAULT_FILTER后,所有的I函数调用默认都会使用strip_tags进行过滤。 当然,我们也可以设置多个过滤方法,例如:

    'DEFAULT_FILTER'        =>  'strip_tags,stripslashes',
    
  2. 即时过滤 如果是仅需要对个别数据采用特殊的过滤方法,可以在调用I函数的时候传入过滤方法,例如:

    I('post.id',0,'intval'); // 用intval过滤$_POST['id']
    I('get.title','','strip_tags'); // 用strip_tags过滤$_GET['title']
    

要尽量避免直接使用$_GET $_POST $_REQUEST 等数据,这些可能会导致安全的隐患。 就算你要获取整个$_GET数据,我们也建议你使用 I('get.') 的方式。

写入数据过滤

如果你没有使用I函数进行数据过滤的话,还可以在模型的写入操作之前调用filter方法对数据进行安全过滤,例如:

$this->data($data)->filter('strip_tags')->add();

16. 表单合法性检测

在处理表单提交的数据的时候,建议尽量采用Think\Model类提供的create方法首先进行数据创建,然后再写入数据库。

create方法在创建数据的同时,可以进行更为安全的处理操作,而且这一切让你的表单处理变得更简单。

使用create方法创建数据对象的时候,可以使用数据的合法性检测,支持两种方式:

1. 配置 insertFieldsupdateFields 属性

可以分别为新增和编辑表单设置insertFieldsupdateFields属性。使用create方法创建数据对象的时候,不在定义范围内的属性将直接丢弃,避免表单提交非法数据。

insertFieldsupdateFields 属性的设置采用字符串(逗号分割多个字段)或者数组的方式。

设置的字段应该是实际的数据表字段,而不受字段映射的影响。例如:

namespace Home\Model;
class UserModel extends \Think\Model{
    protected $insertFields = array('account','password','nickname','email');
    protected $updateFields = array('nickname','email');
 }

定义后,调用add方法写入用户数据的时候,只能写入'account','password','nickname','email'这几个字段,编辑的时候只能更新'nickname','email'两个字段。

在使用的时候,我们调用create方法的时候,会根据提交类型自动识别insertFieldsupdateFields属性:

D('User')->create();

2. 直接调用 field 方法

如果不想定义insertFieldsupdateFields属性,可以在调用create方法之前直接调用field方法,例如,实现和上面的例子同样的作用:

在新增用户数据的时候,使用:

M('User')->field('account,password,nickname,email')->create();

而在更新用户数据的时候,使用:

M('User')->field('nickname,email')->create();

这里的字段也是实际的数据表字段。 field方法也可以使用数组方式。

使用字段合法性检测后,你不再需要担心用户在提交表单的时候注入非法字段数据了。

17. 表单令牌验证

ThinkPHP支持表单令牌验证功能,可以有效防止表单的重复提交等安全防护。

要启用表单令牌功能,需要配置行为绑定,在应用或者模块的配置目录下面的行为定义文件tags.php中,添加:

return array(
     // 添加下面一行定义即可
     'view_filter' => array('Behavior\TokenBuild'),
    // 如果是3.2.1以上版本 需要改成
    // 'view_filter' => array('Behavior\TokenBuildBehavior'),
);

表示在view_filter标签位置执行表单令牌检测行为。

表单令牌验证相关的配置参数有:

'TOKEN_ON'      =>    true,  // 是否开启令牌验证 默认关闭
'TOKEN_NAME'    =>    '__hash__',    // 令牌验证的表单隐藏字段名称,默认为__hash__
'TOKEN_TYPE'    =>    'md5',  //令牌哈希验证规则 默认为MD5
'TOKEN_RESET'   =>    true,  //令牌验证出错后是否重置令牌 默认为true

如果开启表单令牌验证功能,系统会自动在带有表单的模板文件里面自动生成以TOKEN_NAME为名称的隐藏域,其值则是TOKEN_TYPE方式生成的哈希字符串,用于实现表单的自动令牌验证。

自动生成的隐藏域位于表单Form结束标志之前。如果希望自己控制隐藏域的位置,可以手动在表单页面添加{__TOKEN__}标识,系统会在输出模板的时候自动替换。

注意: 如果页面中存在多个表单,建议添加标识,并确保只有一个表单需要令牌验证。

如果个别页面输出不希望进行表单令牌验证,可以在控制器中的输出方法之前动态关闭表单令牌验证,例如:

C('TOKEN_ON',false);
$this->display();

模型类在创建数据对象的同时会自动进行表单令牌验证操作。如果你没有使用create方法创建数据对象的话,则需要手动调用模型的autoCheckToken方法进行表单令牌验证。如果返回false,则表示表单令牌验证错误。例如:

$User = M("User"); // 实例化User对象
 // 手动进行令牌验证
 if (!$User->autoCheckToken($_POST)){
 // 令牌验证错误
 }

18. 用户可控输入 (ThinkPHP 5 vs ThinkPHP 3)

ThinkPHP 5 框架

  • Request::instance()->get() / input('get.')

    • 获取用户传入的URL参数。可用过滤器和类型转换。
    • 例子: 获取url参数中的id值
      • Request::instance()->get('id'); (调用时如不传入参数默认获取全部 Request::instance()->get();)
      • input('get.id'); (调用时如传入get.则获取全部 input('get.');)
      • input('get.id/d'); // 强制变量转换为整型
      • Request::instance()->get('name','','htmlspecialchars'); // 过滤器
      • input('get.name/s'); // 强制转换变量为字符串
      • input('get.ids/a'); // 强制变量转换为数组 默认为/s
  • Request::instance()->post() / input('post.')

    • 获取用户传入的POST参数。
    • 例子: 获取post请求body中的name值
      • Request::instance()->post('name');
      • input('post.name'); (用法同get)
  • Request::instance()->param() / input('param.') / input('')

    • 自动判断用户提交方法(POST GET PUT)获取参数。
    • 用法同get。
    • 可直接调用input('');获取全部参数,或使用input('name');获取单个参数。
    • 注: input方法默认获取param
  • Request::instance()->request() / input('request.')

    • 用法同get,获取$_REQUEST变量。
  • Request::instance()->server() / input('server.')

    • 用法同get,获取$_SERVER 变量。
  • Request::instance()->cookie() / input('cookie.') / Cookie::get('name') / cookie('name')

    • 用法同get,获取$_COOKIE 变量。
  • Request::instance()->header() / input('header.')

    • 用法同get,获取用户传入的HTTP头。
  • Request::instance()->file()

    • 用法同get,获取$_FILES 变量。
  • request() 助手函数

    • 实例化request对象。
    • 例子:
      $req=request(); 
      // 相当于 $req=Request::instance()
      
    • 这种使用方法比较常见,还可以获取用户传入的请求信息。可将前面的Request::instance()直接替换成request()
    • 例子: request()->post();
  • Request::instance() 其他用户变量

  • 模板中获取参数

    • {$Request.变量类型.变量名}

ThinkPHP 3.* 框架

  • I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'])
    • 获取变量。
    • 例子: I('get.id');
    • I('get.'); (使用方法同input)

19. 路由传入值 (Action参数绑定 - ThinkPHP 框架)

通过路由传入

namespace Home\Controller;
use Think\Controller;

class BlogController extends Controller
{
    public function read($id)
    { 
        echo 'id='.$id; 
    }
    
    public function archive($year='2013',$month='01')
    {
        echo 'year='.$year.'&month='.$month;
    }
}

访问 URL 示例:

  • /index.php/Home/Blog/read/id/5
  • /index.php/Home/Blog/archive/year/2013/month/11
  • ?c=Blog&a=read&id=5
  • ?c=Blog&a=archive&year=2013&month=11

核心总结

ThinkPHP 3.2作为一款经典的PHP开发框架,以其清晰的MVC结构、灵活的URL路由和全面的安全机制,为开发者提供了高效稳定的开发体验。

框架核心特色:

  1. 标准化架构 - 规范的目录结构和命名约定,提升团队协作效率
  2. 灵活路由 - 支持四种URL模式,适配不同服务器环境
  3. 安全防护体系 - 从输入过滤到表单令牌验证,构建多层安全防线
  4. 便捷开发 - 连贯操作、查询条件预处理等特性简化开发流程

安全设计亮点:

  • I函数过滤机制 - 统一的输入安全处理
  • 表单合法性检测 - 防止非法字段注入
  • 自动令牌验证 - 有效防御CSRF攻击
  • SQL预处理 - 防范注入风险

实践启示:
ThinkPHP 3.2体现了“安全内置”的设计理念,将安全防护融入框架基础功能中。这种将安全性作为框架原生特性的思路,对现代Web开发仍具有重要参考价值——真正的框架安全不是附加功能,而是底层设计的一部分

尽管技术不断演进,但ThinkPHP 3.2中体现的架构思想与安全实践,依然是理解PHP框架设计与Web安全防护的宝贵案例。