ThinkPHP 3.2.2 完全开发手册


2014/04/14 上海顶想信息科技有限公司 版权所有 ThinkPHP 完全开发手册 版本号 V3.2.2 序言 手册阅读须知:本手册仅针对ThinkPHP3.2.*版本,3.1版本的请 参考这里 请使用左右键( <-- 和和 --> )翻页阅读, CTRL+ALT+F 全屏阅读。 版权申明 发布本资料须遵守开放出版许可协议 1.0 或者更新版本。 未经版权所有者明确授权,禁止发行本文档及其被实质上修改的版本。 未经版权所有者事先授权,禁止将此作品及其衍生作品以标准(纸质)书籍形式发行。 如果有兴趣再发行或再版本手册的全部或部分内容,不论修改过与否,或者有任何问题,请联系版权所有 者 liu21st@gmail.com。 对ThinkPHP有任何疑问或者建议,请进入官方讨论区 [ http://www.thinkphp.cn/topic ] 发布相关讨 论。 有关ThinkPHP项目及本文档的最新资料,请及时访问ThinkPHP项目主站 http://www.thinkphp.cn。 本文档的版权归ThinkPHP文档小组所有,本文档及其描述的内容受有关法律的版权保护,对本文档内容 的任何形式的非法复制,泄露或散布,将导致相应的法律责任。 捐赠我们 ThinkPHP一直在致力于简化企业和个人的WEB应用开发,您的帮助是对我们最大的支持和动力! 我们的团队8年来一直在坚持不懈地努力,并坚持开源和免费提供使用,帮助开发人员更加方便的进行 WEB应用的快速开发,如果您对我们的成果表示认同并且觉得对你有所帮助我们愿意接受来自各方面的捐 赠^_^。 官方微博 - 2 - 基础 ThinkPHP是一个快速、简单的基于MVC和面向对象的轻量级PHP开发框架,遵循Apache2开源协议发 布,从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,尤其注重开发体 验和易用性,并且拥有众多的原创功能和特性,为WEB应用开发提供了强有力的支持。 3.2版本则在原来的基础上进行一些架构的调整,引入了命名空间支持和模块化的完善,为大型应用和模 块化开发提供了更多的便利。 [2014/04/15] 3.2.2 主要更新: 应用支持包括PHP/JSON/INI/XML/YAML以及自定义格式的配置文件; 支持模块的自动生成; 增加插件控制器的访问支持; cookie函数增加httponly参数支持; 改进模型类的安全处理; 修正了3.2.1发布以来社区反馈的一些BUG; [2014/02/14] 3.2.1 主要更新: 性能较3.2.0版本提升30%; 模块类库可以配置不使用命名空间; 支持运行时Lite文件生成,用于替换应用入口文件; 改进路由定义的闭包支持; 增加API应用模式,更加高效; 增加REST、RPC、HPROSE和YAR控制器扩展支持; 增加REPL行为扩展,增强框架开发的调试手段; 重写查询缓存功能; 语言定义支持变量传入; C函数获取配置参数支持默认值; 支持操作方法绑定到类; 改进和修正Image、Verify和Upload类; 修正了3.2.0发布以来社区反馈的一些BUG; [2013/12/18] 3.2.0 主要更新: 多模块的支持完善(原来的独立分组已经改为模块); - 3 - 获取ThinkPHP 环境要求 命名空间及新的自动加载机制; 全新的应用模式; 更完善的Action参数绑定; PDO参数绑定完善; 更方便的云平台支持,包括SAE和BAE; 路由功能的增强及闭包支持 ; 子域名及泛域名部署的完善 ; 完全重写的部分工具类库(包括验证码、图像处理、权限处理、文件上传等)。 获取ThinkPHP的方式很多,官方网站(http://thinkphp.cn)是最好的下载和文档获取来源。 官网提供了稳定版本的下载:http://thinkphp.cn/down/framework.html 如果你希望保持最新的更新,可以通过github获取当前最新的版本更新。 Git获取地址列表(你可以选择一个最快的地址): Github: https://github.com/liu21st/thinkphp Oschina: http://git.oschina.net/liu21st/thinkphp.git Code: https://code.csdn.net/topthink2011/ThinkPHP ThinkPHP无需任何安装,直接拷贝到你的电脑或者服务器的WEB运行目录下面即可。 框架本身没有什么特别模块要求,具体的应用系统运行环境要求视开发所涉及的模块。ThinkPHP底层运 行的内存消耗极低,而本身的文件大小也是轻量级的,因此不会出现空间和内存占用的瓶颈。 支持的服务器和数据库环境 支持Windows/Unix服务器环境 可运行于包括Apache、IIS和nginx在内的多种WEB服务器和模式 支持Mysql、MsSQL、PgSQL、Sqlite、Oracle、Ibase、Mongo以及PDO等多种数据库和连接 PHP版本要求 PHP5.3以上版本(注意:PHP5.3dev版本和PHP6均不支持) - 4 - 目录结构 对于刚刚接触PHP或者ThinkPHP的新手,我们推荐使用集成开发环 境WAMPServer(wampserver是一个集成了Apache、PHP和MySQL的开发套件,而且支持不同 PHP版本、MySQL版本和Apache版本的切换)来使用ThinkPHP进行本地开发和测试。 下载3.2框架后,解压缩到web目录下面,可以看到初始的目录结构如下: 3.2版本相比之前的版本自带了一个完整的应用目录结构和默认的应用入口文件,开发人员可以在这个基 础之上灵活调整。其中, Application 和 Public 目录下面都是空的。 README.md文件仅用于说明,实际部署的时候可以删除。 上面的目录结构和名称是可以改变的,这取决于你的入口文件和配置参数。 Application目录默认是空的,但是第一次访问入口文件会自动生成,参考后面的入口文件部分。 其中框架目录ThinkPHP的结构如下: 1. www WEB部署目录(或者子目录) 2. ├─index.php 入口文件 3. ├─README.md README文件 4. ├─Application 应用目录 5. ├─Public 资源文件目录 6. └─ThinkPHP 框架目录 1. ├─ThinkPHP 框架系统目录(可以部署在非web目录下面) 2. │ ├─Common 核心公共函数目录 3. │ ├─Conf 核心配置目录 4. │ ├─Lang 核心语言包目录 5. │ ├─Library 框架类库目录 6. │ │ ├─Think 核心Think类库包目录 7. │ │ ├─Behavior 行为类库目录 8. │ │ ├─Org Org类库包目录 9. │ │ ├─Vendor 第三方类库目录 10. │ │ ├─ ... 更多类库目录 11. │ ├─Mode 框架应用模式目录 12. │ ├─Tpl 系统模板目录 13. │ ├─LICENSE.txt 框架授权协议文件 - 5 - 入口文件 上述应用的目录结构只是默认设置,事实上,在实际部署应用的时候,我们建议除了应用入口文件 和 Public 资源目录外,其他文件都放到非WEB目录下面,具有更好的安全性。 ThinkPHP采用单一入口模式进行项目部署和访问,无论完成什么功能,一个应用都有一个统一(但不一 定是唯一)的入口。 应该说,所有应用都是从入口文件开始的,并且不同应用的入口文件是类似的。 入口文件定义 入口文件主要完成: 定义框架路径、项目路径(可选) 定义调试模式和应用模式(可选) 定义系统相关常量(可选) 载入框架入口文件(必须) 默认情况下,3.2版本的框架已经自带了一个应用入口文件(以及默认的目录结构),内容如下: 注意:3.2版本开始无需定义APP_NAME常量 如果你改变了项目目录(例如把 Application 更改为 Apps ),只需要在入口文件更改APP_PATH常量 定义即可: 注意:APP_PATH的定义支持相对路径和绝对路径,但必须以“/”结束 如果你调整了框架核心目录的位置或者目录名,只需要这样修改: 14. │ ├─logo.png 框架LOGO文件 15. │ ├─README.txt 框架README文件 16. │ └─index.php 框架入口文件 1. define('APP_PATH','./Application/'); 2. require './ThinkPHP/ThinkPHP.php'; 1. define('APP_PATH','./Apps/'); 2. require './ThinkPHP/ThinkPHP.php'; - 6 - 也可以单独定义一个THINK_PATH常量用于引入: 和APP_PATH一样THINK_PATH路径定义也必须以“/”结尾。 给THINK_PATH和APP_PATH定义绝对路径会提高系统的加载效率。 入口文件中的其他定义 一般不建议在入口文件中做过多的操作,但可以重新定义一些系统常量,入口文件中支持定义(建议)的 一些系统常量包括: 常量 描述 THINK_PATH 框架目录 APP_PATH 应用目录 RUNTIME_PATH 应用运行时目录(可写) APP_DEBUG 应用调试模式 (默认为false) STORAGE_TYPE 存储类型(默认为File) APP_MODE 应用模式(默认为common) 注意:所有路径常量都必须以“/”结尾 例如,我们可以在入口文件中重新定义相关目录并且开启调试模式: 1. define('APP_PATH','./Application/'); 2. require './Think/ThinkPHP.php'; 1. define('APP_PATH','./Application/'); 2. define('THINK_PATH',realpath('../Think').'/'); 3. require THINK_PATH.'ThinkPHP.php'; 1. // 定义应用目录 2. define('APP_PATH','./Apps/'); 3. // 定义运行时目录 4. define('RUNTIME_PATH','./Runtime/'); 5. // 开启调试模式 6. define('APP_DEBUG',True); 7. // 更名框架目录名称,并载入框架入口文件 - 7 - 自动生成 这样最终的应用目录结构如下: 入口文件中还可以定义一些系统变量,用于相关的绑定操作(通常用于多个入口的情况),这个会 在后面涉及,暂且不提。 自动创建目录 在第一次访问应用入口文件的时候,会显示如图所示的默认的欢迎页面,并自动生成了一个默认的应用模 块Home。 接下来再看原来空的 Application 目录下面,已经自动生成了公共模块 Common 、默认的 Home 模块 和 Runtime 运行时目录的目录结构: 8. require './Think/ThinkPHP.php'; 1. www WEB部署目录(或者子目录) 2. ├─index.php 应用入口文件 3. ├─Apps 应用目录 4. ├─Public 资源文件目录 5. ├─Runtime 运行时目录 6. └─Think 框架目录 1. Application 2. ├─Common 应用公共模块 3. │ ├─Common 应用公共函数目录 4. │ └─Conf 应用公共配置文件目录 5. ├─Home 默认生成的Home模块 6. │ ├─Conf 模块配置文件目录 - 8 - 模块 如果你不是Windows环境下面的话,需要对应用目录 Application 设置可写权限才能自动生成。 如果不是调试模式的话,会在Runtime目录下面生成 common~runtime.php 文件(应用编译缓存文 件)。 目录安全文件 在自动生成目录结构的同时,在各个目录下面我们还看到了index.html文件,这是ThinkPHP自动生成的 目录安全文件。 为了避免某些服务器开启了目录浏览权限后可以直接在浏览器输入URL地址查看目录,系统默认开启了目 录安全文件机制,会在自动生成目录的时候生成空白的 index.html 文件,当然安全文件的名称可以设 置,例如你想给安全文件定义为 default.html 可以在入口文件中添加: 如果你的环境足够安全,不希望生成目录安全文件,可以在入口文件里面关闭目录安全文件的生成,例 如: 3.2发布版本自带了一个应用目录结构,并且带了一个默认的应用入口文件,方便部署和测试,默认的应 用目录是Application(实际部署过程中可以随意设置)。 通常情况下3.2无需使用多应用模式,因为大多数情况下,我们都可以通过多模块化以及多入口的设计来 解决应用的扩展需求。 7. │ ├─Common 模块函数公共目录 8. │ ├─Controller 模块控制器目录 9. │ ├─Model 模块模型目录 10. │ └─View 模块视图文件目录 11. ├─Runtime 运行时目录 12. │ ├─Cache 模版缓存目录 13. │ ├─Data 数据目录 14. │ ├─Logs 日志目录 15. │ └─Temp 缓存目录 1. define('DIR_SECURE_FILENAME', 'default.html'); 2. define('APP_PATH','./Application/'); 3. require './ThinkPHP/ThinkPHP.php'; 1. define('BUILD_DIR_SECURE', false); - 9 - 控制器 模块设计 新版采用模块化的设计架构,下面是一个应用目录下面的模块目录结构,每个模块可以方便的卸载和部 署,并且支持公共模块。 注意:3.2版本在原来3.1.3的独立分组的基础上进行了改进,改进后的独立分组就是新版的模块,之 前的模块则改称为控制器。 每个模块是相对独立的,其目录结构如下: 由于采用多层的MVC机制,除了Conf和Common目录外,每个模块下面的目录结构可以根据需要 灵活设置和添加,所以并不拘泥于上面展现的目录 我们可以在自动生成的Application/Home/Controller目录下面找到一个 IndexController.class.php 文件,这就是默认的Index控制器文件。 控制器类的命名方式是:控制器名(驼峰法,首字母大写)+Controller 控制器文件的命名方式是:类名+class.php(类文件后缀) 1. Application 默认应用目录(可以设置) 2. ├─Common 公共模块(不能直接访问) 3. ├─Home 前台模块 4. ├─Admin 后台模块 5. ├─... 其他更多模块 6. ├─Runtime 默认运行时目录(可以设置) 1. ├─Module 模块目录 2. │ ├─Conf 配置文件目录 3. │ ├─Common 公共函数目录 4. │ ├─Controller 控制器目录 5. │ ├─Model 模型目录 6. │ ├─Logic 逻辑目录(可选) 7. │ ├─Service Service目录(可选) 8. │ ... 更多分层目录可选 9. │ └─View 视图目录 - 10 - 默认的欢迎页面其实就是访问的Home模块下面的Index控制器类的index操作方法 我们修改默认的index 操作方法如下: 再次运行应用入口文件,浏览器会显示: hello,world! 。 我们再来看下控制器类,IndexController控制器类的开头是命名空间定义: 这是系统的规范要求,表示当前类是Home模块下的控制器类,命名空间和实际的控制器文件所在的路径 是一致的,也就是说: Home\Controller\IndexController 类 对应的控制器文件位于应用目录下面的 Home/Controller/IndexController.class.php ,如果你改变了当前的模块名,那么这个控制器类的 命名空间也需要随之修改。 注意:命名空间定义必须写在所有的PHP代码之前声明,否则会出错 表示引入 Think\Controller 命名空间便于直接使用。 所以, 等同于使用: 对于3.1的用户而言,如果你习惯了使用Action定义控制器的话,可以这样定义: 1. namespace Home\Controller; 2. use Think\Controller; 3. class IndexController extends Controller { 4. public function index(){ 5. echo 'hello,world!'; 6. } 7. } 1. namespace Home\Controller; 1. use Think\Controller; 1. use Think\Controller; 2. class IndexController extends Controller 1. class IndexController extends \Think\Controller 1. namespace Home\Action; 2. use Think\Action; 3. class IndexAction extends Action{ 4. } - 11 - 开发规范 然后,在配置文件中,设置: 上面的设置方式通常可以用于原有3.1项目的升级。 命名规范 使用ThinkPHP开发的过程中应该尽量遵循下列命名规范: 类文件都是以.class.php为后缀(这里是指的ThinkPHP内部使用的类库文件,不代表外部加载的 类库文件),使用驼峰法命名,并且首字母大写,例如 DbMysql.class.php ; 类的命名空间地址和所在的路径地址一致,例如 Home\Controller\UserController 类所在的 路径应该是 Application/Home/Controller/UserController.class.php ; 确保文件的命名和调用大小写一致,是由于在类Unix系统上面,对大小写是敏感的(而 ThinkPHP在调试模式下面,即使在Windows平台也会严格检查大小写); 类名和文件名一致(包括上面说的大小写一致),例如 UserController 类的文件命名 是 UserController.class.php , InfoModel类的文件名是 InfoModel.class.php , 并且不 同的类库的类命名有一定的规范; 函数、配置文件等其他类库文件之外的一般是以 .php 为后缀(第三方引入的不做要求); 函数的命名使用小写字母和下划线的方式,例如 get_client_ip ; 方法的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 getUserName , _parseType ,通常下划线开头的方法属于私有方法; 属性的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 tableName 、 _instance ,通常下划线开头的属性属于私有属性; 以双下划线“__”打头的函数或方法作为魔法方法,例如 __call 和 __autoload ; 常量以大写字母和下划线命名,例如 HAS_ONE 和 MANY_TO_MANY ; 配置参数以大写字母和下划线命名,例如 HTML_CACHE_ON ; 语言变量以大写字母和下划线命名,例如 MY_LANG ,以下划线打头的语言变量通常用于系统语言 变量,例如 _CLASS_NOT_EXIST_ ; 对变量的命名没有强制的规范,可以根据团队规范来进行; ThinkPHP的模板文件默认是以 .html 为后缀(可以通过配置修改); 数据表和字段采用小写加下划线方式命名,并注意字段名不要以下划线开头,例如 think_user 表和 user_name 字段是正确写法,类似 _username 这样的数据表字段可能会被过滤。 1. 'DEFAULT_C_LAYER'=>'Action' - 12 - 特例:在ThinkPHP里面,有一个函数命名的特例,就是单字母大写函数,这类函数通常是某些操作的快 捷定义,或者有特殊的作用。例如:A、D、S、L 方法等等,他们有着特殊的含义,后面会有所了解。 由于ThinkPHP默认全部使用UTF-8编码,所以请确保你的程序文件采用UTF-8编码格式保存,并且 去掉BOM信息头(去掉BOM头信息有很多方式,不同的编辑器都有设置方法,也可以用工具进行统 一检测和处理),否则可能导致很多意想不到的问题。 开发建议 在使用ThinkPHP进行开发的过程中,我们给出如下建议,会让你的开发变得更轻松: 遵循框架的命名规范和目录规范; 开发过程中尽量开启调试模式,及早发现问题; 多看看日志文件,查找隐患问题; 养成使用I函数获取输入变量的好习惯; 更新或者环境改变后遇到问题首要问题是清空Runtime目录; - 13 - 配置 配置格式 ThinkPHP提供了灵活的全局配置功能,采用最有效率的PHP返回数组方式定义,支持惯例配置、公共配 置、模块配置、调试配置和动态配置。 对于有些简单的应用,你无需配置任何配置文件,而对于复杂的要求,你还可以增加动态配置文件。 系统的配置参数是通过静态变量全局存取的,存取方式简单高效。 PHP数组定义 ThinkPHP框架中所有配置文件的定义格式均采用返回PHP数组的方式,格式为: 配置参数不区分大小写(因为无论大小写定义都会转换成小写),所以下面的配置等效: 但是我们建议保持大写定义配置参数的规范。 还可以在配置文件中可以使用二维数组来配置更多的信息,例如: 1. //项目配置文件 2. return array( 3. 'DEFAULT_MODULE' => 'Index', //默认模块 4. 'URL_MODEL' => '2', //URL模式 5. 'SESSION_AUTO_START' => true, //是否开启session 6. //更多配置参数 7. //... 8. ); 1. //项目配置文件 2. return array( 3. 'default_module' => 'Index', //默认模块 4. 'url_model' => '2', //URL模式 5. 'session_auto_start' => true, //是否开启session 6. //更多配置参数 7. //... 8. ); 1. //项目配置文件 - 14 - 需要注意的是,二级参数配置区分大小写,也就说读取确保和定义一致。 其他配置格式支持 3.2.2版本开始,配置文件增加 yaml/json/xml/ini 以及自定义格式支持。 我们可以在应用入口文件中定义应用的配置文件的后缀,例如: 定义后,应用的配置文件(包括模块的配置文件)后缀都统一采用.ini。 该配置不会影响框架内部的配置文件加载。 ini格式配置示例: xml格式配置示例: 2. return array( 3. 'DEFAULT_MODULE' => 'Index', //默认模块 4. 'URL_MODEL' => '2', //URL模式 5. 'SESSION_AUTO_START' => true, //是否开启session 6. 'USER_CONFIG' => array( 7. 'USER_AUTH' => true, 8. 'USER_TYPE' => 2, 9. ), 10. //更多配置参数 11. //... 12. ); 1. define('CONF_EXT','.ini'); 1. DEFAULT_MODULE=Index ;默认模块 2. URL_MODEL=2 ;URL模式 3. SESSION_AUTO_START=on ;是否开启session 1. 2. Index 3. 2 4. 1 5. - 15 - 配置加载 yaml格式配置示例: json格式配置示例: 除了 yaml/json/xml/ini 格式之外,我们还可以自定义配置格式,定义如下: CONF_PARSE定义的解析函数返回值是一个配置数组。 在ThinkPHP中,一般来说应用的配置文件是自动加载的,加载的顺序是: 以上是配置文件的加载顺序,因为后面的配置会覆盖之前的同名配置(在没有生效的前提下),所 以配置的优先顺序从右到左。 下面说明下不同的配置文件的区别和位置: 惯例配置 惯例重于配置是系统遵循的一个重要思想,框架内置有一个惯例配置文件(位 于 ThinkPHP/Conf/convention.php ),按照大多数的使用对常用参数进行了默认配置。所以,对于应 用的配置文件,往往只需要配置和惯例配置不同的或者新增的配置参数,如果你完全采用默认配置,甚至 可以不需要定义任何配置文件。 建议仔细阅读下系统的惯例配置文件中的相关配置参数,了解下系统默认的配置参数。 1. default_module:Index #默认模块 2. url_model:2 #URL模式 3. session_auto_start:True #是否开启session 1. { 2. "default_module":"Index", 3. "url_model":2, 4. "session_auto_start":True 5. } 1. define('CONF_EXT','.test'); // 配置自定义配置格式(后缀) 2. define('CONF_PARSE','parse_test'); // 对应的解析函数 1. 惯例配置->应用配置->模式配置->调试配置->状态配置->模块配置->扩展配置->动态配置 - 16 - 应用配置 应用配置文件也就是调用所有模块之前都会首先加载的公共配置文件(默认位 于 Application/Common/Conf/config.php )。 如果更改了公共模块的名称的话,公共配置文件的位置也相应改变 模式配置 如果使用了普通应用模式之外的应用模式的话,还可以为应用模式(后面会有描述)单独定义配置文件, 文件命名规范是: Application/Common/Conf/config_应用模式名称.php (仅在运行该模式下面才会 加载)。 模式配置文件是可选的 调试配置 如果开启调试模式的话,则会自动加载框架的调试配置文件(位于 ThinkPHP/Conf/debug.php )和应用 调试配置文件(位于 Application/Common/Conf/debug.php ) 状态配置 每个应用都可以在不同的情况下设置自己的状态(或者称之为应用场景),并且加载不同的配置文件。 举个例子,你需要在公司和家里分别设置不同的数据库测试环境。那么可以这样处理,在公司环境中,我 们在入口文件中定义: 那么就会自动加载该状态对应的配置文件(位于 Application/Common/Conf/office.php )。 如果我们回家后,我们修改定义为: 那么就会自动加载该状态对应的配置文件(位于 Application/Common/Conf/home.php )。 状态配置文件是可选的 模块配置 1. define('APP_STATUS','office'); 1. define('APP_STATUS','home'); - 17 - 读取配置 每个模块会自动加载自己的配置文件(位于 Application/当前模块名/Conf/config.php )。 如果使用了普通模式之外的其他应用模式,你还可以为应用模式单独定义配置文件,命名规范为: Application/当前模块名/Conf/config_应用模式名称.php (仅在运行该模式下面才会加载)。 模块还可以支持独立的状态配置文件(该功能为3.2.2版本新增),命名规范为: Application/当前模 块名/Conf/应用状态.php 。 模块配置文件是可选的 如果你的应用的配置文件比较大,想分成几个单独的配置文件或者需要加载额外的配置文件的话,可以考 虑采用扩展配置或者动态配置(参考后面的描述)。 无论何种配置文件,定义了配置文件之后,都统一使用系统提供的C方法(可以借助Config单词来帮助记 忆)来读取已有的配置。 获取已经设置的参数值:C('参数名称') 例如, 可以读取到系统的调试模式的设置值,同样,由于配置参数不区分大小写,因此 是等效的,但是建议使用大写方式的规范。 注意:配置参数名称中不能含有 “.” 和特殊字符,允许字母、数字和下划线。 如果 url_model 尚未存在设置,则返回NULL。 如果是3.2.1版本的话,支持设置默认值,例如: 如果my_config尚未设置的话,则返回default_config。 C方法也可以用于读取二维配置: 1. $model = C('URL_MODEL'); 1. $model = C('url_model'); 1. C('my_config',null,'default_config'); - 18 - 动态配置 扩展配置 因为配置参数是全局有效的,因此C方法可以在任何地方读取任何配置,即使某个设置参数已经生效过期 了。 之前的方式都是通过预先定义配置文件的方式,而在具体的操作方法里面,我们仍然可以对某些参数进行 动态配置(或者增加新的配置),主要是指那些还没有被使用的参数。 设置新的值: C('参数名称','新的参数值'); 例如,我们需要动态改变数据缓存的有效期的话,可以使用 动态配置赋值仅对当前请求有效,不会对以后的请求造成影响。 动态改变配置参数的方法和读取配置的方法在使用上面非常接近,都是使用C方法,只是参数的不同。 也 可以支持二维数组的读取和设置,使用点语法进行操作,如下: 扩展配置可以支持自动加载额外的自定义配置文件,并且配置格式和项目配置一样。 设置扩展配置的方式 如下(多个文件用逗号分隔): 假设扩展配置文件 user.php 和 db.php 分别用于用户配置和数据库配置,这样做的好处是哪怕以后关闭 调试模式,你修改db配置文件后依然会自动生效。 1. //获取用户配置中的用户类型设置 2. C('USER_CONFIG.USER_TYPE'); 1. // 动态改变缓存有效期 2. C('DATA_CACHE_TIME',60); 1. // 获取已经设置的参数值 2. C('USER_CONFIG.USER_TYPE'); 3. //设置新的值 4. C('USER_CONFIG.USER_TYPE',1); 1. // 加载扩展配置文件 2. 'LOAD_EXT_CONFIG' => 'user,db', - 19 - 批量配置 如果在应用公共设置文件中配置的话,那么会自动加载应用公共配置目录下面的配置文 件 Application/Common/Conf/user.php 和 Application/Common/Conf/db.php 。 如果在模块(假设是Home模块)的配置文件中配置的话,则会自动加载模块目录下面的配置文件 Application/Home/Conf/user.php 和 Application/Home/Conf/db.php 。 默认情况下,扩展配置文件中的设置参数会并入项目配置文件中。也就是默认都是一级配置参数,例如 user.php中的配置参数如下: 那么,最终获取用户参数的方式是: 如果配置文件改成: 则最终获取用户参数的方式改成: C配置方法支持批量配置,例如: $config数组中的配置参数会合并到现有的全局配置中。 我们可以通过这种方式读取数据库中的配置参数,例如: 1. 2, //用户类型 5. 'USER_AUTH_ID' => 10, //用户认证ID 6. 'USER_AUTH_TYPE' => 2, //用户认证模式 7. ); 1. C('USER_AUTH_ID'); 1. // 加载扩展配置文件 2. 'LOAD_EXT_CONFIG' => array('USER'=>'user','DB'=>'db'), 1. C('USER.USER_AUTH_ID'); 1. $config = array('WEB_SITE_TITLE'=>'ThinkPHP','WEB_SITE_DESCRIPTION'=>'开源PHP框 架'); 2. C($config); - 20 - 合并之后,我们就可以和前面读取普通配置参数一样,读取数据库中的配置参数了,当然也可以动态改 变。 1. // 读取数据库中的配置(假设有一个config表用于保存配置参数) 2. $config = M('Config')->getField('name,value'); 3. // config是一个关联数组 键值就是配置参数 值就是配置值 4. // 例如: array('config1'=>'val1','config2'=>'val2',...) 5. C($config); // 合并配置参数到全局配置 1. // 读取合并到全局配置中的数据库中的配置参数 2. C('CONFIG1'); 3. // 动态改变配置参数(当前请求有效,不会自动保存到数据库) 4. C('CONFIG2','VALUE_NEW'); - 21 - 架构 模块化设计 ThinkPHP3.2的架构在3.1版本的基础上进行了改进和增强,我们先初步了解下新版的架构特性,包括: 模块化设计 URL模式 多层MVC CBD模式 命名空间 自动加载 应用模式 一个完整的ThinkPHP应用基于模块/控制器/操作设计,并且,如果有需要的话,可以支持多入口文件和 多级控制器。 ThinkPHP3.2采用模块化的架构设计思想,对目录结构规范做了调整,可以支持多模块应用的创建,让应 用的扩展更加方便。 一个典型的URL访问规则是(我们以默认的PATHINFO模式为例说明,当然也可以支持普通的URL模 式): ThinkPHP3.2的应用可以支持切换到命令行访问,如果切换到命令行模式下面的访问规则是: 解释下其中的几个概念: 名称 描述 应用 基于同一个入口文件访问的项目我们称之为一个应用。 模块 一个应用下面可以包含多个模块,每个模块在应用目录下面都是一个独立的子目录。 控制 器 每个模块可以包含多个控制器,一个控制器通常体现为一个控制器类。 操作 每个控制器类可以包含多个操作方法,也可能是绑定的某个操作类,每个操作是URL访问的最 1. http://serverName/index.php(或者其他应用入口文件)/模块/控制器/操作/[参数名/参数值 ...] 1. >php.exe index.php(或其它应用入口文件) 模块/控制器/操作/[参数名/参数值...] - 22 - 操作 小单元。 模块化设计的思想下面模块是最重要的部分,模块其实是一个包含配置文件、函数文件和MVC文件 (目录)的集合。 模块设计 新版采用模块化的设计架构,下面是一个应用目录下面的模块目录结构,每个模块可以方便的卸载和部 署,并且支持公共模块。 注意:3.2版本在原来3.1.3的独立分组的基础上进行了改进,改进后的独立分组就是新版的模块,之 前的模块则改称为控制器。 默认情况下,只要应用目录下面存在模块目录,该模块就可以访问,只 有当你希望禁止某些模块或者仅允许模块访问的时候才需要进行模块列表的相关设置。 每个模块是相对独立的,其目录结构如下: 由于采用多层的MVC机制,除了Conf和Common目录外,每个模块下面的目录结构可以根据需要 灵活设置和添加,所以并不拘泥于上面展现的目录 公共模块 1. Application 默认应用目录(可以设置) 2. ├─Common 公共模块(不能直接访问) 3. ├─Home 前台模块 4. ├─Admin 后台模块 5. ├─... 其他更多模块 6. ├─Runtime 默认运行时目录(可以设置) 1. ├─Module 模块目录 2. │ ├─Conf 配置文件目录 3. │ ├─Common 公共函数目录 4. │ ├─Controller 控制器目录 5. │ ├─Model 模型目录 6. │ ├─Logic 逻辑目录(可选) 7. │ ├─Service Service目录(可选) 8. │ ... 更多分层目录可选 9. │ └─View 视图目录 - 23 - Common模块是一个特殊的模块,是应用的公共模块,访问所有的模块之前都会首先加载公共模块下面 的配置文件( Conf/config.php )和公共函数文件( Common/function.php )。但Common模块本身 不能通过URL直接访问,公共模块的其他文件则可以被其他模块继承或者调用。 公共模块的位置可以通过COMMON_PATH常量改变,我们可以在入口文件中重新定 义COMMON_PATH如下: 其应用目录结构变成: 定义之后,Application目录下面就不再需要Common目录了。 自动生成模块目录 从3.2.2版本开始,可以支持自动生成默认模块之外的模块目录以及批量生成控制器和模型类。 例如,如果我们需要生成一个Admin模块用于后台应用,在应用入口文件中定义如下: 然后访问URL地址 就会生成Admin模块的目录,并生成一个默认的控制器类 Admin\Controller\IndexController 。 如果 需要生成更多的控制器类,可以定义 BUILD_CONTROLLER_LIST 常量,例如: 1. define('COMMON_PATH','./Common/'); 2. define('APP_PATH','./Application/'); 3. require './ThinkPHP/ThinkPHP.php'; 1. www WEB部署目录(或者子目录) 2. ├─index.php 入口文件 3. ├─README.md README文件 4. ├─Common 应用公共模块目录 5. ├─Application 应用模块目录 6. ├─Public 应用资源文件目录 7. └─ThinkPHP 框架目录 1. // 绑定Admin模块到当前入口文件 2. define('BIND_MODULE','Admin'); 3. define('APP_PATH','./Application/'); 4. require './ThinkPHP/ThinkPHP.php'; 1. http://serverName/index.php 1. // 绑定Admin模块到当前入口文件 - 24 - 访问后会自动生成三个指定的控制器类: 注意:默认生成的控制器类都是继承 Think\Controller ,如果需要继承其他的公共类需要另外调 整。 如果在应用的公共配置文件中设置关闭了 APP_USE_NAMESPACE 的话,生成的控制器类则不会 采用命名空间定义。 还可以自己手动调用 Think\Build 类的方法来生成控制器类,例如: 同样,也可以定义 BUILD_MODEL_LIST 支持生成多个模型类: 访问会自动生成模型类: 注意:默认生成的模型类都是继承 Think\Model ,如果需要继承公共的模型类需要另外调整。 如 果在应用的公共配置文件中设置关闭了 APP_USE_NAMESPACE 的话,生成的模型类则不会采用命名 空间定义。 2. define('BIND_MODULE','Admin'); 3. define('BUILD_CONTROLLER_LIST','Index,User,Menu'); 4. define('APP_PATH','./Application/'); 5. require './ThinkPHP/ThinkPHP.php'; 1. Admin\Controller\IndexController 2. Admin\Controller\UserController 3. Admin\Controller\MenuController 1. // 生成Admin模块的Role控制器类 2. // 默认类库为Admin\Controller\RoleController 3. // 如果已经存在则不会重新生成 4. \Think\Build::buildController('Admin','Role'); 1. // 绑定Admin模块到当前入口文件 2. define('BIND_MODULE','Admin'); 3. define('BUILD_CONTROLLER_LIST','Index,User,Menu'); 4. define('BUILD_MODEL_LIST','User,Menu'); 5. define('APP_PATH','./Application/'); 6. require './ThinkPHP/ThinkPHP.php'; 1. Admin\Model\UserModel 2. Admin\Model\MenuModel - 25 - 也可以自己手动调用Think\Build类的方法来生成模型类,例如: 禁止访问模块 3.2对模块的访问是自动判断的,所以通常情况下无需配置模块列表即可访问,但可以配置禁止访问的模 块列表(用于被其他模块调用或者不开放访问),默认配置中是禁止访问 Common 模块和 Runtime 模块 (Runtime目录是默认的运行时目录),我们可以增加其他的禁止访问模块列表: 设置后,Api模块不能通过URL直接访问,事实上,可能我们只是在该模块下面放置一些公共的接口文 件,因此都是内部调用即可。 设置访问列表 如果你的应用下面模块比较少,还可以设置允许访问列表和默认模块,这样可以简化默认模块的URL访 问。 设置之后,除了Home、Admin和User模块之外的模块都不能被直接访问,并且Home模块是默认访问 模块(可以不出现在URL地址)。 单模块设计 如果你的应用够简单,那么也许仅仅用一个模块就可以完成,那么可以直接设置: 一旦关闭多模块访问后,就只能访问默认模块(这里设置的是Home)。 单模块设计后公共模块依然有效 1. // 生成Admin模块的Role模型类 2. // 默认类库为Admin\Model\RoleModel 3. // 如果已经存在则不会重新生成 4. \Think\Build::buildModel('Admin','Role'); 1. // 设置禁止访问的模块列表 2. 'MODULE_DENY_LIST' => array('Common','Runtime','Api'), 1. 'MODULE_ALLOW_LIST' => array('Home','Admin','User'), 2. 'DEFAULT_MODULE' => 'Home', 1. // 关闭多模块访问 2. 'MULTI_MODULE' => false, 3. 'DEFAULT_MODULE' => 'Home', - 26 - 多入口设计 可以给相同的应用及模块设置多个入口,不同的入口文件可以设置不同的应用模式或者绑定模块。 例如,我们在 index.php 文件的同级目录新增一个 home.php 入口文件,并绑定Home模块: 3.2.0版本写法: 3.2.1以上版本写法: 如果你更改了系统默认的变量设置,则需要做对应的模块绑定的变量调整。 绑定模块后,原来的访问地址 就变成 同样的方式,我们也可以在入口文件中绑定控制器,例如: 3.2.0版本写法: 3.2.1以上版本写法: 1. // 绑定Home模块到当前入口文件 2. $_GET['m'] = 'Home'; 3. define('APP_PATH','./Application/'); 4. require './ThinkPHP/ThinkPHP.php'; 1. // 绑定Home模块到当前入口文件 2. define('BIND_MODULE','Home'); 3. define('APP_PATH','./Application/'); 4. require './ThinkPHP/ThinkPHP.php'; 1. http://serverName/index.php/Home/Index/index 1. http://serverName/home.php/Index/index 1. $_GET['m'] = 'Home'; // 绑定Home模块到当前入口文件 2. $_GET['c'] = 'Index'; // 绑定Index控制器到当前入口文件 3. define('APP_PATH','./Application/'); 4. require './ThinkPHP/ThinkPHP.php'; 1. define('BIND_MODULE', 'Home'); // 绑定Home模块到当前入口文件 2. define('BIND_CONTROLLER','Index'); // 绑定Index控制器到当前入口文件 - 27 - URL模式 绑定模块和控制器后,原来的访问地址: 就变成: 不同的入口文件还可以用于绑定不同的应用模式,参考应用模式部分。 入口文件是应用的单一入口,对应用的所有请求都定向到应用入口文件,系统会从URL参数中解析当前请 求的模块、控制器和操作: 这是3.2版本的标准URL格式。 可以通过设置模块绑定或者域名部署等方式简化URL地址中的模块及控制器名称。 URL大小写 ThinkPHP框架的URL是区分大小写(主要是针对模块、控制器和操作名,不包括应用参数)的,这一点 非常关键,因为ThinkPHP的命名规范是采用驼峰法(首字母大写)的规则,而URL中的模块和控制器都 是对应的文件,因此在Linux环境下面必然存在区分大小写的问题。 框架内置了一个配置参数用于解决URL大小写的问题,如下: 当 URL_CASE_INSENSITIVE 设置为true的时候表示URL地址不区分大小写,这个也是框架在部署模式下 面的默认设置。 当开启调试模式的情况下,这个参数是false,因此你会发现在调试模式下面URL区分大小写的情 况。 URL模式 3. define('APP_PATH','./Application/'); 4. require './ThinkPHP/ThinkPHP.php'; 1. http://serverName/index.php/Home/Index/index 1. http://serverName/home.php/index 1. http://serverName/index.php/模块/控制器/操作 1. 'URL_CASE_INSENSITIVE' => true, - 28 - 如果我们直接访问入口文件的话,由于URL中没有模块、控制器和操作,因此系统会访问默认模块 (Home)下面的默认控制器(Index)的默认操作(index),因此下面的访问是等效的: 这种URL模式就是系统默认的PATHINFO模式,不同的URL模式获取模块和操作的方法不同,ThinkPHP 支持的URL模式有四种:普通模式、PATHINFO、REWRITE和兼容模式,可以设置URL_MODEL参数改 变URL模式。 URL模式 URL_MODEL设置 普通模式 0 PATHINFO模式 1 REWRITE模式 2 兼容模式 3 如果你整个应用下面的模块都是采用统一的URL模式,就可以在应用配置文件中设置URL模式,如果 不同的模块需要设置不同的URL模式,则可以在模块配置文件中设置。 普通模式 普通模式也就是传统的GET传参方式来指定当前访问的模块和操作,例如: http://localhost/? m=home&c=user&a=login&var=value m参数表示模块,c参数表示控制器,a参数表示操作(当然这些参数都是可以配置的),后面的表示其他 GET参数。 如果默认的变量设置和你的应用变量有冲突的话,你需要重新设置系统配置,例如改成下面的: 上面的访问地址则变成: http://localhost/? module=home&controller=user&action=login&var=value 注意,VAR_MODULE只能在应用配置文件中设置,其他参数可以则也可以在模块配置中设置 1. http://serverName/index.php 2. http://serverName/index.php/Home/Index/index 1. 'VAR_MODULE' => 'module', // 默认模块获取变量 2. 'VAR_CONTROLLER' => 'controller', // 默认控制器获取变量 3. 'VAR_ACTION' => 'action', // 默认操作获取变量 - 29 - 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是可定制的,例如,通过下面的配置: 我们还可以支持下面的URL访问: http://localhost/index.php/home-user-login-var-value REWRITE模式 REWRITE模式是在PATHINFO模式的基础上添加了重写规则的支持,可以去掉URL地址里面的入口文件 index.php,但是需要额外配置WEB服务器的重写规则。 如果是Apache则需要在入口文件的同级添加.htaccess文件,内容如下: 接下来,就可以用下面的URL地址访问了: http://localhost/home/user/login/var/value 更多环境的URL重写支持参考部署部分的URL重写。 兼容模式 兼容模式是用于不支持PATHINFO的特殊环境,URL地址是: http://localhost/? s=/home/user/login/var/value 可以更改兼容模式变量的名称定义,例如: 1. // 更改PATHINFO参数分隔符 2. 'URL_PATHINFO_DEPR'=>'-', 1. 2. RewriteEngine on 3. RewriteCond %{REQUEST_FILENAME} !-d 4. RewriteCond %{REQUEST_FILENAME} !-f 5. RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] 6. - 30 - 多层MVC PATHINFO参数分隔符对兼容模式依然有效,例如: 使用以上配置的话,URL访问地址可以变成: http://localhost/?s=/home-user-login-var-value 兼容模式配合Web服务器重写规则的定义,可以达到和REWRITE模式一样的URL效果。 例如,我们在Apache下面的话,.htaccess文件改成如下内容: 就可以和REWRITE模式一样访问下面的URL地址访问了: http://localhost/home/user/login/var/value ThinkPHP基于MVC(Model-View-Controller,模型-视图-控制器)模式,并且均支持多层(multi- Layer)设计。 模型(Model)层 默认的模型层由Model类构成,但是随着项目的增大和业务体系的复杂化,单一的模型层很难解决要求, 从3.1开始推出了多层Model的支持,设计思路很简单,不同的模型层仍然都继承自系统的Model类,但 是在目录结构和命名规范上做了区分。 例如在某个项目设计中需要区分数据层、逻辑层、服务层等不同的模型层,我们可以在模块目录下面创 建 Model 、 Logic 和 Service 目录,把对用户表的所有模型操作分成三层: 1. 数据层:Model/UserModel 用于定义数据相关的自动验证和自动完成和数据存取接口 2. 逻辑层:Logic/UserLogic 用于定义用户相关的业务逻辑 3. 服务层:Service/UserService 用于定义用户相关的服务接口等 而这三个模型操作类统一都继承Model类即可,例如: 数据层: Home/Model/UserModel.class.php 1. 'VAR_PATHINFO' => 'pathinfo' 1. // 更改PATHINFO参数分隔符 2. 'URL_PATHINFO_DEPR'=>'-', 1. 2. RewriteEngine on 3. RewriteCond %{REQUEST_FILENAME} !-d 4. RewriteCond %{REQUEST_FILENAME} !-f 5. RewriteRule ^(.*)$ index.php?s=/$1 [QSA,PT,L] 6. - 31 - 逻辑层: Home/Logic/UserLogic.class.php 服务层: Home/Service/UserService.class.php 这样区分不同的模型层之后对用户数据的操作就非常清晰,在调用的时候,我们也可以用内置的D方法很 方便的调用: 默认的模型层是Model,我们也可以更改设置,例如: 更改之后,实例化的时候需要改成: 对模型层的分层划分是很灵活的,开发人员可以根据项目的需要自由定义和增加模型分层,你也完 全可以只使用Model层。 视图(View)层 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model{ 4. } 1. namespace Home\Logic; 2. use Think\Model; 3. class UserLogic extends Model{ 4. } 1. namespace Home\Service; 2. use Think\Model; 3. class UserService extends Model{ 4. } 1. D('User') //实例化UserModel 2. D('User','Logic') //实例化UserLogic 3. D('User','Service') //实例化UserService 1. 'DEFAULT_M_LAYER' => 'Logic', // 更改默认的模型层名称为Logic 1. D('User') //实例化UserLogic 2. D('User','Model') //实例化UserModel 3. D('User','Service') //实例化UserService - 32 - 视图层由模板和模板引擎组成,在模板中可以直接使用PHP代码,模板引擎的设计会在后面讲述,通过驱 动也可以支持其他第三方的模板引擎。视图的多层可以简单的通过目录(也就是模板主题)区分,例如: 复杂一点的多层视图还可以更进一步,采用不同的视图目录来完成,例如: 这样做的好处是每个不同的视图层都可以支持不同的模板主题功能。 默认的视图层是View目录,我们可以调整设置如下: 非默认视图层目录的模板获取需要使用T函数,后面会讲到。 控制器(Controller)层 ThinkPHP的控制器层由核心控制器和业务控制器组成,核心控制器由系统内部的App类完成,负责应用 (包括模块、控制器和操作)的调度控制,包括HTTP请求拦截和转发、加载配置等。业务控制器则由用 户定义的控制器类完成。多层业务控制器的实现原理和模型的分层类似,例如业务控制器和事件控制器: 访问控制器 Home/Controller/UserController.class.php 定义如下: 事件控制器 Home/Event/UserEvent.class.php 定义如下: UserController负责外部交互响应,通过URL请求响应,例如 http://serverName/User/index ,而 1. View/default/User/add.html 2. View/blue/User/add.html 1. view 普通视图层目录 2. mobile 手机端访问视图层目录 1. 'DEFAULT_V_LAYER' => 'Mobile', // 默认的视图层名称更改为Mobile 1. Controller/UserController //用于用户的业务逻辑控制和调度 2. Event/UserEvent //用于用户的事件响应操作 1. namespace Home\Controller; 2. use Think\Controller; 3. class UserController extends Controller{ 4. } 1. namespace Home\Event; 2. use Think\Controller; 3. class UserEvent extends Controller{ 4. } - 33 - CBD模式 UserEvent 负责内部的事件响应,并且只能在内部调用: A('User','Event'); 默认的访问控制器层是Controller,我们可以调整设置如下: 所以是和外部隔离的。多层控制器的划分也不是强制的,可以根据应用的需要自由分层。控制器分层里面 可以根据需要调用分层模型,也可以调用不同的分层视图(主题)。 在MVC三层中,ThinkPHP并不依赖M或者V,甚至可以只有C或者只有V,这个在ThinkPHP的设计 里面是一个很重要的用户体验设计,用户只需要定义视图,在没有C的情况下也能自动识别。 ThinkPHP从3.0版本开始引入了全新的CBD(核心Core+行为Behavior+驱动Driver)架构模式,因为 从底层开始,框架就采用核心+行为+驱动的架构体系,核心保留了最关键的部分,并在重要位置设置了 标签用以标记,其他功能都采用行为扩展和驱动的方式组合,开发人员可以根据自己的需要,对某个标签 位置进行行为扩展或者替换,就可以方便的定制框架底层,也可以在应用层添加自己的标签位置和添加应 用行为。而标签位置类似于AOP概念中的“切面”,行为都是围绕这个“切面”来进行编程。 Core(核心) ThinkPHP的核心部分包括核心函数库、惯例配置、核心类库(包括基础类和内置驱动及核心行为),这 些是ThinkPHP必不可少的部分。 1. 'DEFAULT_C_LAYER' => 'Event', // 默认的控制器层名称改为Event 1. ThinkPHP/Common/functions.php // 核心函数库 2. ThinkPHP/Conf/convention.php // 惯例配置文件 3. ThinkPHP/Conf/debug.php // 惯例调试配置文件 4. ThinkPHP/Mode/common.php // 普通模式定义文件 5. ThinkPHP/Library/Think // 核心类库包 6. ThinkPHP/Library/Behavior // 系统行为类库 7. ThinkPHP/Library/Think/App.class.php // 核心应用类 8. ThinkPHP/Library/Think/Behavior.class.php // 基础行为类 9. ThinkPHP/Library/Think/Cache.class.php // 核心缓存类 10. ThinkPHP/Library/Think/Controller.class.php // 基础控制器类 11. ThinkPHP/Library/Think/Db.class.php // 数据库操作类 12. ThinkPHP/Library/Think/Dispatcher.class.php // URL解析调度类 13. ThinkPHP/Library/Think/Exception.class.php // 系统基础异常类 14. ThinkPHP/Library/Think/Hook.class.php // 系统钩子类 - 34 - Behavior目录下面是系统内置的一些行为类库,内置驱动则分布在各个不同的驱动目录下面(参考下面的 驱动部分)。 Driver(驱动) 3.2在架构设计上更加强化了驱动的设计,替代了之前的引擎和模式扩展,并且改进了行为的设计,使得 框架整体更加灵活,并且由于在需要写入数据的功能类库中都采用了驱动化的设计思想,所以使得新的框 架能够轻松满足分布式部署的需求,对云平台的支持可以更简单的实现了。因此,在新版的扩展里面,已 经取消了引擎扩展和模式扩展,改成配置不同的应用模式即可。 驱动包括 Behavior(行为) 行为(Behavior)是ThinkPHP扩展机制中比较关键的一项扩展,行为既可以独立调用,也可以绑定到某 个标签(位)中进行侦听。这里的行为指的是一个比较抽象的概念,你可以想象成在应用执行过程中的一 个动作或者处理,在框架的执行流程中,各个位置都可以有行为产生,例如路由检测是一个行为,静态缓 存是一个行为,用户权限检测也是行为,大到业务逻辑,小到浏览器检测、多语言检测等等都可以当做是 一个行为,甚至说你希望给你的网站用户的第一次访问弹出Hello,world!这些都可以看成是一种行 为,行为的存在让你无需改动框架和应用,而在外围通过扩展或者配置来改变或者增加一些功能。 而不同的行为之间也具有位置共同性,比如,有些行为的作用位置都是在应用执行前,有些行为都是在模 板输出之后,我们把这些行为发生作用的位置称之为标签(位),也可以称之为钩子,当应用程序运行到 这个标签的时候,就会被拦截下来,统一执行相关的行为,类似于AOP编程中的“切面”的概念,给某一 15. ThinkPHP/Library/Think/Log.class.php // 系统日志记录类 16. ThinkPHP/Library/Think/Model.class.php // 系统基础模型类 17. ThinkPHP/Library/Think/Route.class.php // 系统路由类 18. ThinkPHP/Library/Think/Storage.class.php // 系统存储类 19. ThinkPHP/Library/Think/Template.class.php // 内置模板引擎类 20. ThinkPHP/Library/Think/Think.class.php // 系统引导类 21. ThinkPHP/Library/Think/View.class.php // 系统视图类 1. ThinkPHP/Library/Think/Cache/Driver // 缓存驱动类库 2. ThinkPHP/Library/Think/Db/Driver // 数据库驱动类库 3. ThinkPHP/Library/Think/Log/Driver // 日志记录驱动类库 4. ThinkPHP/Library/Think/Session/Driver // Session驱动类库 5. ThinkPHP/Library/Think/Storage/Driver // 存储驱动类库 6. ThinkPHP/Library/Think/Template/Driver // 第三方模板引擎驱动类库 7. ThinkPHP/Library/Think/Template/TagLib // 内置模板引擎标签库扩展类库 - 35 - 个标签绑定相关行为就成了一种类AOP编程的思想。 系统标签位 系统核心提供的标签位置包括(按照执行顺序排列): app_init 应用初始化标签位 module_check 模块检测标签位(3.2.1版本新增) path_info PATH_INFO检测标签位 app_begin 应用开始标签位 action_name 操作方法名标签位 action_begin 控制器开始标签位 view_begin 视图输出开始标签位 view_template 视图模板解析标签位 view_parse 视图解析标签位 template_filter 模板解析过滤标签位 view_filter 视图输出过滤标签位 view_end 视图输出结束标签位 action_end 控制器结束标签位 app_end 应用结束标签位 在每个标签位置,可以配置多个行为,行为的执行顺序按照定义的顺序依次执行。除非前面的行为里面中 断执行了(某些行为可能需要中断执行,例如检测机器人或者非法执行行为),否则会继续下一个行为的 执行。 除了这些系统内置标签之外,开发人员还可以在应用中添加自己的应用标签,在任何需要拦截的位置添加 如下代码即可: tag函数用于设置某个标签位,可以传入并且只接受一个参数,如果需要传入多个参数,请使用数组, 该参数为引用传值,所以只能传入变量,因此下面的传值是错误的: 核心行为 系统的很多核心功能也是采用行为扩展组装的,对于满足项目日益纷繁复杂的需求和定制底层框架提供了 更多的方便和可能性。 1. tag('my_tag'); // 添加my_tag 标签侦听 2. // 下面的写法作用一致 3. \Think\Hook::listen('my_tag'); 1. tag('my_tag',$params); // 添加my_tag 标签侦听 1. tag('my_tag','param'); // 添加my_tag 标签侦听 - 36 - 核心行为位于 ThinkPHP/Behavior/ 目录下面,框架核心内置的行为包括如下: 行为名称 说明 对应标签位置 BuildLite 生成Lite文件(3.2.1版本新增) app_init ParseTemplate 模板文件解析,并支持第三方模板引擎驱动 view_parse ShowPageTrace 页面Trace功能行为,完成页面Trace功能 view_end ShowRuntime 运行时间显示行为,完成运行时间显示 view_filter TokenBuild 令牌生成行为,完成表单令牌的自动生成 view_filter ReadHtmlCache 读取静态缓存行为 app_init WriteHtmlCache 生成静态缓存行为 view_filter 行为定义 自定义的扩展行为可以放在核心或者应用目录,只要遵循命名空间的定义规则即可。 行为类的命名采用: 行为名称(驼峰法,首字母大写)+Behavior 行为类的定义方式如下: 3.2.1版本开始,行为类的定义无需继承 Think\Behavior 类,所以,上面的定义可以简化为: 1. namespace Home\Behavior; 2. use Think\Behavior; 3. class TestBehavior extends Behavior { 4. // 行为扩展的执行入口必须是run 5. public function run(&$params){ 6. if(C('TEST_PARAM')) { 7. echo 'RUNTEST BEHAVIOR '.$params; 8. } 9. } 10. } 1. namespace Home\Behavior; 2. class TestBehavior { 3. // 行为扩展的执行入口必须是run 4. public function run(&$params){ 5. if(C('TEST_PARAM')) { 6. echo 'RUNTEST BEHAVIOR '.$params; 7. } - 37 - 行为类必须定义执行入口方法 run ,由于行为的调用机制影响,run方法不需要任何返回值,所有返回都通 过引用返回。 run方法的参数只允许一个,但可以传入数组。 行为绑定 行为定义完成后,就需要绑定到某个标签位置才能生效,否则是不会执行的。 我们需要在应用的行为定义文件 tags.php 文件中进行行为和标签的位置定义,格式如下: 标签名称包括我们前面列出的系统标签和应用中自己定义的标签名称,比如你需要在app_init标签位置定 义一个CheckLangBehavior行为类的话,可以使用: 可以给一个标签位定义多个行为,行为的执行顺序就是定义的先后顺序,例如: 默认情况下tags.php中定义的行为会并入系统行为一起执行,也就是说如果系统的行为定义中app_init标 签中已经定义了其他行为,则会首先执行系统行为扩展中定义的行为,然后再执行项目行为中定义的行 为。例如: 系统行为定义文件中定义了: 8. } 9. } 1. return array( 2. '标签名称1'=>array('行为名1','行为名2',...), 3. '标签名称2'=>array('行为名1','行为名2',...), 4. ); 1. return array( 2. 'app_init'=>array('Home\Behavior\CheckLang'), 3. // **如果是3.2.1版本 需要改成(后面不再重复说明)** 4. // 'app_init'=>array('Home\Behavior\CheckLangBehavior'), 5. ); 1. return array( 2. 'app_init'=>array( 3. 'Home\Behavior\CheckLang', 4. 'Home\Behavior\CronRun' 5. ), 6. ); - 38 - 而应用行为定义文件有定义: 则最终执行到app_begin标签(位)的时候,会依次执行: 三个行为(除非中间某个行为有中止执行的操作)。 如果希望应用的行为配置文件中的定义覆盖系统的行为定义,可以改为为如下方式: 则最终执行到app_begin标签(位)的时候,会依次执行下面两个行为: 应用行为的定义没有限制,你可以把一个行为绑定到多个标签位置执行,例如: 单独执行 行为的调用不一定要放到标签才能调用,如果需要的话,我们可以在控制器中或者其他地方直接调用行 为。例如,我们可以把用户权限检测封装成一个行为类,例如: 1. 'app_begin' => array( 2. 'Behavior\ReadHtmlCache', // 读取静态缓存 3. ), 1. 'app_begin' => array( 2. 'Home\Behavior\CheckModule', 3. 'Home\Behavior\CheckLang', 4. ), 1. Library\Behavior\ReadHtmlCache 2. Home\Behavior\CheckModule 3. Home\Behavior\CheckLang 1. 'app_begin' => array( 2. 'Home\Behavior\CheckModule', 3. 'Home\Behavior\CheckLang', 4. '_overlay' => true, 5. ), 1. Home\Behavior\CheckModule 2. Home\Behavior\CheckLang 1. return array( 2. 'app_begin'=>array('Home\Behavior\Test'), // 在app_begin 标签位添加Test行为 3. 'app_end'=>array('Home\Behavior\Test'), // 在app_end 标签位添加Test行为 4. ); - 39 - 命名空间 定义了AuthCheck行为后,我们可以在控制器的_initialize方法中直接用下面的方式调用: 3.2版本全面采用命名空间方式定义和加载类库文件,有效的解决多个模块之间的冲突问题,并且实现了 更加高效的类库自动加载机制。 命名空间的概念必须了解,否则会成为3.2版本开发的重大障碍。 如果不清楚什么是命名空间,可以参考PHP手册:PHP命名空间 由于新版完全采用了命名空间的特性,因此只需要给类库正确定义所在的命名空间,而命名空间的路径与 类库文件的目录一致,那么就可以实现类的自动加载。 例如, Org\Util\File 类的定义为: 其所在的路径是 ThinkPHP/Library/Org/Util/File.class.php ,因此,如果我们实例化该类的话: 系统会自动加载 ThinkPHP/Library/Org/Util/File.class.php 文件。 1. namespace Home\Behavior; 2. use Think\Behavior; 3. class AuthCheckBehavior extends Behavior { 4. 5. // 行为扩展的执行入口必须是run 6. public function run(&$return){ 7. if(C('USER_AUTH_ON')) { 8. // 进行权限认证逻辑 如果认证通过 $return = true; 9. // 否则用halt输出错误信息 10. } 11. } 12. } 1. B('Home\Behavior\AuthCheck'); 2. // 3.2.1版本中需要改成 3. B('Home\Behavior\AuthCheckBehavior'); 1. namespace Org\Util; 2. class File { 3. } 1. $class = new \Org\Util\File(); - 40 - 注意:和3.1不同,我们无需在实例化命名空间定义的类之前导入类库文件了。 根命名空间 根命名空间是一个关键的概念,以上面的 Org\Util\File 类为例, Org 就是一个根命名空间,其对应的 初始命名空间目录就是系统的类库目录( ThinkPHP/Library ),Library目录下面的子目录会自动识别 为根命名空间,这些命名空间无需注册即可使用。 例如,我们在Library目录下面新增一个My根命名空间目录,然后定义一个Test类如下: Test类保存在 ThinkPHP/Library/My/Test.class.php ,我们就可以直接实例化和调用: 模块中的类库命名空间的根都是以模块名命名,例如: 其类文件位于 Application/Home/Model/UserModel.class.php 。 其类文件位于 Application/Admin/Event/UserEvent.class.php 。 3.2.1版本以上的话,允许设置对应用类库不使用命名空间,你在配置文件中进行如下设置: 那么,所有的应用类库不再需要使用命名空间定义,但继承和调用核心类和系统类的时候,仍然需要使用 命名空间,例如: 1. namespace My; 2. class Test { 3. public function sayHello(){ 4. echo 'hello'; 5. } 6. } 1. $Test = new \My\Test(); 2. $Test->sayHello(); 1. namespace Home\Model; 2. class UserModel extends \Think\Model { 3. } 1. namespace Admin\Event; 2. class UserEvent { 3. } 1. 'APP_USE_NAMESPACE' => false, - 41 - 自动加载 特别注意:如果你需要在3.2版本中实例化PHP内置的类库或者第三方的没有使用命名空间定义的 类,需要采用下面的方式: 在3.2中,基本上无需手动加载类库文件,你可以很方便的完成自动加载。 命名空间自动加载 系统可以通过类的命名空间自动定位到类库文件,例如: 我们定义了一个类 Org\Util\Auth 类: 保存到 ThinkPHP/Library/Org/Util/Auth.class.php 。 接下来,我们就可以直接实例化了。 在实例化 Org\Util\Auth 类的时候,系统会自动加载 ThinkPHP/Library/Org/Util/Auth.class.php 文件。 框架的Library目录下面的命名空间都可以自动识别和定位,例如: Library目录下面的子目录都是一个根命名空间,也就是说以Think、Org为根命名空间的类都可以自动加 载: 1. class UserModel extends \Think\Model { 2. } 1. $class = new \stdClass(); 2. $sxml = new \SimpleXmlElement($xmlstr); 1. namespace Org\Util; 2. class Auth { 3. } 1. new \Org\Util\Auth(); 1. ├─Library 框架类库目录 2. │ ├─Think 核心Think类库包目录 3. │ ├─Org Org类库包目录 4. │ ├─ ... 更多类库目录 - 42 - 都可以自动加载对应的类库文件。 你可以在Library目录下面任意增加新的目录,就会自动注册成为一个新的根命名空间。 注册新的命名空间 除了Library目录下面的命名空间之外,我们还可以注册其他的根命名空间,例如: 配置了上面的 AUTOLOAD_NAMESPACE 后,如果我们实例化下面的类库 会自动加载对应的类库文件 如果命名空间不在Library目录下面,并且没有定义对应的 AUTOLOAD_NAMESPACE 参数的话,则会当作模 块的命名空间进行自动加载,例如: 由于ThinkPHP/Library目录下面不存在Home目录,也没在 AUTOLOAD_NAMESPACE 参数定义Home命名 空间,所以就把Home当成模块命名空间来识别,所以会自动加载: 注意:命名空间的大小写需要和目录名的大小写对应,否则可能会自动加载失败。 1. new Think\Cache\Driver\File(); 2. new Org\Util\Auth(); 3. new Org\Io\File(); 1. 'AUTOLOAD_NAMESPACE' => array( 2. 'My' => THINK_PATH.'My', 3. 'One' => THINK_PATH.'One', 4. ) 1. new My\Net\IpLocation(); 2. new One\Util\Log(); 1. ThinkPHP/My/Net/IpLocation.class.php 2. ThinkPHP/One/Util/Log.class.php 1. new Home\Model\UserModel(); 2. new Home\Event\UserEvent(); 1. Application/Home/Model/UserModel.class.php 2. Application/Home/Event/UserEvent.class.php - 43 - 类库映射 遵循我们上面的命名空间定义规范的话,基本上可以完成类库的自动加载了,但是如果定义了较多的命名 空间的话,效率会有所下降,所以,我们可以给常用的类库定义类库映射。命名类库映射相当于给类文件 定义了一个别名,效率会比命名空间定位更高效,例如: 也可以利用addMap方法批量导入类库映射定义,例如: 当然,比较方便的方式是我们可以在模块配置目录下面创建alias.php文件用于定义类库映射,该文件会自 动加载,定义方式如下: 自动加载的优先级 在实际的应用类库加载过程中,往往会涉及到自动加载的优先级问题,以 Test\MyClass 类为例,自动加 载的优先顺序如下: 1. 判断是否有注册了Test\MyClass类库映射,如果有则自动加载类库映射定义的文件; 2. 判断是否存在Library/Test目录,有则以该目录为初始目录加载; 3. 判断是否有注册Test根命名空间,有则以注册的目录为初始目录加载; 4. 如果以上都不成立,则以Test为模块目录进行初始目录加载; 以上面获取到的初始目录加载命名空间对应路径的文件; 手动加载第三方类库 如果要加载第三方类库,包括不符合命名规范和后缀的类库,以及没有使用命名空间或者命名空间和路径 不一致的类库,或者你就是想手动加载类库文件,我们都可以通过手动导入的方式加载。 我们可以使用import方法导入任何类库,用法如下: 1. Think\Think::addMap('Think\Log',THINK_PATH.'Think\Log.php'); 2. Think\Think::addMap('Org\Util\Array',THINK_PATH.'Org\Util\Array.php'); 1. $map = array('Think\Log'=>THINK_PATH.'Think\Log.php','Org\Util\Array'=>THINK_PAT H.'Org\Util\Array.php'); 2. Think\Think::addMap($map); 1. return array( 2. 'Think\Log' => THINK_PATH.'Think\Log.php', 3. 'Org\Util\Array' => THINK_PATH.'Org\Util\Array.php' 4. ); 1. // 导入Org类库包 Library/Org/Util/Date.class.php类库 2. import("Org.Util.Date"); - 44 - 对于import方法,系统会自动识别导入类库文件的位置,ThinkPHP可以自动识别的类库包包括Think、 Org、Com、Behavior和Vendor包,以及Library目录下面的子目录,如果你在Library目录下面创建了 一个Test子目录,并且创建了一个UserTest.class.php类库,那么可以这样导入: 其他的就认为是应用类库导入。 注意,如果你的类库没有使用命名空间定义的话,实例化的时候需要加上根命名空间,例如: 按照系统的规则,import方法是无法导入具有点号的类库文件的,因为点号会直接转化成斜线,例如我们 定义了一个名称为User.Info.class.php 的文件的话,采用: 方式加载的话就会出现错误,导致加载的文件不是Org/User.Info.class.php 文件,而是 Org/User/Info.class.php 文件,这种情况下,我们可以使用: 来导入。 大多数情况下,import方法都能够自动识别导入类库文件的位置,如果是特殊情况的导入,需要指定 import方法的第二个参数作为起始导入路径。例如,要导入当前文件所在目录下面的 RBAC/AccessDecisionManager.class.php 文件,可以使用: 如果你要导入的类库文件名的后缀不是class.php而是php,那么可以使用import方法的第三个参数指定 后缀: 3. // 导入Home模块下面的 Application/Home/Util/UserUtil.class.php类库 4. import("Home.Util.UserUtil"); 5. // 导入当前模块下面的类库 6. import("@.Util.Array"); 7. // 导入Vendor类库包 Library/Vendor/Zend/Server.class.php 8. import('Vendor.Zend.Server'); 1. import('Test.UserTest'); 1. import('Test.UserTest'); 2. $test = new \UserTest(); 1. import("Org.User.Info"); 1. import("Org.User#Info"); 1. import("RBAC.AccessDecisionManager",dirname(__FILE__)); 1. import("RBAC.AccessDecisionManager",dirname(__FILE__),".php"); - 45 - 应用模式 注意:在Unix或者Linux主机下面是区别大小写的,所以在使用import方法的时候要注意目录名和 类库名称的大小写,否则会导入失败。 如果你的第三方类库都放在Vendor目录下面,并且都以.php为类文件后缀,也没用采用命名空间的话, 那么可以使用系统内置的Vendor函数简化导入。 例如,我们把 Zend 的 Filter\Dir.php 放到 Vendor 目 录下面,这个时候 Dir 文件的路径就是 Vendor\Zend\Filter\Dir.php,我们使用vendor 方法导入只需要 使用: 就可以导入Dir类库了。 Vendor方法也可以支持和import方法一样的基础路径和文件名后缀参数,例如: 3.2版本的应用模式可以替代之前的引擎扩展和模式扩展,支持应用模式定义。 每个应用模式有自己的定义文件,用于配置当前模式需要加载的核心文件和配置文件,以及别名定义、行 为扩展定义等等。除了模式定义外,应用自身也可以独立定义模式文件。 如果应用模式涉及到不同的存储类型,例如采用分布式存储等,就需要另外设置存储类型 (STORAGE_TYPE)。不同的存储类型由Think\Storage类及相关驱动进行支持。 默认情况下的应用模式是普通模式(common),如果要采用其他的应用模式(当然,前提是已经有定 义),必须在入口文件中定义,设置 APP_MODE 常量即可,例如: 应用模式的一个典型应用是对分布式平台的支持,对不同的平台定义不同的应用模式就可以支持。 每个入口文件仅能定义一个应用模式,所以,如果需要对相同的应用模块设置不同的应用模式访 问,就可以通过增加入口文件的方式来解决。 1. Vendor('Zend.Filter.Dir'); 1. Vendor('Zend.Filter.Dir',dirname(__FILE__),'.class.php'); 1. // 定义存储类型和应用模式为SAE(用于支持SAE平台) 2. define('STORAGE_TYPE','sae'); 3. define('APP_MODE','sae'); 4. define('APP_PATH','./Application/'); 5. require './ThinkPHP/ThinkPHP.php'; - 46 - 项目编译 每个应用模式可以定义单独的配置文件,一般是 config_模式名称 ,例如,sae模式下面可以定义: 或者 config_sae配置文件只会sae模式下面加载,如果不是sae模式则不会加载。 应用编译机制作为ThinkPHP独创的功能特色,从1.0版本就延续至今,3.2版本的编译机制更加具有特 色。 应用编译缓存 编译缓存的基础原理是第一次运行的时候把核心需要加载的文件去掉空白和注释后合并到一个文件中,第 二次运行的时候就直接载入编译缓存而无需载入众多的核心文件。当第二次执行的时候就会根据当前的应 用模式直接载入编译过的缓存文件,从而省去很多IO开销,加快执行速度。 项目编译机制对运行没有任何影响,预编译机制只会执行一次,因此无论在预编译过程中做了多少复杂的 操作,对后面的执行没有任何效率的缺失。 编译缓存文件默认生成在应用目录的Runtime目录下面,我们可以在Application/Runtime目录下面看到 有一个 common~runtime.php 文件,这个就是普通模式的编译缓存文件。如果你当前运行在其他的应用 模式下面,那么编译缓存文件就是: 应用模式~runtime.php 例如,如果你当前用的是SAE模式,那么生成的编译缓存文件则会变成 sae~runtime.php 。 普通模式的编译缓存的内容包括:系统函数库、系统基础核心类库、核心行为类库、项目函数文件,当然 这些是可以改变的。 运行Lite文件 这是3.2.1版本的新特性。 运行Lite文件的作用是替换框架的入口文件或者替换应用入口文件,提高运行 效率。因为默认生成的文件名为lite.php,并且是运行时动态生成,因此称之为运行Lite文件。 Lite文件的特点包括: 1. // 应用配置文件 2. Application/Common/Conf/config_sae.php 1. // 模块配置文件 2. Application/Home/Conf/config_sae.php - 47 - 系统流程 运行时动态生成; 常量定义为针对当前环境; 支持定义需要编译的文件列表; 支持生成Lite文件的名称; 如何生成Lite文件,请参考部署部分的替换入口。 我们来系统的了解下ThinkPHP框架开发的应用的标准执行流程: 1. 用户URL请求 2. 调用应用入口文件(通常是网站的index.php) 3. 载入框架入口文件(ThinkPHP.php) 4. 记录初始运行时间和内存开销 5. 系统常量判断及定义 6. 载入框架引导类(Think\Think)并执行Think::start方法进行应用初始化 7. 设置错误处理机制和自动加载机制 8. 调用Think\Storage类进行存储初始化(由STORAGE_TYPE常量定义存储类型) 9. 部署模式下如果存在应用编译缓存文件则直接加载(直接跳转到步骤22) 10. 读取应用模式(由APP_MODE常量定义)的定义文件(以下以普通模式为例说明) 11. 加载当前应用模式定义的核心文件(普通模式是 ThinkPHP/Mode/common.php) 12. 加载惯例配置文件(普通模式是 ThinkPHP/Conf/convention.php) 13. 加载应用配置文件(普通模式是 Application/Common/Conf/config.php) 14. 加载系统别名定义 15. 判断并读取应用别名定义文件(普通模式是 Application/Common/Conf/alias.php) 16. 加载系统行为定义 17. 判断并读取应用行为定义文件(普通模式是 Application/Common/Conf/tags.php) 18. 加载框架底层语言包(普通模式是 ThinkPHP/Lang/zh-cn.php) 19. 如果是部署模式则生成应用编译缓存文件 20. 加载调试模式系统配置文件(ThinkPHP/Conf/debug.php) 21. 判断并读取应用的调试配置文件(默认是 Application/Common/Conf/debug.php) 22. 判断应用状态并读取状态配置文件(如果APP_STATUS常量定义不为空的话) 23. 检测应用目录结构并自动生成(如果CHECK_APP_DIR配置开启并且RUNTIME_PATH目录不存在的情 况下) 24. 调用Think\App类的run方法启动应用 25. 应用初始化(app_init)标签位侦听并执行绑定行为 26. 判断并加载动态配置和函数文件 - 48 - 27. 调用Think\Dispatcher::dispatch方法进行URL请求调度 28. 自动识别兼容URL模式和命令行模式下面的$_SERVER['PATH_INFO']参数 29. 检测域名部署以及完成模块和控制器的绑定操作(APP_SUB_DOMAIN_DEPLOY参数开启) 30. 分析URL地址中的PATH_INFO信息 31. 获取请求的模块信息 32. 检测模块是否存在和允许访问 33. 判断并加载模块配置文件、别名定义、行为定义及函数文件 34. 判断并加载模块的动态配置和函数文件 35. 模块的URL模式判断 36. 模块的路由检测(URL_ROUTER_ON开启) 37. PATH_INFO处理(path_info)标签位侦听并执行绑定行为 38. URL后缀检测(URL_DENY_SUFFIX以及URL_HTML_SUFFIX处理) 39. 获取当前控制器和操作,以及URL其他参数 40. URL请求调度完成(url_dispatch)标签位侦听并执行绑定行为 41. 应用开始(app_begin)标签位侦听并执行绑定行为 42. 调用SESSION_OPTIONS配置参数进行Session初始化(如果不是命令行模式) 43. 根据请求执行控制器方法 44. 如果控制器不存在则检测空控制器是否存在 45. 控制器开始(action_begin)标签位侦听并执行绑定行为 46. 默认调用系统的ReadHtmlCache行为读取静态缓存(HTML_CACHE_ON参数开启) 47. 判断并调用控制器的_initialize初始化方法 48. 判断操作方法是否存在,如果不存在则检测是否定义空操作方法 49. 判断前置操作方法是否定义,有的话执行 50. Action参数绑定检测,自动匹配操作方法的参数 51. 如果有模版渲染(调用控制器display方法) 52. 视图开始(view_begin)标签位侦听并执行绑定行为 53. 调用Think\View的fetch方法解析并获取模版内容 54. 自动识别当前主题以及定位模版文件 55. 视图解析(view_parse)标签位侦听并执行绑定行为 56. 默认调用内置ParseTemplate行为解析模版(普通模式下面) 57. 模版引擎解析模版内容后生成模版缓存 58. 模版过滤替换(template_filter)标签位侦听并执行绑定行为 59. 默认调用系统的ContentReplace行为进行模版替换 60. 输出内容过滤(view_filter)标签位侦听并执行绑定行为 61. 默认调用系统的WriteHtmlCache行为写入静态缓存(HTML_CACHE_ON参数开启) 62. 调用Think\View类的render方法输出渲染内容 - 49 - 如果你绑定了更多的应用行为的话,流程可能会更加复杂。 如果是部署模式下面的第二次请求的话,上面的流程中的步骤10~21是可以省略的。 63. 视图结束(view_end)标签位侦听并执行绑定行为 64. 判断后置操作方法是否定义,有的话执行 65. 控制器结束(action_end)标签位侦听并执行绑定行为 66. 应用结束(app_end)标签位侦听并执行绑定行为 67. 执行系统的ShowPageTrace行为(SHOW_PAGE_TRACE参数开启并且不是AJAX请求) 68. 日志信息存储写入 - 50 - 路由 路由定义 利用路由功能,可以让你的URL地址更加简洁和优雅。ThinkPHP支持对模块的URL地址进行路由操作。 路由功能是针对PATHINFO模式或者兼容URL而设计的,暂时不支持普通URL模式。 ThinkPHP的路由功能包括: 正则路由 规则路由 静态路由(URL映射) 闭包支持 由于ThinkPHP3.2采用的URL规则是: http://serverName/module/controller/action/param/value/... 路由解析的最终结果通常是把URL地址解析到当前模块的某个控制器下的操作方法(不能跨模块路由), 在特殊的情况下,也可以跳转到外部地址或者执行闭包函数。 注意:3.2版本的路由定义是针对模块定义的,所以路由是在模块配置文件中定义的,并且模块本身 不能被路由(模块名的路由使用模块映射或者采用域名部署即可)。 启用路由 要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情 况下不支持路由功能),并且在应用(或者模块)配置文件中开启路由: 3.2的路由功能是针对模块设置的,所以URL中的模块名不能被路由,路由定义也通常是放在模块配 置文件中。 然后就是配置路由规则了,在模块的配置文件中使用URL_ROUTE_RULES参数进行配置,配置格式是一 个数组,每个元素都代表一个路由规则,例如: 1. // 开启路由 2. 'URL_ROUTER_ON' => true, 1. 'URL_ROUTE_RULES'=>array( - 51 - 系统会按定义的顺序依次匹配路由规则,一旦匹配到的话,就会定位到路由定义中的控制器和操作方法去 执行(可以传入其他的参数),并且后面的规则不会继续匹配。 路由定义 路由规则的定义格式为: '路由表达式'=>'路由地址和传入参数' 或者:array('路由表达式','路由地址','传入参数') 路由表达式 路由表达式包括规则路由和正则路由的定义表达式,只能使用字符串。 表达式 示例 正则表达式 /^blog\/(\d+)$/ 规则表达式 blog/:id 详细的规则路由和正则路由表达式的定义方法参考后面的章节。 路由地址 路由地址(可以支持传入额外参数)表示前面的路由表达式需要路由到的地址(包括内部地址和外部地 址),并且允许隐式传入URL里面没有的一些参数,这里允许使用字符串或者数组方式定义,特殊情况下 还可以采用闭包函数定义路由功能,支持下面6种方式定义: 定义方式 定义格式 方式1:路由到内部地址(字符串) '[分组/模块/操作]?额外参数1=值1&额外参数2=值2...' 方式2:路由到内部地址(数组)参数 采用字符串方式 array('[分组/模块/操作]','额外参数1=值1&额外参数2=值2...') 方式3:路由到内部地址(数组)参数 采用数组方式 array('[分组/模块/操作]',array('额外参数1'=>'值1','额外参数 2'=>'值2'...)[,路由参数]) 方式4:路由到外部地址(字符 串)301重定向 '外部地址' 方式5:路由到外部地址(数组)可以 2. 'news/:year/:month/:day' => array('News/archive', 'status=1'), 3. 'news/:id' => 'News/read', 4. 'news/read/:id' => '/news/:1', 5. ), - 52 - 方式5:路由到外部地址(数组)可以 指定重定向代码 array('外部地址','重定向代码'[,路由参数]) 方式6:闭包函数 function($name){ echo 'Hello,'.$name;} 如果路由地址以“/”或者“http”开头则会认为是一个重定向地址或者外部地址,例如: 和 虽然都是路由到同一个地址,但是前者采用的是301重定向的方式路由跳转,这种方式的好处是URL可以 比较随意(包括可以在URL里面传入更多的非标准格式的参数),而后者只是支持模块和操作地址。 举个例子,如果我们希望 avatar/123 重定向到 /member/avatar/id/123_small 的话,只能使用: 路由地址采用重定向地址的话,如果要引用动态变量,也是采用 :1、:2 的方式。 采用重定向到外部地址通常对网站改版后的URL迁移过程非常有用,例如: 表示当前网站(可能是http://thinkphp.cn)的 blog/123 地址会直接重定向到 http://blog.thinkphp.cn/read/123 。 默认情况下,外部地址的重定向采用301重定向,如果希望采用其它的,可以使用: 在路由跳转的时候支持额外传入参数对(额外参数指的是不在URL里面的参数,隐式传入需要的操作中, 有时候能够起到一定的安全防护作用,后面我们会提到),支持 额外参数1=值1&额外参数2=值2 或者 array('额外参数1'=>'值1','额外参数2'=>'值2'...) 这样的写法,可以参考不同的定义方式选择。 例如: 上面的路由规则定义中额外参数的传值方式都是等效的。 status 和 app_id 参数都是URL里面不存在 的,属于隐式传值,当然并不一定需要用到,只是在需要的时候可以使用。 1. 'blog/:id'=>'/blog/read/id/:1' 1. 'blog/:id'=>'blog/read' 1. 'avatar/:id'=>'/member/avatar/id/:1_small' 1. 'blog/:id'=>'http://blog.thinkphp.cn/read/:1' 1. 'blog/:id'=>array('http://blog.thinkphp.cn/read/:1',302); 1. 'blog/:id'=>'blog/read?status=1&app_id=5', 2. 'blog/:id'=>array('blog/read?status=1&app_id=5'), 3. 'blog/:id'=>array('blog/read','status=1&app_id=5'), 4. 'blog/:id'=>array('blog/read',array('status'=>1,'app_id'=>5)), - 53 - 规则路由 路由参数 当路由地址采用数组方式定义的时候,还可以传入额外的路由参数。 这些参数的作用是限制前面定义的路由规则的生效条件。 限制URL后缀 例如: 就可以限制html后缀访问该路由规则才能生效。 限制请求类型 例如: 就限制了只有GET请求该路由规则才能生效。 自定义检测 支持自定义检测,例如: 例如: 就可以自定义定义checkFun函数来检测是否生效,如果函数返回false则表示不生效。 规则路由是一种比较容易理解的路由定义方式,采用ThinkPHP设计的规则表达式来定义。 规则表达式 规则表达式通常包含静态地址和动态地址,或者两种地址的结合,例如下面都属于有效的规则表达式: 规则表达式的定义始终以“/”为参数分割符,不受 URL_PATHINFO_DEPR 设置的影响 1. 'blog/:id'=>array('blog/read','status=1&app_id=5',array('ext'=>'html')), 1. 'blog/:id'=>array('blog/read','status=1&app_id=5',array('method'=>'get')), 1. 'blog/:id'=>array('blog/read','status=1&app_id=5',array('callback'=>'checkFun')) , 1. 'my' => 'Member/myinfo', // 静态地址路由 2. 'blog/:id' => 'Blog/read', // 静态地址和动态地址结合 3. 'new/:year/:month/:day'=>'News/read', // 静态地址和动态地址结合 4. ':user/:blog_id' =>'Blog/read',// 全动态地址 - 54 - 每个参数中以“:”开头的参数都表示动态参数,并且会自动对应一个GET参数,例如 :id 表示该处匹配 到的参数可以使用 $_GET['id'] 方式获取, :year 、 :month 、 :day 则分别对 应 $_GET['year'] 、 $_GET['month'] 和 $_GET['day'] 。 数字约束 支持对变量的类型检测,但仅仅支持数字类型的约束定义,例如 表示只会匹配数字参数,如果你需要更加多的变量类型检测,请使用正则表达式定义来解决。 目前不支持长度约束,需要的话采用正则定义解决 函数支持 可以支持对路由变量的函数过滤,例如: 表示对匹配到的id变量进行md5处理,也就是说,实际传入read操作方法的 $_GET['id'] 其实是 md5($_GET['id']) 。 注意:不支持对变量使用多次函数处理和函数额外参数传入。 可选定义 支持对路由参数的可选定义,例如: [:month\d] 变量用[ ]包含起来后就表示该变量是路由匹配的可选变量。 以上定义路由规则后,下面的URL访问地址都可以被正确的路由匹配: 采用可选变量定义后,之前需要定义两个或者多个路由规则才能处理的情况可以合并为一个路由规则。 可选参数只能放到路由规则的最后,如果在中间使用了可选参数的话,后面的变量都会变成可选参 数。 1. 'blog/:id\d'=>'Blog/read', 1. 'blog/:id\d|md5'=>'Blog/read', 1. 'blog/:year\d/[:month\d]'=>'Blog/archive', 1. http://serverName/index.php/Home/blog/2013 2. http://serverName/index.php/Home/blog/2013/12 - 55 - 规则排除 非数字变量支持简单的排除功能,主要是起到避免解析混淆的作用,例如: 3.2.2版本开始,为了避免和函数规则冲突,规则路由排除分隔符改为“-”,所以上面的路由定义需 要改为: 'news/:cate^add-edit-delete'=>'News/category' 因为规则定义的局限性,恰巧我们的路由规则里面的news和实际的news模块是相同的命名, 而 :cate 并不能自动区分当前URL里面的动态参数是实际的操作名还是路由变量,所以为了避免混淆, 我们需要对路由变量cate进行一些排除以帮助我们进行更精确的路由匹配,格式 ^add|edit|delete 表 示,匹配除了add edit 和delete之外的所有字符串,我们建议更好的方式还是改进你的路由规则,避免 路由规则和模块同名的情况存在,例如 就可以更简单的定义路由规则了。 完全匹配 规则匹配检测的时候只是对URL从头开始匹配,只要URL地址包含了定义的路由规则就会匹配成功,如果 希望完全匹配,可以使用$符号,例如: http://serverName/index.php/Home/new/info 会匹配成功,而 http://serverName/index.php/Home/new/info/2 则不会匹配成功。 如果是采用 方式定义的话,则两种方式的URL访问都可以匹配成功。 完全匹配的路由规则中如果使用可选参数的话将会无效。 1. 'news/:cate^add|edit|delete'=>'News/category' 1. 'new/:cate'=>'News/category' 1. 'new/:cate$'=> 'News/category' 1. 'new/:cate'=> 'News/category' - 56 - 正则路由 静态路由 正则路由也就是采用正则表达式定义路由的一种方式,依靠强大的正则表达式,能够定义更灵活的路由规 则。 路由表达式支持的正则定义必须以“/”开头,否则就视为规则表达式。也就是说如果采用: 方式定义的正则表达式不会被支持,而会被认为是规则表达式进行解析,从而无法正确匹配。 下面是一种正确的正则路由定义: 对于正则表达式中的每个变量(即正则规则中的子模式)部分,如果需要在后面的路由地址中引 用,可以采用:1、:2这样的方式,序号就是子模式的序号。 正则定义也支持函数过滤处理,例如: 其中 year=:1|format_year 就表示对匹配到的变量进行format_year函数处理(假设format_year是一个 用户自定义函数)。 更多的关于如何定义正则表达式就不在本文的描述范畴了。 静态路由其实属于规则路由的静态简化版(又称为URL映射),路由定义中不包含动态参数,静态路由不 需要遍历路由规则而是直接定位,因此效率较高,但作用也有限。 如果我们定义了下面的静态路由 注意:为了不影响动态路由的遍历效率,静态路由采用URL_MAP_RULES定义和动态路由区分开来 1. '#^blog\/(\d+)$#' => 'Blog/read/id/:1' 1. '/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1&month=:2', 1. '/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1|format_year&month=:2', 1. 'URL_ROUTER_ON' => true, 2. 'URL_MAP_RULES'=>array( 3. 'new/top' => 'news/index/type/top' 4. ) - 57 - 闭包支持 定义之后,如果我们访问: http://serverName/Home/new/top 其实是访问: http://serverName/Home/news/index/type/top 静态路由是完整匹配,所以如果访问: http://serverName/Home/new/top/var/test 尽管前面也有 new/top ,但并不会被匹配到 news/index/type/top 。 如果当前URL地址采用了伪静态支持的话,静态路由的定义需要包含伪静态后缀才能生效,例如: 静态路由的路由地址 只支持字符串,格式: [控制器/操作?]参数1=值1&参数2=值2 闭包定义 我们可以使用闭包的方式定义一些特殊需求的路由,而不需要执行控制器的操作方法了,例如: 参数传递 闭包定义的参数传递在规则路由和正则路由的两种情况下有所区别。 规则路由 规则路由的参数传递比较简单: 1. 'URL_ROUTER_ON' => true, 2. 'URL_MAP_RULES'=>array( 3. 'new/top.html' => 'news/index?type=top' 4. ) 1. 'URL_ROUTE_RULES'=>array( 2. 'test' => 3. function(){ 4. echo 'just test'; 5. }, 6. 'hello/:name' => 7. function($name){ 8. echo 'Hello,'.$name; 9. } 10. ) - 58 - 规则路由中定义的动态变量的名称 就是闭包函数中的参数名称,不分次序。 因此,如果我们访问的URL 地址是: http://serverName/Home/hello/thinkphp 则浏览器输出的结果是: Hello,thinkphp 如果多个参数可以使用: 正则路由 如果是正则路由的话,闭包函数中的参数就以正则中出现的参数次序来传递,例如: 如果我们访问: http://serverName/Home/new/2013/03 浏览器输出结果是: year=2013&month=03 继续执行 默认的情况下,使用闭包定义路由的话,一旦匹配到路由规则,执行完闭包方法之后,就会中止后续执 行。如果希望闭包函数执行后,后续的程序继续执行,可以在闭包函数中使用布尔类型的返回值,例如: 该路由定义中的闭包函数首先执行了一段输出代码,然后重新设置了 $_SERVER['PATH_INFO'] 变量,交 给后续的程序继续执行,因为返回值是false,所以会继续执行控制器和操作的检测,从而会执行Blog控 制器的read操作方法。 1. 'hello/:name' => 2. function($name){ 3. echo 'Hello,'.$name; 4. } 1. 'blog/:year/:month' => 2. function($year,$month){ 3. echo 'year='.$year.'&month='.$month; 4. } 1. '/^new\/(\d{4})\/(\d{2})$/' => 2. function($year,$month){ 3. echo 'year='.$year.'&month='.$month; 4. } 1. 'hello/:name' => 2. function($name){ 3. echo 'Hello,'.$name.'
'; 4. $_SERVER['PATH_INFO'] = 'blog/read/name/'.$name; 5. return false; 6. } - 59 - 实例说明 假设blog控制器中的read操作方法代码如下: 如果我们访问的URL地址是: http://serverName/Home/hello/thinkphp 则浏览器输出的结果是: 我们已经了解了如何定义路由规则,下面我们来举个例子加深印象。 假设我们定义了News控制器如下(代码实现仅供参考): 1. public function read($name){ 2. echo 'read,'.$name.'!
'; 3. } 1. Hello,thinkphp 2. read,thinkphp! 1. namespace Home\Controller; 2. use Think\Controller; 3. class NewsController extends Controller{ 4. public function read(){ 5. $New = M('New'); 6. if(isset($_GET['id'])) { 7. // 根据id查询结果 8. $data = $New->find($_GET['id']); 9. }elseif(isset($_GET['name'])){ 10. // 根据name查询结果 11. $data = $New->getByName($_GET['name']); 12. } 13. $this->data = $data; 14. $this->display(); 15. } 16. 17. public function archive(){ 18. $New = M('New'); 19. $year = $_GET['year']; - 60 - 定义路由规则如下: 然后,我们访问: http://serverName/index.php/Home/new/8 会匹配到第一个路由规则,实际执行的效果等效于访问: http://serverName/index.php/Home/News/read/id/8 当访问: http://serverName/index.php/Home/new/hello 会匹配到第二个路由规则,实际执行的效果等效于访问: http://serverName/index.php/Home/News/read/name/hello 那么如果访问: http://serverName/index.php/Home/new/2012/03 是否会匹配第三个路由规则呢?我们期望的实际执行的效果能够等效于访问: http://serverName/index.php/Home/News/archive/year/2012/month/03 事实上却没有,因为 http://serverName/index.php/Home/new/2012/ 这个URL在进行路由匹配过程中 已经优先匹配到了第一个路由规则了,把2012当成id的值传入了,这种情况属于路由规则的冲突,解决办 法有两个: 1、调整定义顺序 20. $month = $_GET['month']; 21. $begin_time = strtotime($year . $month . "01"); 22. $end_time = strtotime("+1 month", $begin_time); 23. $map['create_time'] = array(array('gt',$begin_time),array('lt',$end_tim e)); 24. $map['status'] = 1; 25. $list = $New->where($map)->select(); 26. $this->list = $list; 27. $this->display(); 28. } 29. } 1. 'URL_ROUTER_ON' => true, //开启路由 2. 'URL_ROUTE_RULES' => array( //定义路由规则 3. 'new/:id\d' => 'News/read', 4. 'new/:name' => 'News/read', 5. 'new/:year\d/:month\d' => 'News/archive', 6. ), - 61 - 路由定义改成: 接下来,当我们再次访问: http://serverName/index.php/Home/new/2012/03 的时候,达到了预期的访问效果。所以如果存在可能规则冲突的情况,尽量把规则复杂的规则定义放到前 面,确保最复杂的规则可以优先匹配到。但是如果路由规则定义多了之后,仍然很容易混淆,所以需要寻 找更好的解决办法。 2、利用完全匹配功能 现在我们来利用路由的完全匹配定义功能,把路由定义改成: 在规则最后加上$符号之后,表示完整匹配当前的路由规则,就可以避免规则定义的冲突了。对于规则路 由来说,简单的理解就是URL里面的参数数量或者类型约束要完全一致。 所以,如果我们访问 http://serverName/index.php/Home/new/2012/03/01 的话,是不会匹配成功任何一条路由的。 3、利用正则路由 当然,解决问题的办法总是不止一种,对于复杂的情况,我们不要忘了使用正则路由规则定义,在你找不 到解决方案的时候,正则路由总能帮到你。 要实现上面的同样路由功能的话,还可以用下面的规则定义: 1. 'URL_ROUTE_RULES' => array( //定义路由规则 2. 'new/:year\d/:month\d' => 'News/archive', 3. 'new/:id\d' => 'News/read', 4. 'new/:name' => 'News/read', 5. ), 1. 'URL_ROUTE_RULES' => array( //定义路由规则 2. 'new/:id\d$' => 'News/read', 3. 'new/:name$' => 'News/read', 4. 'new/:year\d/:month\d$' => 'News/archive', 5. ), 1. 'URL_ROUTE_RULES' => array( //定义路由规则 2. '/^new\/(\d+)$/' => 'News/read?id=:1', 3. '/^new\/(\w+)$/' => 'News/read?name=:1', 4. '/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1&month=:2', 5. ), - 62 - 控制器 控制器定义 控制器和操作 一般来说,ThinkPHP的控制器是一个类,而操作则是控制器类的一个公共方法。 下面就是一个典型的控制器类的定义: Home\IndexController 类就代表了Home模块下的Index控制器,而hello操作就 是 Home\IndexController 类的hello(公共)方法。 当访问 http://serverName/index.php/Home/Index/hello 后会输出: 注意:如果你设置了操作方法绑定到类,则操作方法对应了一个类(参考操作绑定到类)。 定义控制器 控制器通常需要继承系统的Controller类或者其子类,例如,下面定义了一个 \Home\Controller\IndexController 控制器类: 1. 'Action', // 操作方法后缀 - 64 - 设置操作方法的后缀为Action,这样,控制器的操作方法定义调整为: 操作方法的后缀设置只是影响控制器类的定义,对URL访问没有影响。 多层控制器 ThinkPHP的控制器支持多层和多级,多层指的是控制器可以分层,例如除了默认的Controller控制器层 (我们可以称之为访问控制器),还可以添加事件控制器(层),例如: 访问控制器的名称是通过DEFAULT_C_LAYER设置的,默认是Controller。 1. 2, 1. ├─Controller 访问控制器 2. │ ├─User User分级(组) 3. │ │ ├─UserTypeController.class.php 4. │ │ ├─UserAuthController.class.php 5. │ ... 6. │ ├─Admin Admin分级(组) 7. │ │ ├─UserController.class.php 8. │ │ ├─ConfigController.class.php 9. │ ... - 66 - 多级控制器中的命名空间需要这样定义: 然后就可以通过URL地址访问: 如果希望简化URL地址中的模块地址,可以参考 模块部署 实例化控制器 访问控制器的实例化通常是自动完成的,系统会根据URL地址解析出访问的控制器名称自动实例化,并且 调用相关的操作方法。 如果你需要跨控制器调用的话,则可以单独实例化: 系统为上面的控制器实例化提供了一个快捷调用方法A,上面的代码可以简化为: 1. '; 8. } 9. public function index(){ 10. echo 'index
'; 11. } 12. //后置操作方法 13. public function _after_index(){ 14. echo 'after
'; - 68 - Action参数绑定 如果我们访问 http://serverName/index.php/Home/Index/index 结果会输出 前置和后置操作的注意事项如下: 1. 如果当前的操作并没有定义操作方法,而是直接渲染模板文件,那么如果定义了前置和后置方法 的话,依然会生效。真正有模板输出的可能仅仅是当前的操作,前置和后置操作一般情况是没有任 何输出的。 2. 需要注意的是,在有些方法里面使用了exit或者错误输出之类的话 有可能不会再执行后置方法 了。例如,如果在当前操作里面调用了系统Action的error方法,那么将不会再执行后置操作,但是 不影响success方法的后置方法执行。 参数绑定是通过直接绑定URL地址中的变量作为操作方法的参数,可以简化方法的定义甚至路由的解析。 参数绑定功能默认是开启的,其原理是把URL中的参数(不包括模块、控制器和操作名)和操作方法中的 参数进行绑定。 要启用参数绑定功能,首先确保你开启了 URL_PARAMS_BIND 设置: 参数绑定有两种方式:按照变量名绑定和按照变量顺序绑定。 按变量名绑定 默认的参数绑定方式是按照变量名进行绑定,例如,我们给Blog控制器定义了两个操作方法read和 archive方法,由于read操作需要指定一个id参数,archive方法需要指定年份(year)和月份(month) 两个参数,那么我们可以如下定义: 15. } 16. } 1. before 2. index 3. after 1. 'URL_PARAMS_BIND' => true, // URL变量绑定到操作方法作为参数 1. namespace Home\Controller; 2. use Think\Controller; 3. class BlogController extends Controller{ - 69 - 注意这里的操作方法并没有具体的业务逻辑,只是简单的示范。 URL的访问地址分别是: 两个URL地址中的id参数和year和month参数会自动和read操作方法以及archive操作方法的同名参数绑 定。 变量名绑定不一定由访问URL决定,路由地址也能起到相同的作用 输出的结果依次是: 按照变量名进行参数绑定的参数必须和URL中传入的变量名称一致,但是参数顺序不需要一致。也就是说 和上面的访问结果是一致的,URL中的参数顺序和操作方法中的参数顺序都可以随意调整,关键是确保参 数名称一致即可。 如果使用下面的URL地址进行访问,参数绑定仍然有效: 4. public function read($id){ 5. echo 'id='.$id; 6. } 7. 8. public function archive($year='2013',$month='01'){ 9. echo 'year='.$year.'&month='.$month; 10. } 11. } 1. http://serverName/index.php/Home/Blog/read/id/5 2. http://serverName/index.php/Home/Blog/archive/year/2013/month/11 1. id=5 2. year=2013&month=11 1. http://serverName/index.php/Home/Blog/archive/month/11/year/2013 1. http://serverName/index.php?s=/Home/Blog/read/id/5 2. http://serverName/index.php?s=/Home/Blog/archive/year/2013/month/11 3. http://serverName/index.php?c=Blog&a=read&id=5 4. http://serverName/index.php?c=Blog&a=archive&year=2013&month=11 - 70 - 如果用户访问的URL地址是(至于为什么会这么访问暂且不提): 那么会抛出下面的异常提示: 参数错误:id 报错的原因很简单,因为在执行read操作方法的时候,id参数是必须传入参数的,但是方法无法从URL地 址中获取正确的id参数信息。由于我们不能相信用户的任何输入,因此建议你给read方法的id参数添加默 认值,例如: 这样,当我们访问 http://serverName/index.php/Home/Blog/read/ 的时候 就会输出 当我们访问 http://serverName/index.php/Home/Blog/archive/ 的时候,输出: 始终给操作方法的参数定义默认值是一个避免报错的好办法 按变量顺序绑定 第二种方式是按照变量的顺序绑定,这种情况下URL地址中的参数顺序非常重要,不能随意调整。要按照 变量顺序进行绑定,必须先设置 URL_PARAMS_BIND_TYPE 为1: 操作方法的定义不需要改变,URL的访问地址分别改成: 输出的结果依次是: 这个时候如果改成 1. http://serverName/index.php/Home/Blog/read/ 1. public function read($id=0){ 2. echo 'id='.$id; 3. } 1. id=0 1. year=2013&month=01 1. 'URL_PARAMS_BIND_TYPE' => 1, // 设置参数绑定按照变量顺序绑定 1. http://serverName/index.php/Home/Blog/read/5 2. http://serverName/index.php/Home/Blog/archive/2013/11 1. id=5 2. year=2013&month=11 1. http://serverName/index.php/Home/Blog/archive/11/2013 - 71 - 伪静态 输出的结果就变成了: 显然就有问题了,所以不能随意调整参数在URL中的传递顺序,要确保和你的操作方法定义顺序一致。 可以看到,这种参数绑定的效果有点类似于简单的规则路由。 按变量顺序绑定的方式目前仅对PATHINFO地址有效,所以下面的URL访问参数绑定会失效: 但是,兼容模式URL地址访问依然有效: 如果你的操作方法定义都不带任何参数或者不希望使用该功能的话,可以关闭参数绑定功能: URL伪静态通常是为了满足更好的SEO效果,ThinkPHP支持伪静态URL设置,可以通过设 置 URL_HTML_SUFFIX 参数随意在URL的最后增加你想要的静态后缀,而不会影响当前操作的正常执行。 例如,我们设置 的话,我们可以把下面的URL http://serverName/Home/Blog/read/id/1 变成 http://serverName/Home/Blog/read/id/1.shtml 后者更具有静态页面的URL特征,但是具有和前面的URL相同的执行效果,并且不会影响原来参数的使 用。 默认情况下,伪静态的设置为 html ,如果我们设置伪静态后缀为空, 则可以支持所有的静态后缀,并且会记录当前的伪静态后缀到常量 __EXT__ ,但不会影响正常的页面访 问。 1. year=11&month=2013 1. http://serverName/index.php?c=Blog&a=read&id=5 2. http://serverName/index.php?c=Blog&a=archive&year=2013&month=11 1. http://serverName/index.php?s=/Home/Blog/read/5 2. http://serverName/index.php?s=/Home/Blog/archive/2013/11 1. 'URL_PARAMS_BIND' => false 1. 'URL_HTML_SUFFIX'=>'shtml' 1. 'URL_HTML_SUFFIX'=>'' - 72 - URL大小写 例如: 都可以正常访问,如果要获取当前的伪静态后缀,通过常量 __EXT__ 获取即可。 如果希望支持多个伪静态后缀,可以直接设置如下: 那么,当访问 http://serverName/Home/blog/3.pdf 的时候会报系统错误。 可以设置禁止访问的URL后缀,例如: 如果访问 http://serverName/Home/blog/3.pdf 就会直接返回404错误。 注意: URL_DENY_SUFFIX 的优先级比 URL_HTML_SUFFIX 要高。 系统默认的规范是根据URL里面的模块名、控制器名来定位到具体的控制器类的,从而执行控制器类的操 作方法。 以URL访问 http://serverName/index.php/Home/Index/index 为例,其实访问的控制器类文件是: 如果是Windows环境,无论大小写如何都能定位到 IndexController.class.php 文件,所以下面的访 问都是有效的: 如果在Linux环境下面,一旦大小写不一致,就会发生URL里面使用小写模块名不能找到模块类的情况。 1. http://serverName/Home/blog/3.html 2. http://serverName/Home/blog/3.shtml 3. http://serverName/Home/blog/3.xml 4. http://serverName/Home/blog/3.pdf 1. // 多个伪静态后缀设置 用|分割 2. 'URL_HTML_SUFFIX' => 'html|shtml|xml' 1. 'URL_DENY_SUFFIX' => 'pdf|ico|png|gif|jpg', // URL禁止访问的后缀设置 1. Application/Home/Controller/IndexController.class.php 1. http://serverName/index.php/Home/Index/index 2. http://serverName/index.php/Home/index/index 3. http://serverName/index.php/home/index/index - 73 - URL生成 例如在Linux环境下面,我们访问 http://serverName/index.php/home/index/index 其实请求的控制 器文件是 因为,我们定义的控制器类是IndexController而不是indexController(参考ThinkPHP的命名规范), 由于Linux的文件特性,其实是不存在indexController控制器文件的,就会出现Index控制器不存在的错 误,这样的问题会造成用户体验的下降。 但是系统本身提供了一个不区分URL大小写的解决方案,可以通过配置简单实现。 只要在项目配置中,增加: 配置好后,即使是在Linux环境下面,也可以实现URL访问不再区分大小写了。 这里需要注意一个地方,一旦开启了不区分URL大小写后,如果我们要访问类似UserTypeController的 控制器,那么正确的URL访问应该是: 利用系统提供的U方法可以为你自动生成相关的URL地址。 如果设置 的话,URL就又变成: http://serverName/index.php/Home/UserType/add 注意:URL不区分大小写并不会改变系统的命名规范,并且只有按照系统的命名规范后才能正确的 实现URL不区分大小写。 1. Application/home/Controller/indexController.class.php 1. 'URL_CASE_INSENSITIVE' =>true 1. http://serverName/index.php/Home/Index/index 2. // 将等效于 3. http://serverName/index.php/home/index/index 1. // 正确的访问地址 2. http://serverName/index.php/home/user_type/index 3. // 错误的访问地址(linux环境下) 4. http://serverName/index.php/home/usertype/index 1. 'URL_CASE_INSENSITIVE' =>false - 74 - 为了配合所使用的URL模式,我们需要能够动态的根据当前的URL设置生成对应的URL地址,为 此,ThinkPHP内置提供了U方法,用于URL的动态生成,可以确保项目在移植过程中不受环境的影响。 定义规则 U方法的定义规则如下(方括号内参数根据实际应用决定): U('地址表达式',['参数'],['伪静态后缀'],['显示域名']) 地址表达式 地址表达式的格式定义如下: 如果不定义模块的话 就表示当前模块名称,下面是一些简单的例子: 参数 U方法的第二个参数支持数组和字符串两种定义方式,如果只是字符串方式的参数可以在第一个参数中定 义,例如: 三种方式是等效的,都是生成Blog控制器的cate操作 并且 cate_id 为1 status 为1的URL地址。 但是不允许使用下面的定义方式来传参数 伪静态后缀 U函数会自动识别当前配置的伪静态后缀,如果你需要指定后缀生成URL地址的话,可以显式传入,例 如: 自动识别 根据项目的不同URL设置,同样的U方法调用可以智能地对应产生不同的URL地址效果,例如针对: 1. [模块/控制器/操作#锚点@域名]?参数1=值1&参数2=值2... 1. U('User/add') // 生成User控制器的add操作的URL地址 2. U('Blog/read?id=1') // 生成Blog控制器的read操作 并且id为1的URL地址 3. U('Admin/User/select') // 生成Admin模块的User控制器的select操作的URL地址 1. U('Blog/cate',array('cate_id'=>1,'status'=>1)) 2. U('Blog/cate','cate_id=1&status=1') 3. U('Blog/cate?cate_id=1&status=1') 1. U('Blog/cate/cate_id/1/status/1'); 1. U('Blog/cate','cate_id=1&status=1','xml'); 1. U('Blog/read?id=1'); - 75 - 这个定义为例。 如果当前URL设置为普通模式的话,最后生成的URL地址是: 如果当前URL设置为PATHINFO模式的话,同样的方法最后生成的URL地址是: 如果当前URL设置为REWRITE模式的话,同样的方法最后生成的URL地址是: 如果当前URL设置为REWRITE模式,并且设置了伪静态后缀为.html的话,同样的方法最后生成的URL地 址是: 如果开启了 URL_CASE_INSENSITIVE ,则会统一生成小写的URL地址。 生成路由地址 U方法还可以支持路由,如果我们定义了一个路由规则为: 那么可以使用 最终生成的URL地址是: 注意:如果你是在模板文件中直接使用U方法的话,需要采用 {:U('参数1', '参数2'…)} 的方式,具体 参考模板的使用函数内容。 域名支持 如果你的应用涉及到多个子域名的操作地址,那么也可以在U方法里面指定需要生成地址的域名,例如: 1. http://serverName/index.php?m=Blog&a=read&id=1 1. http://serverName/index.php/Home/Blog/read/id/1 1. http://serverName/Home/Blog/read/id/1 1. http://serverName/Home/Blog/read/id/1.html 1. 'news/:id\d'=>'News/read' 1. U('/news/1'); 1. http://serverName/index.php/Home/news/1 1. U('Blog/read@blog.thinkphp.cn','id=1'); - 76 - AJAX返回 @后面传入需要指定的域名即可。 系统会自动判断当前是否SSL协议,生成 https:// 。 此外,U方法的第4个参数如果设置为true,表示自动识别当前的域名,并且会自动根据子域名部署设 置 APP_SUB_DOMAIN_DEPLOY 和 APP_SUB_DOMAIN_RULES 自动匹配生成当前地址的子域名。 锚点支持 U函数可以直接生成URL地址中的锚点,例如: 生成的URL地址可能是: ThinkPHP可以很好的支持AJAX请求,系统的\Think\Controller类提供了ajaxReturn方法用于AJAX调用 后返回数据给客户端。并且支持JSON、JSONP、XML和EVAL四种方式给客户端接受数据,并且支持配 置其他方式的数据格式返回。 ajaxReturn方法调用示例: 支持返回数组数据: 默认配置采用JSON格式返回数据(通过配置DEFAULT_AJAX_RETURN进行设置),我们可以指定格式 返回,例如: 返回数据data可以支持字符串、数字和数组、对象,返回客户端的时候根据不同的返回格式进行编码后传 1. U('Blog/read#comment?id=1'); 1. http://serverName/index.php/Home/Blog/read/id/1#comment 1. $data = 'ok'; 2. $this->ajaxReturn($data); 1. $data['status'] = 1; 2. $data['content'] = 'content'; 3. $this->ajaxReturn($data); 1. // 指定XML格式返回数据 2. $data['status'] = 1; 3. $data['content'] = 'content'; 4. $this->ajaxReturn($data,'xml'); - 77 - 跳转和重定向 输。如果是JSON/JSONP格式,会自动编码成JSON字符串,如果是XML方式,会自动编码成XML字符 串,如果是EVAL方式的话,只会输出字符串data数据。 JSON和JSONP虽然只有一个字母的差别,但其实他们根本不是一回事儿:JSON是一种数据交换格 式,而JSONP是一种非官方跨域数据交互协议。一个是描述信息的格式,一个是信息传递的约定方 法。 默认的JSONP格式返回的处理方法是 jsonpReturn ,如果你采用不同的方法,可以设置: 或者直接在页面中用callback参数来指定。 除了上面四种返回类型外,我们还可以通过行为扩展来增加其他类型的支持,只需要 对 ajax_return 标签位进行行为绑定即可。 页面跳转 在应用开发中,经常会遇到一些带有提示信息的跳转页面,例如操作成功或者操作错误页面,并且自动跳 转到另外一个目标页面。系统的\Think\Controller类内置了两个跳转方法success和error,用于页面跳 转提示,而且可以支持ajax提交。 使用方法很简单,举例如下: success和error方法的第一个参数表示提示信息,第二个参数表示跳转地址,第三个参数是跳转时间(单 位为秒),例如: 1. 'DEFAULT_JSONP_HANDLER' => 'myJsonpReturn', // 默认JSONP格式返回的处理方法 1. $User = M('User'); //实例化User对象 2. $result = $User->add($data); 3. if($result){ 4. //设置成功后跳转页面的地址,默认的返回页面是$_SERVER['HTTP_REFERER'] 5. $this->success('新增成功', 'User/list'); 6. } else { 7. //错误页面的默认跳转页面是返回前一页,通常不需要设置 8. $this->error('新增失败'); 9. } - 78 - 跳转地址是可选的,success方法的默认跳转地址是 $_SERVER["HTTP_REFERER"] ,error方法的默认跳 转地址是 javascript:history.back(-1); 。 默认的等待时间success方法是1秒,error方法是3秒 success 和 error 方法都可以对应的模板,默认的设置是两个方法对应的模板都是: 也可以使用项目内部的模板文件 模板文件可以使用模板标签,并且可以使用下面的模板变量: 变量 含义 $msgTitle 操作标题 $message 页面提示信息 $status 操作状态 1表示成功 0 表示失败 具体还可以由项目本身定义规则 $waitSecond 跳转等待时间 单位为秒 $jumpUrl 跳转页面地址 success和error方法会自动判断当前请求是否属于Ajax请求,如果属于Ajax请求则会调用ajaxReturn方 法返回信息。 ajax方式下面,success和error方法会封装下面的数据返回: 1. // 操作完成3秒后跳转到 /Article/index 2. $this->success('操作完成','/Article/index',3); 3. // 操作失败5秒后跳转到 /Article/error 4. $this->error('操作失败','/Article/error',5); 1. //默认错误跳转对应的模板文件 2. 'TMPL_ACTION_ERROR' => THINK_PATH . 'Tpl/dispatch_jump.tpl', 3. //默认成功跳转对应的模板文件 4. 'TMPL_ACTION_SUCCESS' => THINK_PATH . 'Tpl/dispatch_jump.tpl', 1. //默认错误跳转对应的模板文件 2. 'TMPL_ACTION_ERROR' => 'Public:error'; 3. //默认成功跳转对应的模板文件 4. 'TMPL_ACTION_SUCCESS' => 'Public:success'; 1. $data['info'] = $message; // 提示信息内容 - 79 - 输入变量 重定向 Controller类的redirect方法可以实现页面的重定向功能。 redirect方法的参数用法和U函数的用法一致(参考URL生成部分),例如: 上面的用法是停留5秒后跳转到New模块的category操作,并且显示页面跳转中字样,重定向后会改变当 前的URL地址。 如果你仅仅是想重定向要一个指定的URL地址,而不是到某个模块的操作方法,可以直接使 用 redirect 函数重定向,例如: Redirect函数的第一个参数是一个URL地址。 控制器的redirect方法和redirect函数的区别在于前者是用URL规则定义跳转地址,后者是一个纯粹 的URL地址。 在Web开发过程中,我们经常需要获取系统变量或者用户提交的数据,这些变量数据错综复杂,而且一不 小心就容易引起安全隐患,但是如果利用好ThinkPHP提供的变量获取功能,就可以轻松的获取和驾驭变 量了。 获取变量 虽然你仍然可以在开发过程中使用传统方式获取各种系统变量,例如: 2. $data['status'] = $status; // 状态 如果是success是1 error 是0 3. $data['url'] = $jumpUrl; // 成功或者错误的跳转地址 1. //重定向到New模块的Category操作 2. $this->redirect('New/category', array('cate_id' => 2), 5, '页面跳转中...'); 1. //重定向到指定的URL地址 2. redirect('/New/category/cate_id/2', 5, '页面跳转中...') 1. $id = $_GET['id']; // 获取get变量 2. $name = $_POST['name']; // 获取post变量 3. $value = $_SESSION['var']; // 获取session变量 4. $name = $_COOKIE['name']; // 获取cookie变量 5. $file = $_SERVER['PHP_SELF']; // 获取server变量 - 80 - 但是我们不建议直接使用传统方式获取,因为没有统一的安全处理机制,后期如果调整的话,改起来会比 较麻烦。所以,更好的方式是在框架中统一使用I函数进行变量获取和过滤。 I方法是ThinkPHP众多单字母函数中的新成员,其命名来自于英文Input(输入),主要用于更加方便和 安全的获取系统输入变量,可以用于任何地方,用法格式如下: I('变量类型.变量名',['默认值'],['过滤方法'],['额外数据源']) 变量类型是指请求方式或者输入类型,包括: 变量类型 含义 get 获取GET参数 post 获取POST参数 param 自动判断请求类型获取GET、POST或者PUT参数 request 获取REQUEST 参数 put 获取PUT 参数 session 获取 $_SESSION 参数 cookie 获取 $_COOKIE 参数 server 获取 $_SERVER 参数 globals 获取 $GLOBALS参数 path 获取 PATHINFO模式的URL参数(3.2.2新增) data 获取 其他类型的参数,需要配合额外数据源参数(3.2.2新增) 注意:变量类型不区分大小写。 变量名则严格区分大小写。 默认值和过滤方法均属于可选参数。 我们以GET变量类型为例,说明下I方法的使用: 支持默认值: 1. echo I('get.id'); // 相当于 $_GET['id'] 2. echo I('get.name'); // 相当于 $_GET['name'] 1. echo I('get.id',0); // 如果不存在$_GET['id'] 则返回0 - 81 - 采用方法过滤: 支持直接获取整个变量类型,例如: 用同样的方式,我们可以获取post或者其他输入类型的变量,例如: param变量类型是框架特有的支持自动判断当前请求类型的变量获取方式,例如: echo I('param.id'); 如果当前请求类型是GET,那么等效于 $_GET['id'],如果当前请求类型是POST或者PUT,那么相当于获 取 $_POST['id'] 或者 PUT参数id。 由于param类型是I函数默认获取的变量类型,因此事实上param变量类型的写法可以简化为: 3.2.2新增了 path和data两个变量类型 ,用法如下: path类型变量可以用于获取URL参数(必须是PATHINFO模式参数有效,无论是GET还是POST方式都有 效),例如: 当前访问URL地址是 http://serverName/index.php/New/2013/06/01 那么我们可以通过 data类型变量可以用于获取不支持的变量类型的读取,例如: 2. echo I('get.name',''); // 如果不存在$_GET['name'] 则返回空字符串 1. // 采用htmlspecialchars方法对$_GET['name'] 进行过滤,如果不存在则返回空字符串 2. echo I('get.name','','htmlspecialchars'); 1. // 获取整个$_GET 数组 2. I('get.'); 1. I('post.name','','htmlspecialchars'); // 采用htmlspecialchars方法对$_POST['name' ] 进行过滤,如果不存在则返回空字符串 2. I('session.user_id',0); // 获取$_SESSION['user_id'] 如果不存在则默认为0 3. I('cookie.'); // 获取整个 $_COOKIE 数组 4. I('server.REQUEST_METHOD'); // 获取 $_SERVER['REQUEST_METHOD'] 1. I('id'); // 等同于 I('param.id') 2. I('name'); // 等同于 I('param.name') 1. echo I('path.1'); // 输出2013 2. echo I('path.2'); // 输出06 3. echo I('path.3'); // 输出01 - 82 - 变量过滤 如果你没有在调用I函数的时候指定过滤方法的话,系统会采用默认的过滤机制(由DEFAULT_FILTER配 置),事实上,该参数的默认设置是: 也就说,I方法的所有获取变量如果没有设置过滤方法的话都会进行htmlspecialchars过滤,那么: 同样,该参数也可以设置支持多个过滤,例如: 设置后,我们在使用: 如果我们在使用I方法的时候 指定了过滤方法,那么就会忽略DEFAULT_FILTER的设置,例如: I方法的第三个参数如果传入函数名,则表示调用该函数对变量进行过滤并返回(在变量是数组的情况下自 动使用 array_map 进行过滤处理),否则会调用PHP内置的 filter_var 方法进行过滤处理,例如: 表示 会对 $_POST['email'] 进行 格式验证,如果不符合要求的话,返回空字符串。 (关于更多的验证 格式,可以参考 官方手册的 filter_var 用法。) 或者可以用下面的字符标识方式: 可以支持的过滤名称必须是 filter_list 方法中的有效值(不同的服务器环境可能有所不同),可能支 持的包括: 1. I('data.file1','','',$_FILES); 1. // 系统默认的变量过滤机制 2. 'DEFAULT_FILTER' => 'htmlspecialchars' 1. // 等同于 htmlspecialchars($_GET['name']) 2. I('get.name'); 1. 'DEFAULT_FILTER' => 'strip_tags,htmlspecialchars' 1. // 等同于 htmlspecialchars(strip_tags($_GET['name'])) 2. I('get.name'); 1. // 等同于 strip_tags($_GET['name']) 2. echo I('get.name','','strip_tags'); 1. I('post.email','',FILTER_VALIDATE_EMAIL); 1. I('post.email','','email'); 1. int 2. boolean - 83 - 请求类型 在有些特殊的情况下,我们不希望进行任何过滤,即使DEFAULT_FILTER已经有所设置,可以使用: 一旦过滤参数设置为空字符串或者false,即表示不再进行任何的过滤。 判断请求类型 在很多情况下面,我们需要判断当前操作的请求类型是GET 、POST 、PUT或 DELETE,一方面可以针对 请求类型作出不同的逻辑处理,另外一方面有些情况下面需要验证安全性,过滤不安全的请求。 系统内置 了一些常量用于判断请求类型,包括: 常量 说明 IS_GET 判断是否是GET方式提交 IS_POST 判断是否是POST方式提交 IS_PUT 判断是否是PUT方式提交 3. float 4. validate_regexp 5. validate_url 6. validate_email 7. validate_ip 8. string 9. stripped 10. encoded 11. special_chars 12. unsafe_raw 13. email 14. url 15. number_int 16. number_float 17. magic_quotes 18. callback 1. // 下面两种方式都不采用任何过滤方法 2. I('get.name','',''); 3. I('get.id','',false); - 84 - 空操作 IS_DELETE 判断是否是DELETE方式提交 IS_AJAX 判断是否是AJAX提交 REQUEST_METHOD 当前提交类型 使用举例如下: 需要注意的是,如果使用的是ThinkAjax或者自己写的Ajax类库的话,需要在表单里面添加一个隐藏 域,告诉后台属于ajax方式提交,默认的隐藏域名称是ajax(可以通过VAR_AJAX_SUBMIT配 置),如果是JQUERY类库的话,则无需添加任何隐藏域即可自动判断。 空操作是指系统在找不到请求的操作方法的时候,会定位到空操作( _empty )方法来执行,利用这个机 制,我们可以实现错误页面和一些URL的优化。 例如,下面我们用空操作功能来实现一个城市切换的功能。 我们只需要给CityAction类定义一 个 _empty (空操作)方法: 1. class UserController extends Controller{ 2. public function update(){ 3. if (IS_POST){ 4. $User = M('User'); 5. $User->create(); 6. $User->save(); 7. $this->success('保存完成'); 8. }else{ 9. $this->error('非法请求'); 10. } 11. } 12. } 1. city($name); 8. } 9. 10. //注意 city方法 本身是 protected 方法 11. protected function city($name){ 12. //和$name这个城市相关的处理 13. echo '当前城市' . $name; 14. } 15. } 1. http://serverName/index.php/Home/City/beijing/ 2. http://serverName/index.php/Home/City/shanghai/ 3. http://serverName/index.php/Home/City/shenzhen/ 1. 当前城市:beijing 2. 当前城市:shanghai 3. 当前城市:shenzhen 1. http://serverName/index.php/Home/City/shanghai/ 1. http://serverName/index.php/Home/shanghai/ - 86 - 类的index方法里面进行处理。可是如果使用空控制器功能,这个问题就可以迎刃而解了。 我们可以给项目定义一个EmptyController类 接下来,我们就可以在浏览器里面输入 由于系统并不存在beijing、shanghai或者shenzhen控制器,因此会定位到空控制器 (EmptyController)去执行,会看到依次输出的结果是: 如果你的默认控制器层不是Controller的话,空控制器也会随之改变,例如默认的控制器层是Action: 那么可能你的空控制器定义就变成了EmptyAction类: 1. city($cityName); 9. } 10. //注意 city方法 本身是 protected 方法 11. protected function city($name){ 12. //和$name这个城市相关的处理 13. echo '当前城市' . $name; 14. } 15. } 1. http://serverName/index.php/Home/beijing/ 2. http://serverName/index.php/Home/shanghai/ 3. http://serverName/index.php/Home/shenzhen/ 1. 当前城市:beijing 2. 当前城市:shanghai 3. 当前城市:shenzhen 1. 'DEFAULT_C_LAYER' => 'Action', // 默认的控制器层名称 1. city($cityName); 9. } 10. //注意 city方法 本身是 protected 方法 11. protected function city($name){ 12. //和$name这个城市相关的处理 13. echo '当前城市' . $name; 14. } 15. } 1. 'ACTION_BIND_CLASS' => True, 1. Application/Home/Controller/IndexController.class.php 1. namespace Home\Controller; 2. use Think\Controller; 3. class IndexController extends Controller{ 4. public function index(){ 5. echo '执行Index控制器的index操作'; 6. } 7. } - 88 - 可以看到,实际上我们调用的是 Home\Controller\IndexController 类的index方法。 设置后,控制器文件位置改为: 控制器类的定义如下: 现在,我们调用的其实是 Home\Controller\Index\index 类的run方法。 run方法依旧可以支持传入参数和进行Action参数绑定操作,但不再支持A方法实例化和R方法远程 调用,我们建议R方法不要进行当前访问控制器的远程调用。 前置和后置操作 当设置操作方法绑定到类后,前置和后置操作的定义有所改变,只需要在类里面定 义 _before_run 和 _after_run 方法即可,例如: 1. Application/Home/Controller/Index/index.class.php 1. namespace Home\Controller\Index; 2. use Think\Controller; 3. class index extends Controller{ 4. public function run(){ 5. echo '执行Index控制器的index操作'; 6. } 7. } 1. namespace Home\Controller\Index; 2. use Think\Controller; 3. class index extends Controller{ 4. public function _before_run(){ 5. echo 'before_'.ACTION_NAME; 6. } 7. 8. public function run(){ 9. echo '执行Index控制器的index操作'; 10. } 11. 12. public function _after_run(){ 13. echo 'after_'.ACTION_NAME; - 89 - 空控制器 操作方法绑定到类后,一样可以支持空控制器,我们可以创建 Application/Home/Controller/_empty 目录,即表示如果找不到当前的控制器的话,会到_empty控 制器目录下面定位操作方法。 例如,我们访问了URL地址 http://serverName/Home/Test/index ,但并不存在 Application/Home/Controller/Test 目录,但是有定义 Application/Home/Controller/_empty 目 录。 并且我们有定义: 控制器定义如下: 访问 http://serverName/Home/Test/index 后 输出结果显示: 空操作 操作绑定到类后,我们依然可以实现空操作方法,我们只要定义一个 Home\Controller\Index\_empty 类,就可以支持Index控制器的空操作访问,例如: 控制器定义如 下: 14. } 15. } 1. Application/Home/Controller/_empty/index.class.php 1. namespace Home\Controller\_empty; 2. use Think\Controller; 3. class index extends Controller{ 4. public function run(){ 5. echo '执行'CONTROLLER_NAME.'控制器的'.ACTION_NAME.'操作'; 6. } 7. } 1. 执行Test控制器的index操作 1. namespace Home\Controller\Index; 2. use Think\Controller; 3. class _empty extends Controller{ 4. public function run(){ 5. echo '执行Index控制器的'.ACTION_NAME.'操作'; - 90 - 当我们访问 http://serverName/Home/Index/test 后 输出结果显示: 6. } 7. } 1. 执行Index控制器的test操作 - 91 - 模型 模型定义 在ThinkPHP中基础的模型类就是Think\Model类,该类完成了基本的CURD、ActiveRecord模式、连贯 操作和统计查询,一些高级特性被封装到另外的模型扩展中。 基础模型类的设计非常灵活,甚至可以无需进行任何模型定义,就可以进行相关数据表的ORM和CURD操 作,只有在需要封装单独的业务逻辑的时候,模型类才是必须被定义的。 模型定义 模型类并非必须定义,只有当存在独立的业务逻辑或者属性的时候才需要定义。 模型类通常需要继承系统的\Think\Model类或其子类,下面是一个Home\Model\UserModel类的定 义: 模型类的作用大多数情况是操作数据表的,如果按照系统的规范来命名模型类的话,大多数情况下是可以 自动对应数据表。 模型类的命名规则是除去表前缀的数据表名称,采用驼峰法命名,并且首字母大写,然后加上模型层的名 称(默认定义是Model),例如: 模型名 约定对应数据表(假设数据库的前缀定义是 think_) UserModel think_user UserTypeModel think_user_type 如果你的规则和上面的系统约定不符合,那么需要设置Model类的数据表名称属性,以确保能够找到对应 的数据表。 数据表定义 在ThinkPHP的模型里面,有几个关于数据表名称的属性定义: 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model { 4. } - 92 - 属性 说明 tablePrefix 定义模型对应数据表的前缀,如果未定义则获取配置文件中的DB_PREFIX参数 tableName 不包含表前缀的数据表名称,一般情况下默认和模型名称相同,只有当你的表名和当 前的模型类的名称不同的时候才需要定义。 trueTableName 包含前缀的数据表名称,也就是数据库中的实际表名,该名称无需设置,只有当上面 的规则都不适用的情况或者特殊情况下才需要设置。 dbName 定义模型当前对应的数据库名称,只有当你当前的模型类对应的数据库名称和配置文 件不同的时候才需要定义。 举个例子来加深理解,例如,在数据库里面有一个 think_categories 表,而我们定义的模型类名称 是 CategoryModel ,按照系统的约定,这个模型的名称是Category,对应的数据表名称应该 是 think_category (全部小写),但是现在的数据表名称是 think_categories ,因此我们就需要设 置 tableName 属性来改变默认的规则(假设我们已经在配置文件里面定义了 DB_PREFIX 为 think_)。 注意这个属性的定义不需要加表的前缀 think_ 如果我们需要CategoryModel模型对应操作的数据表是 top_category ,那么我们只需要设置数据表前 缀即可: 如果你的数据表直接就是 category ,而没有前缀,则可以设置 tablePrefix 为空字符串。 1. namespace Home\Model; 2. use Think\Model; 3. class CategoryModel extends Model { 4. protected $tableName = 'categories'; 5. } 1. namespace Home\Model; 2. use Think\Model; 3. class CategoryModel extends Model { 4. protected $tablePrefix = 'top_'; 5. } 1. namespace Home\Model; 2. use Think\Model; 3. class CategoryModel extends Model { 4. protected $tablePrefix = ''; - 93 - 模型实例化 没有表前缀的情况必须设置,否则会获取当前配置文件中的 DB_PREFIX 。 而对于另外一种特殊情况,我们需要操作的数据表是 top_categories ,这个时候我们就需要定义 trueTableName 属性 注意 trueTableName 需要完整的表名定义。 除了数据表的定义外,还可以对数据库进行定义(用于操作当前数据库以外的数据表),例如 top.top_categories : 系统的规则下,tableName会转换为小写定义,但是trueTableName定义的数据表名称是保持原 样。 在ThinkPHP中,可以无需进行任何模型定义。只有在需要封装单独的业务逻辑的时候,模型类才是必须 被定义的,因此ThinkPHP在模型上有很多的灵活和方便性,让你无需因为表太多而烦恼。 根据不同的模型定义,我们有几种实例化模型的方法,根据需要采用不同的方式: 直接实例化 可以和实例化其他类库一样实例化模型类,例如: 5. } 1. namespace Home\Model; 2. use Think\Model; 3. class CategoryModel extends Model { 4. protected $trueTableName = 'top_categories'; 5. } 1. namespace Home\Model; 2. use Think\Model; 3. class CategoryModel extends Model { 4. protected $trueTableName = 'top_categories'; 5. protected $dbName = 'top'; 6. } - 94 - 模型类通常都是继承系统的\Think\Model类,该类的架构方法有三个参数,分别是: Model(['模型名'],['数据表前缀'],['数据库连接信息']); 三个参数都是可选的,大多数情况下,我们根本无需传入任何参数即可实例化。 参数 描述 模型名 模型的名称 和数据表前缀一起配合用于自动识别数据表名称 数据表前缀 当前数据表前缀 和模型名一起配合用于自动识别数据表名称 数据库连接信息 当前数据表的数据库连接信息 如果没有则获取配置文件中的 如果当前数据表没有前缀,则传入空字符串即可 数据库连接信息参数支持三种格式: 1、字符串定义 字符串定义采用DSN格式定义,格式定义规范为: 例如: 2、数组定义 可以传入数组格式的数据库连接信息,例如: 1. $User = new \Home\Model\UserModel(); 2. $Info = new \Admin\Model\InfoModel(); 3. // 带参数实例化 4. $New = new \Home\Model\NewModel('blog','think_',$connection); 1. type://username:passwd@hostname:port/DbName 2. // 3.2.1以上版本还可以支持字符集设定 3. type://username:passwd@hostname:port/DbName#charset 1. new \Home\Model\NewModel('blog','think_','mysql://root:1234@localhost/demo'); 1. $connection = array( 2. 'db_type' => 'mysql', 3. 'db_host' => '127.0.0.1', 4. 'db_user' => 'root', 5. 'db_pwd' => '12345', 6. 'db_port' => 3306, - 95 - 3.2.1以上版本还可以支持数据编码设定,例如: 3、配置定义 我们可以事先在配置文件中定义好数据库连接信息,然后在实例化的时候直接传入配置的名称即可,例 如: 在配置文件中定义数据库连接信息的时候也支持字符串和数组格式,格式和上面实例化传入的参数一样。 然后,我们就可以这样实例化模型类传入连接信息: 7. 'db_name' => 'demo', 8. ); 9. new \Home\Model\NewModel('new','think_',$connection); 1. $connection = array( 2. 'db_type' => 'mysql', 3. 'db_host' => '127.0.0.1', 4. 'db_user' => 'root', 5. 'db_pwd' => '12345', 6. 'db_port' => 3306, 7. 'db_name' => 'demo', 8. 'db_charset' => 'utf8', 9. ); 10. new \Home\Model\NewModel('new','think_',$connection); 1. //数据库配置1 2. 'DB_CONFIG1' => array( 3. 'db_type' => 'mysql', 4. 'db_user' => 'root', 5. 'db_pwd' => '1234', 6. 'db_host' => 'localhost', 7. 'db_port' => '3306', 8. 'db_name' => 'thinkphp' 9. ), 10. //数据库配置2 11. 'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp', 1. new \Home\Model\NewModel('new','think_','DB_CONFIG1'); 2. new \Home\Model\BlogModel('blog','think_','DB_CONFIG2'); - 96 - 事实上,当我们实例化的时候没有传入任何的数据库连接信息的时候,系统其实默认会获取配置文件中的 相关配置参数,包括: 如果应用配置文件中有配置上述数据库连接信息的话,实例化模型将会变得非常简单。 D方法实例化 上面实例化的时候我们需要传入完整的类名,系统提供了一个快捷方法D用于数据模型的实例化操作。 要实例化自定义模型类,可以使用下面的方式: 当 \Home\Model\UserModel 类不存在的时候,D函数会尝试实例化公共模块下面的 \Common\Model\UserModel 类。 D方法的参数就是模型的名称,并且和模型类的大小写定义是一致的,例如: 参数 实例化的模型文件(假设当前模块为Home) User 对应的模型类文件的 \Home\Model\UserModel.class.php UserType 对应的模型类文件的 \Home\Model\UserTypeModel.class.php 如果在Linux环境下面,一定要注意D方法实例化的时候的模型名称的大小写。 1. 'DB_TYPE' => '', // 数据库类型 2. 'DB_HOST' => '', // 服务器地址 3. 'DB_NAME' => '', // 数据库名 4. 'DB_USER' => '', // 用户名 5. 'DB_PWD' => '', // 密码 6. 'DB_PORT' => '', // 端口 7. 'DB_PREFIX' => '', // 数据库表前缀 8. 'DB_DSN' => '', // 数据库连接DSN 用于PDO方式 9. 'DB_CHARSET' => 'utf8', // 数据库的编码 默认为utf8 1. select(); - 97 - D方法可以自动检测模型类,如果存在自定义的模型类,则实例化自定义模型类,如果不存在,则会实例 化系统的\Think\Model基类,同时对于已实例化过的模型,不会重复去实例化。 注意:跨模块实例化模型类的时候 不支持自动加载公共模块的模型类。 M方法实例化模型 D方法实例化模型类的时候通常是实例化某个具体的模型类,如果你仅仅是对数据表进行基本的CURD操 作的话,使用M方法实例化的话,由于不需要加载具体的模型类,所以性能会更高。 例如: M方法也可以支持跨库操作,例如: M方法的参数和\Think\Model类的参数是一样的,也就是说,我们也可以这样实例化: 具体的参数含义可以参考前面的介绍。 M方法实例化的时候,默认情况下是直接实例化系统的\Think\Model类,如果我们希望实例化其他的公 共模型类的话,可以使用如下方法: 1. D方法还可以支持跨模块调用,需要使用: 2. //实例化Admin模块的User模型 3. D('Admin/User'); 4. //实例化Extend扩展命名空间下的Info模型 5. D('Extend://Editor/Info'); 1. // 使用M方法实例化 2. $User = M('User'); 3. // 和用法 $User = new \Think\Model('User'); 等效 4. // 执行其他的数据操作 5. $User->select(); 1. // 使用M方法实例化 操作db_name数据库的ot_user表 2. $User = M('db_name.User','ot_'); 3. // 执行其他的数据操作 4. $User->select(); 1. $New = M('new','think_',$connection); 2. // 等效于 $New = new \Think\Model('new','think_',$connection); - 98 - 字段定义 如果你的模型类有自己的业务逻辑,M方法是无法支持的,就算是你已经定义了具体的模型类,M 方法实例化的时候是会直接忽略。 实例化空模型类 如果你仅仅是使用原生SQL查询的话,不需要使用额外的模型类,实例化一个空模型类即可进行操作了, 例如: 实例化空模型类后还可以用table方法切换到具体的数据表进行操作 我们在实例化的过程中,经常使用D方法和M方法,这两个方法的区别在于M方法实例化模型无需用户为 每个数据表定义模型类,如果D方法没有找到定义的模型类,则会自动调用M方法。 通常每个模型类是操作某个数据表,在大多数情况下,系统会自动获取当前数据表的字段信息。 系统会在模型首次实例化的时候自动获取数据表的字段信息(而且只需要一次,以后会永久缓存字段信 息,除非设置不缓存或者删除),如果是调试模式则不会生成字段缓存文件,则表示每次都会重新获取数 据表字段信息。 字段缓存保存在 Runtime/Data/_fields/ 目录下面,缓存机制是每个模型对应一个字段缓存文件(注 意:并非每个数据表对应一个字段缓存文件),命名格式是: 数据库名.模型名(小写).php 例如: 1. $User = M('\Home\Model\CommonModel:User','think_','db_config'); 2. // 相当于 $User = new \Home\Model\CommonModel('User','think_','db_config'); 1. //实例化空模型 2. $Model = new Model(); 3. //或者使用M快捷方法是等效的 4. $Model = M(); 5. //进行原生的SQL查询 6. $Model->query('SELECT * FROM think_user WHERE status = 1'); 1. demo.user.php // User模型生成的字段缓存文件 2. demo.article.php // Article模型生成的字段缓存文件 - 99 - 字段缓存包括数据表的字段信息、主键字段和是否自动增长,如果开启字段类型验证的话还包括字段类型 信息等等,无论是用M方法还是D方法,或者用原生的实例化模型类一般情况下只要是不开启调试模式都 会生成字段缓存(字段缓存可以单独设置关闭)。 可以通过设置 DB_FIELDS_CACHE 参数来关闭字段自动缓存,如果在开发的时候经常变动数据库的结构, 而不希望进行数据表的字段缓存,可以在项目配置文件中增加如下配置: 注意:调试模式下面由于考虑到数据结构可能会经常变动,所以默认是关闭字段缓存的。 如果需要显式获取当前数据表的字段信息,可以使用模型类的getDbFields方法来获取当前数据对象的全 部字段信息,例如: 如果你在部署模式下面修改了数据表的字段信息,可能需要清空 Data/_fields 目录下面的缓存文件,让 系统重新获取更新的数据表字段信息,否则会发生新增的字段无法写入数据库的问题。 如果不希望依赖字段缓存或者想提高性能,也可以在模型类里面手动定义数据表字段的名称,可以避免IO 加载的效率开销,例如: _pk 表示定义当前数据表的主键名。 除了可以设置数据表的字段之外,我们还可以定义字段的类型,用于某些验证环节。例如: 1. // 关闭字段缓存 2. 'DB_FIELDS_CACHE'=>false 1. $User = M('User'); 2. $fields = $User->getDbFields(); 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model { 4. protected $fields = array('id', 'username', 'email', 'age','_pk'=>'id'); 5. } 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model { 4. protected $fields = array('id', 'username', 'email', 'age','_pk'=>'id', 5. '_type'=>array('id'=>'bigint','username'=>'varchar','email'=>'varchar',' age'=>'int') - 100 - 连接数据库 ThinkPHP内置了抽象数据库访问层,把不同的数据库操作封装起来,我们只需要使用公共的Db类进行操 作,而无需针对不同的数据库写不同的代码和底层实现,Db类会自动调用相应的数据库驱动来处理。目 前的数据库包括Mysql、SqlServer、PgSQL、Sqlite、Oracle、Ibase、Mongo,也包括对PDO的支 持。 如果应用需要使用数据库,必须配置数据库连接信息,数据库的配置文件有多种定义方式。 一、全局配置定义 常用的配置方式是在应用配置文件或者模块配置文件中添加下面的配置参数: 数据库的类型由DB_TYPE参数设置。 下面是目前支持的数据库设置: DB_TYPE设置 支持的数据库类型 mysql或mysqli mysql pgsql pgsql sqlite sqlite mssql 或sqlsrv sqlserver oracle oracle ibase ibase 6. ); 7. } 1. //数据库配置信息 2. 'DB_TYPE' => 'mysql', // 数据库类型 3. 'DB_HOST' => 'localhost', // 服务器地址 4. 'DB_NAME' => 'thinkphp', // 数据库名 5. 'DB_USER' => 'root', // 用户名 6. 'DB_PWD' => '123456', // 密码 7. 'DB_PORT' => 3306, // 端口 8. 'DB_PREFIX' => 'think_', // 数据库表前缀 9. 'DB_CHARSET'=> 'utf8', // 字符集 - 101 - mongo mongo PDO PDO支持的所有数据库 如果DB_TYPE使用 PDO 类型的话,数据库类型则由DB_DSN配置决定。 或者采用如下配置 使用DB_DSN方式定义可以简化配置参数,DSN参数格式为: 数据库类型://用户名:密码@数据库地址:数据库端口/数据库名#字符集 字符集设置需要3.2.1版本以上有效,字符集如果没有设置的话,默认为utf8。 如果两种配置参数同时存在的话, DB_DSN 配置参数优先。 注意:如果要设置分布式数据库,暂时不支持DB_DSN方式配置。 如果采用PDO驱动的话,则必须首先配置**DB_TYPE **为 pdo ,然后还需要单独配置其他参数,例如: 注意:PDO方式的DB_DSN配置格式有所区别,根据不同的数据库类型设置有所不同,具体可以参 考PHP手册。 配置文件定义的数据库连接信息一般是系统默认采用的,因为一般一个应用的数据库访问配置是相同的。 该方法系统在连接数据库的时候会自动获取,无需手动连接。 可以对每个模块定义不同的数据库连接信息,如果开启了调试模式的话,还可以在不同的应用状态的配置 文件里面定义独立的数据库配置信息。 二、模型类定义 1. 'DB_DSN' => 'mysql://root:123456@localhost:3306/thinkphp#utf8' 1. //PDO连接方式 2. 'DB_TYPE' => 'pdo', // 数据库类型 3. 'DB_USER' => 'root', // 用户名 4. 'DB_PWD' => '', // 密码 5. 'DB_PREFIX' => 'think_', // 数据库表前缀 6. 'DB_DSN' => 'mysql:host=localhost;dbname=thinkphp;charset=UTF-8' - 102 - 如果在某个模型类里面定义了 connection 属性的话,则实例化该自定义模型的时候会采用定义的数据库 连接信息,而不是配置文件中设置的默认连接信息,通常用于某些数据表位于当前数据库连接之外的其它 数据库,例如: 也可以采用DSN方式定义,例如: 如果我们已经在配置文件中配置了额外的数据库连接信息,例如: 1. //在模型里单独设置数据库连接信息 2. namespace Home\Model; 3. use Think\Model; 4. class UserModel extends Model{ 5. protected $connection = array( 6. 'db_type' => 'mysql', 7. 'db_user' => 'root', 8. 'db_pwd' => '1234', 9. 'db_host' => 'localhost', 10. 'db_port' => '3306', 11. 'db_name' => 'thinkphp', 12. 'db_charset' => 'utf8', 13. ); 14. } 1. //在模型里单独设置数据库连接信息 2. namespace Home\Model; 3. use Think\Model; 4. class UserModel extends Model{ 5. //或者使用DSN定义 6. protected $connection = 'mysql://root:1234@localhost:3306/thinkphp#utf8'; 7. } 1. //数据库配置1 2. 'DB_CONFIG1' => array( 3. 'db_type' => 'mysql', 4. 'db_user' => 'root', 5. 'db_pwd' => '1234', 6. 'db_host' => 'localhost', 7. 'db_port' => '3306', - 103 - 那么,我们可以把模型类的属性定义改为: 三、实例化定义 除了在模型定义的时候指定数据库连接信息外,我们还可以在实例化的时候指定数据库连接信息,例如: 如果采用的是M方法实例化模型的话,也可以支持传入不同的数据库连接信息,例如: 表示实例化User模型,连接的是demo数据库的other_user表,采用的连接信息是第三个参数配置的。如 果我们在项目配置文件中已经配置了 DB_CONFIG2 的话,也可以采用: 需要注意的是,ThinkPHP的数据库连接的惰性的,所以并不是在实例化的时候就连接数据库,而是 在有实际的数据操作的时候才会去连接数据库(额外的情况是,在系统第一次实例化模型的时候, 会自动连接数据库获取相关模型类对应的数据表的字段信息)。 8. 'db_name' => 'thinkphp', 9. 'db_charset'=> 'utf8', 10. ), 11. //数据库配置2 12. 'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp#utf8'; 1. //在模型里单独设置数据库连接信息 2. namespace Home\Model; 3. use Think\Model; 4. class UserModel extends Model{ 5. //调用配置文件中的数据库配置1 6. protected $connection = 'DB_CONFIG1'; 7. } 1. //在模型里单独设置数据库连接信息 2. namespace Home\Model; 3. use Think\Model; 4. class InfoModel extends Model{ 5. //调用配置文件中的数据库配置1 6. protected $connection = 'DB_CONFIG2'; 7. } 1. $User = M('User','other_','mysql://root:1234@localhost/demo#utf8'); 1. $User = M('User','other_','DB_CONFIG2'); - 104 - 切换数据库 除了在预先定义数据库连接和实例化的时候指定数据库连接外,我们还可以在模型操作过程中动态的切换 数据库,支持切换到相同和不同的数据库类型。用法很简单, 只需要调用Model类的db方法,用法: Model->db("数据库编号","数据库配置"); 数据库编号用数字格式,对于已经调用过的数据库连接,是不需要再传入数据库连接信息的,系统会自动 记录。对于默认的数据库连接,内部的数据库编号是0,因此为了避免冲突,请不要再次定义数据库编号 为0的数据库配置。 数据库配置的定义方式和模型定义connection属性一样,支持数组、字符串以及调用配置参数三种格 式。 Db方法调用后返回当前的模型实例,直接可以继续进行模型的其他操作,所以该方法可以在查询的过程 中动态切换,例如: 该方法添加了一个编号为1的数据库连接,并自动切换到当前的数据库连接。 当第二次切换到相同的数据库的时候,就不需要传入数据库连接信息了,可以直接使用: 如果需要切换到默认的数据库连接,只需要调用: 如果我们已经在项目配置中定义了其他的数据库连接信息,例如: 1. $this->db(1,"mysql://root:123456@localhost:3306/test")->query("查询SQL"); 1. $this->db(1)->query("查询SQL"); 1. $this->db(0); 1. //数据库配置1 2. 'DB_CONFIG1' = array( 3. 'db_type' => 'mysql', 4. 'db_user' => 'root', 5. 'db_pwd' => '1234', 6. 'db_host' => 'localhost', 7. 'db_port' => '3306', 8. 'db_name' => 'thinkphp' 9. ), 10. //数据库配置2 11. 'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp'; - 105 - 分布式数据库支持 我们就可以直接在db方法中调用配置进行连接了: 如果切换数据库之后,数据表和当前不一致的话,可以使用table方法指定要操作的数据表: ThinkPHP内置了分布式数据库的支持,包括主从式数据库的读写分离,但是分布式数据库必须是相同的 数据库类型。 配置 DB_DEPLOY_TYPE 为1 可以采用分布式数据库支持。如果采用分布式数据库,定义数据库配置信息 的方式如下: 连接的数据库个数取决于DB_HOST定义的数量,所以即使是两个相同的IP也需要重复定义,但是其他的 参数如果存在相同的可以不用重复定义,例如: 和 等效。 和 1. $this->db(1,"DB_CONFIG1")->query("查询SQL"); 2. $this->db(2,"DB_CONFIG2")->query("查询SQL"); 1. $this->db(1)->table("top_user")->find(); 1. //分布式数据库配置定义 2. 'DB_DEPLOY_TYPE'=> 1, // 设置分布式数据库支持 3. 'DB_TYPE' => 'mysql', //分布式数据库类型必须相同 4. 'DB_HOST' => '192.168.0.1,192.168.0.2', 5. 'DB_NAME' => 'thinkphp', //如果相同可以不用定义多个 6. 'DB_USER' => 'user1,user2', 7. 'DB_PWD' => 'pwd1,pwd2', 8. 'DB_PORT' => '3306', 9. 'DB_PREFIX' => 'think_', 1. 'DB_PORT'=>'3306,3306' 1. 'DB_PORT'=>'3306' 1. 'DB_USER'=>'user1', 2. 'DB_PWD'=>'pwd1', - 106 - 连贯操作 等效。 还可以设置分布式数据库的读写是否分离,默认的情况下读写不分离,也就是每台服务器都可以进行读写 操作,对于主从式数据库而言,需要设置读写分离,通过下面的设置就可以: 在读写分离的情况下,默认第一个数据库配置是主服务器的配置信息,负责写入数据,如果设置 了 DB_MASTER_NUM 参数,则可以支持多个主服务器写入。其它的都是从数据库的配置信息,负责读取数 据,数量不限制。每次连接从服务器并且进行读取操作的时候,系统会随机进行在从服务器中选择。 还可以设置 DB_SLAVE_NO 指定某个服务器进行读操作。 调用模型的CURD操作的话,系统会自动判断当前执行的方法的读操作还是写操作,如果你用的是原生 SQL,那么需要注意系统的默认规则: 写操作必须用模型的execute方法,读操作必须用模型的query方 法,否则会发生主从读写错乱的情况。 注意:主从数据库的数据同步工作不在框架实现,需要数据库考虑自身的同步或者复制机制。 ThinkPHP模型基础类提供的连贯操作方法(也有些框架称之为链式操作),可以有效的提高数据存取的 代码清晰度和开发效率,并且支持所有的CURD操作。 使用也比较简单, 假如我们现在要查询一个User表的满足状态为1的前10条记录,并希望按照用户的创建 时间排序 ,代码如下: 这里的 where 、 order 和 limit 方法就被称之为连贯操作方法,除了select方法必须放到最后一个外 (因为select方法并不是连贯操作方法),连贯操作的方法调用顺序没有先后,例如,下面的代码和上面 的等效: 如果不习惯使用连贯操作的话,还支持直接使用参数进行查询的方式。例如上面的代码可以改写为: 1. 'DB_USER'=>'user1,user1', 2. 'DB_PWD'=>'pwd1,pwd1', 1. 'DB_RW_SEPARATE'=>true, 1. $User->where('status=1')->order('create_time')->limit(10)->select(); 1. $User->order('create_time')->limit(10)->where('status=1')->select(); 1. $User->select(array('order'=>'create_time','where'=>'status=1','limit'=>'10')); - 107 - 使用数组参数方式的话,索引的名称就是连贯操作的方法名称。其实不仅仅是查询方法可以使用连贯操 作,包括所有的CURD方法都可以使用,例如: 连贯操作通常只有一个参数,并且仅在当此查询或者操作有效,完成后会自动清空连贯操作的所有传值 (有个别特殊的连贯操作有多个参数,并且会记录当前的传值)。简而言之,连贯操作的结果不会带入以 后的查询。 系统支持的连贯操作方法有: 连贯操作 作用 支持的参数类型 where* 用于查询或者更新条件的定义 字符串、数组和对象 table 用于定义要操作的数据表名称 字符串和数组 alias 用于给当前数据表定义别名 字符串 data 用于新增或者更新数据之前的数据对象赋值 数组和对象 field 用于定义要查询的字段(支持字段排除) 字符串和数组 order 用于对结果排序 字符串和数组 limit 用于限制查询结果数量 字符串和数字 page 用于查询分页(内部会转换成limit) 字符串和数字 group 用于对查询的group支持 字符串 having 用于对查询的having支持 字符串 join* 用于对查询的join支持 字符串和数组 union* 用于对查询的union支持 字符串、数组和对象 distinct 用于查询的distinct支持 布尔值 lock 用于数据库的锁机制 布尔值 cache 用于查询缓存 支持多个参数 relation 用于关联查询(需要关联模型支持) 字符串 result 用于返回数据转换 字符串 1. $User->where('id=1')->field('id,name,email')->find(); 2. $User->where('status=1 and id=1')->delete(); - 108 - WHERE validate 用于数据自动验证 数组 auto 用于数据自动完成 数组 filter 用于数据过滤 字符串 scope* 用于命名范围 字符串、数组 bind* 用于数据绑定操作 数组或多个参数 token 用于令牌验证 布尔值 comment 用于SQL注释 字符串 所有的连贯操作都返回当前的模型实例对象(this),其中带*标识的表示支持多次调用。 where方法的用法是ThinkPHP查询语言的精髓,也是ThinkPHP ORM的重要组成部分和亮点所在,可以 完成包括普通查询、表达式查询、快捷查询、区间查询、组合查询在内的查询操作。where方法的参数支 持字符串和数组,虽然也可以使用对象但并不建议。 字符串条件 使用字符串条件直接查询和操作,例如: 最后生成的SQL语句是 如果使用3.1以上版本的话,使用字符串条件的时候,建议配合预处理机制,确保更加安全,例如: 或者使用: 如果 $id 变量来自用户提交或者URL地址的话,如果传入的是非数字类型,则会强制格式化为数字格式后 1. $User = M("User"); // 实例化User对象 2. $User->where('type=1 AND status=1')->select(); 1. SELECT * FROM think_user WHERE type=1 AND status=1 1. $Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx))->s elect(); 1. $Model->where("id=%d and username='%s' and xx='%f'",$id,$username,$xx)->select() ; - 109 - 进行查询操作。 字符串预处理格式类型支持指定数字、字符串等,具体可以参考vsprintf方法的参数说明。 数组条件 数组条件的where用法是ThinkPHP推荐的用法。 普通查询 最简单的数组查询方式如下: 最后生成的SQL语句是 表达式查询 上面的查询条件仅仅是一个简单的相等判断,可以使用查询表达式支持更多的SQL查询语法,查询表达式 的使用格式: 表达式不分大小写,支持的查询表达式有下面几种,分别表示的含义是: 表达式 含义 EQ 等于(=) NEQ 不等于(<>) GT 大于(>) EGT 大于等于(>=) LT 小于(<) ELT 小于等于(<=) 1. $User = M("User"); // 实例化User对象 2. $map['name'] = 'thinkphp'; 3. $map['status'] = 1; 4. // 把查询条件传入查询方法 5. $User->where($map)->select(); 1. SELECT * FROM think_user WHERE `name`='thinkphp' AND status=1 1. $map['字段1'] = array('表达式','查询条件1'); 2. $map['字段2'] = array('表达式','查询条件2'); 3. $Model->where($map)->select(); // 也支持 - 110 - TABLE LIKE 模糊查询 [NOT] BETWEEN (不在)区间查询 [NOT] IN (不在)IN 查询 EXP 表达式查询,支持SQL语法 多次调用 3.1.3版本开始,where方法支持多次调用,但字符串条件只能出现一次,例如: 多次的数组条件表达式会最终合并,但字符串条件则只支持一次。 更多的查询用法,可以参考查询语言部分。 table方法也属于模型类的连贯操作方法之一,主要用于指定操作的数据表。 用法 一般情况下,操作模型的时候系统能够自动识别当前对应的数据表,所以,使用table方法的情况通常是 为了: 1. 切换操作的数据表; 2. 对多表进行操作; 例如: 也可以在table方法中指定数据库,例如: table方法指定的数据表需要完整的表名,但可以采用下面的方式简化数据表前缀的传入,例如: 会自动获取当前模型对应的数据表前缀来生成 think_user 数据表名称。 1. $map['a'] = array('gt',1); 2. $where['b'] = 1; 3. $Model->where($map)->where($where)->where('status=1')->select(); 1. $Model->table('think_user')->where('status>1')->select(); 1. $Model->table('db_name.think_user')->where('status>1')->select(); 1. $Model->table('__USER__')->where('status>1')->select(); - 111 - ALIAS DATA 需要注意的是table方法不会改变数据库的连接,所以你要确保当前连接的用户有权限操作相应的数据库 和数据表。 切换数据表后,系统会自动重新获取切换后的数据表的字段缓存信息。 如果需要对多表进行操作,可以这样使用: 为了尽量避免和mysql的关键字冲突,可以建议使用数组方式定义,例如: 使用数组方式定义的优势是可以避免因为表名和关键字冲突而出错的情况。 一般情况下,无需调用table 方法,默认会自动获取当前模型对应或者定义的数据表。 alias用于设置当前数据表的别名,便于使用其他的连贯操作例如join方法等。 示例: 最终生成的SQL语句类似于: data方法也是模型类的连贯操作方法之一,用于设置当前要操作的数据对象的值。 写操作 通常情况下我们都是通过create方法或者赋值的方式生成数据对象,然后写入数据库,例如: 1. $Model->field('user.name,role.title') 2. ->table('think_user user,think_role role') 3. ->limit(10)->select(); 1. $Model->field('user.name,role.title') 2. ->table(array('think_user'=>'user','think_role'=>'role')) 3. ->limit(10)->select(); 1. $Model = M('User'); 2. $Model->alias('a')->join('__DEPT__ b ON b.user_id= a.id')->select(); 1. SELECT * FROM think_user a INNER JOIN think_dept b ON b.user_id= a.id 1. $Model = D('User'); 2. $Model->create(); 3. // 这里略过具体的自动生成和验证判断 - 112 - 又或者直接对数据对象赋值,例如: 那么data方法则是直接生成要操作的数据对象,例如: 注意:如果我们同时使用create方法和data创建数据对象的话,则最后调用的方法有效。 data方法支持数组、对象和字符串,对象方式如下: 字符串方式用法如下: 也可以直接在add方法中传入数据对象来新增数据,例如: 但是这种方式data参数只能使用数组。 当然data方法也可以用于更新数据,例如: 4. $Model->add(); 1. $Model = M('User'); 2. $Model->name = '流年'; 3. $Model->email = 'thinkphp@qq.com'; 4. $Model->add(); 1. $Model = M('User'); 2. $data['name'] = '流年'; 3. $data['email'] = 'thinkphp@qq.com'; 4. $Model->data($data)->add(); 1. $Model = M('User'); 2. $obj = new \stdClass; 3. $obj->name = '流年'; 4. $obj->email = 'thinkphp@qq.com'; 5. $Model->data($obj)->add(); 1. $Model = M('User'); 2. $data = 'name=流年&email=thinkphp@qq.com'; 3. $Model->data($data)->add(); 1. $Model = M('User'); 2. $data['name'] = '流年'; 3. $data['email'] = 'thinkphp@qq.com'; 4. $Model->add($data); - 113 - FIELD 当然我们也可以直接这样用: 同样,此时data参数只能传入数组。 在调用save方法更新数据的时候 会自动判断当前的数据对象里面是否有主键值存在,如果有的话会自动 作为更新条件。也就是说,下面的用法和上面等效: 读操作 除了写操作外,data方法还可以用于读取当前的数据对象,例如: field方法属于模型的连贯操作方法之一,主要目的是标识要返回或者操作的字段,可以用于查询和写入操 作。 1、用于查询 1. $Model = M('User'); 2. $data['id'] = 8; 3. $data['name'] = '流年'; 4. $data['email'] = 'thinkphp@qq.com'; 5. $Model->data($data)->save(); 1. $Model = M('User'); 2. $data['id'] = 8; 3. $data['name'] = '流年'; 4. $data['email'] = 'thinkphp@qq.com'; 5. $Model->save($data); 1. $Model = M('User'); 2. $data['name'] = '流年'; 3. $data['email'] = 'thinkphp@qq.com'; 4. $Model->data($data)->where('id=8')->save(); 1. $User = M('User'); 2. $map['name'] = '流年'; 3. $User->where($map)->find(); 4. // 读取当前数据对象 5. $data = $User->data(); - 114 - 指定字段 在查询操作中field方法是使用最频繁的。 这里使用field方法指定了查询的结果集中包含id,title,content三个字段的值。执行的SQL相当于: 可以给某个字段设置别名,例如: 执行的SQL语句相当于: 使用SQL函数 可以在field方法中直接使用函数,例如: 执行的SQL相当于: 当然,除了select方法之外,所有的查询方法,包括find等都可以使用field方法,这里只是以select为例 说明。 使用数组参数 field方法的参数可以支持数组,例如: 最终执行的SQL和前面用字符串方式是等效的。 数组方式的定义可以为某些字段定义别名,例如: 执行的SQL相当于: 对于一些更复杂的字段要求,数组的优势则更加明显,例如: 1. $Model->field('id,title,content')->select(); 1. SELECT id,title,content FROM table 1. $Model->field('id,nickname as name')->select(); 1. SELECT id,nickname as name FROM table 1. $Model->field('id,SUM(score)')->select(); 1. SELECT id,SUM(score) FROM table 1. $Model->field(array('id','title','content'))->select(); 1. $Model->field(array('id','nickname'=>'name'))->select(); 1. SELECT id,nickname as name FROM table - 115 - 执行的SQL相当于: 获取所有字段 如果有一个表有非常多的字段,需要获取所有的字段(这个也许很简单,因为不调用field方法或者直接使 用空的field方法都能做到): 上面三个用法是等效的,都相当于执行SQL: 但是这并不是我说的获取所有字段,我希望显式的调用所有字段(对于对性能要求比较高的系统,这个要 求并不过分,起码是一个比较好的习惯),那么OK,仍然很简单,下面的用法可以完成预期的作用: fied(true) 的用法会显式的获取数据表的所有字段列表,哪怕你的数据表有100个字段。 字段排除 如果我希望获取排除数据表中的 content 字段(文本字段的值非常耗内存)之外的所有字段值,我们就 可以使用field方法的排除功能,例如下面的方式就可以实现所说的功能: 则表示获取除了content之外的所有字段,要排除更多的字段也可以: 2、用于写入 除了查询操作之外,field方法还有一个非常重要的安全功能--字段合法性检测(注意:该功能3.1版本开 始才能支持)。field方法结合create方法使用就可以完成表单提交的字段合法性检测,如果我们在表单提 交的处理方法中使用了: 1. $Model->field(array('id','concat(name,'-',id)'=>'truename','LEFT(title,7)'=>'sub _title'))->select(); 1. SELECT id,concat(name,'-',id) as truename,LEFT(title,7) as sub_title FROM table 1. $Model->select(); 2. $Model->field()->select(); 3. $Model->field('*')->select(); 1. SELECT * FROM table 1. $Model->field(true)->select(); 1. $Model->field('content',true)->select(); 1. $Model->field('user_id,content',true)->select(); 2. //或者用 3. $Model->field(array('user_id','content'),true)->select(); - 116 - ORDER LIMIT 即表示表单中的合法字段只有 title , email 和 content 字段,无论用户通过什么手段更改或者添加了 浏览器的提交字段,都会直接屏蔽。因为,其他是所有字段我们都不希望由用户提交来决定,你可以通过 自动完成功能定义额外的字段写入。 order方法属于模型的连贯操作方法之一,用于对操作的结果排序。 用法如下: 注意:连贯操作方法没有顺序,可以在select方法调用之前随便改变调用顺序。 支持对多个字段的排序,例如: 如果没有指定desc或者asc排序规则的话,默认为asc。 如果你的字段和mysql关键字有冲突,那么建议采用数组方式调用,例如: limit方法也是模型类的连贯操作方法之一,主要用于指定查询和操作的数量,特别在分页查询的时候使用 较多。ThinkPHP的limit方法可以兼容所有的数据库驱动类的。 限制结果数量 例如获取满足要求的10个用户,如下调用即可: limit方法也可以用于写操作,例如更新满足要求的3条数据: 1. $Model->field('title,email,content')->create(); 1. $Model->where('status=1')->order('id desc')->limit(5)->select(); 1. $Model->where('status=1')->order('id desc,status')->limit(5)->select(); 1. $Model->where('status=1')->order(array('order','id'=>'desc'))->limit(5)->select( ); 1. $User = M('User'); 2. $User->where('status=1')->field('id,name')->limit(10)->select(); 1. $User = M('User'); - 117 - PAGE 分页查询 用于文章分页查询是limit方法比较常用的场合,例如: 表示查询文章数据,从第10行开始的25条数据(可能还取决于where条件和order排序的影响 这个暂且 不提)。 在3.1版本后,你也可以这样使用: 对于大数据表,尽量使用limit限制查询结果,否则会导致很大的内存开销和性能问题。 page方法也是模型的连贯操作方法之一,是完全为分页查询而诞生的一个人性化操作方法。 我们在前面已经了解了关于limit方法用于分页查询的情况,而page方法则是更人性化的进行分页查询的 方法,例如还是以文章列表分页为例来说,如果使用limit方法,我们要查询第一页和第二页(假设我们每 页输出10条数据)写法如下: 虽然利用扩展类库中的分页类Page可以自动计算出每个分页的limit参数,但是如果要自己写就比较费力 了,如果用page方法来写则简单多了,例如: 显而易见的是,使用page方法你不需要计算每个分页数据的起始位置,page方法内部会自动计算。 3.1版本以后,page方法也支持2个参数的写法,例如: 2. $User->where('score=100')->limit(3)->save(array('level'=>'A')); 1. $Article = M('Article'); 2. $Article->limit('10,25')->select(); 1. $Article = M('Article'); 2. $Article->limit(10,25)->select(); 1. $Article = M('Article'); 2. $Article->limit('0,10')->select(); // 查询第一页数据 3. $Article->limit('10,10')->select(); // 查询第二页数据 1. $Article = M('Article'); 2. $Article->page('1,10')->select(); // 查询第一页数据 3. $Article->page('2,10')->select(); // 查询第二页数据 1. $Article->page(1,10)->select(); - 118 - GROUP HAVING 和 等效。 page方法还可以和limit方法配合使用,例如: 当page方法只有一个值传入的时候,表示第几页,而limit方法则用于设置每页显示的数量,也就是说上 面的写法等同于: GROUP方法也是连贯操作方法之一,通常用于结合合计函数,根据一个或多个列对结果集进行分组 。 group方法只有一个参数,并且只能使用字符串。 例如,我们都查询结果按照用户id进行分组统计: 生成的SQL语句是: 也支持对多个字段进行分组,例如: 生成的SQL语句是: HAVING方法也是连贯操作之一,用于配合group方法完成从分组的结果中筛选(通常是聚合条件)数 据。 having方法只有一个参数,并且只能使用字符串,例如: 1. $Article->page('1,10')->select(); 1. $Article->limit(25)->page(3)->select(); 1. $Article->page('3,25')->select(); 1. $this->field('username,max(score)')->group('user_id')->select(); 1. SELECT username,max(score) FROM think_score GROUP BY user_id 1. $this->field('username,max(score)')->group('user_id,test_time')->select(); 1. SELECT username,max(score) FROM think_score GROUP BY user_id,test_time 1. $this->field('username,max(score)')->group('user_id')->having('count(test_time)> - 119 - JOIN 生成的SQL语句是: JOIN方法也是连贯操作方法之一,用于根据两个或多个表中的列之间的关系,从这些表中查询数据。 join通常有下面几种类型,不同类型的join操作会影响返回的数据结果。 INNER JOIN: 如果表中有至少一个匹配,则返回行,等同于 JOIN LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行 RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行 FULL JOIN: 只要其中一个表中存在匹配,就返回行 join方法可以支持以上四种类型,例如: join方法支持多次调用,但指定的数据表必须是全称,但我们可以这样来定义: __WORK__ 和 __CARD__ 在最终解析的时候会转换为 think_work 和 think_card 。 默认采用INNER JOIN 方式,如果需要用其他的JOIN方式,可以改成 或者使用: 3')->select(); 1. SELECT username,max(score) FROM think_score GROUP BY user_id HAVING count(test_t ime)>3 1. $Model = M('Artist'); 2. $Model 3. ->join('think_work ON think_artist.id = think_work.artist_id') 4. ->join('think_card ON think_artist.card_id = think_card.id') 5. ->select(); 1. $Model 2. ->join('__WORK__ ON __ARTIST__.id = __WORK__.artist_id') 3. ->join('__CARD__ ON __ARTIST__.card_id = __CARD__.id') 4. ->select(); 1. $Model->join('RIGHT JOIN __WORK__ ON __ARTIST__.id = __WORK__.artist_id')->selec t(); 1. $Model->join('__WORK__ ON __artist__.id = __WORK__.artist_id','RIGHT')->select() - 120 - UNION join方法的第二个参数支持的类型包括:INNER LEFT RIGHT FULL。 如果join方法的参数用数组的话,只能使用一次join方法,并且不能和字符串方式混合使用。 例如: 使用数组方式的情况下,第二个参数无效。因此必须在字符串中显式定义join类型,例如: UNION操作用于合并两个或多个 SELECT 语句的结果集。 使用示例: 数组用法: 或者 ; 1. join(array(' __WORK__ ON __ARTIST__.id = __WORK__.artist_id','__CARD__ ON __ARTI ST__.card_id = __CARD__.id')) 1. join(array(' LEFT JOIN __WORK__ ON __ARTIST__.id = __WORK__.artist_id','RIGHT JO IN __CARD__ ON __ARTIST__.card_id = __CARD__.id')) 1. $Model->field('name') 2. ->table('think_user_0') 3. ->union('SELECT name FROM think_user_1') 4. ->union('SELECT name FROM think_user_2') 5. ->select(); 1. $Model->field('name') 2. ->table('think_user_0') 3. ->union(array('field'=>'name','table'=>'think_user_1')) 4. ->union(array('field'=>'name','table'=>'think_user_2')) 5. ->select(); 1. $Model->field('name') 2. ->table('think_user_0') 3. ->union(array('SELECT name FROM think_user_1','SELECT name FROM think_user _2')) 4. ->select(); - 121 - DISTINCT LOCK 支持UNION ALL 操作,例如: 或者 每个union方法相当于一个独立的SELECT语句。 注意:UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时, 每条 SELECT 语句中的列的顺序必须相同。 DISTINCT 方法用于返回唯一不同的值 。 例如: 生成的SQL语句是: SELECT DISTINCT name FROM think_user distinct方法的参数是一个布尔值。 Lock方法是用于数据库的锁机制,如果在查询或者执行操作的时候使用: 就会自动在生成的SQL语句最后加上 FOR UPDATE 或者 FOR UPDATE NOWAIT (Oracle数据库)。 1. $Model->field('name') 2. ->table('think_user_0') 3. ->union('SELECT name FROM think_user_1',true) 4. ->union('SELECT name FROM think_user_2',true) 5. ->select(); 1. $Model->field('name') 2. ->table('think_user_0') 3. ->union(array('SELECT name FROM think_user_1','SELECT name FROM think_user _2'),true) 4. ->select(); 1. $Model->distinct(true)->field('name')->select(); 1. lock(true); - 122 - CACHE COMMENT cache方法用于查询缓存操作,也是连贯操作方法之一。 cache可以用于 select 、 find 和 getField 方法,以及其衍生方法,使用cache方法后,在缓存有效 期之内不会再次进行数据库查询操作,而是直接获取缓存中的数据,关于数据缓存的类型和设置可以参考 缓存部分。 下面举例说明,例如,我们对find方法使用cache方法如下: 第一次查询结果会被缓存,第二次查询相同的数据的时候就会直接返回缓存中的内容,而不需要再次进行 数据库查询操作。 默认情况下, 缓存有效期和缓存类型是由DATA_CACHE_TIME和DATA_CACHE_TYPE配置参数决定的, 但cache方法可以单独指定,例如: 表示对查询结果使用xcache缓存,缓存有效期60秒。 cache方法可以指定缓存标识: 指定查询缓存的标识可以使得查询缓存更有效率。 这样,在外部就可以通过S方法直接获取查询缓存的数据,例如: COMMENT方法 用于在生成的SQL语句中添加注释内容,例如: 1. $Model = M('User'); 2. $Model->where('id=5')->cache(true)->find(); 1. $Model = M('User'); 2. $Model->cache(true,60,'xcache')->find(); 1. $Model = M('User'); 2. $Model->cache('key',60)->find(); 1. $Model = M('User'); 2. $result = $Model->cache('key',60)->find(); 3. $data = S('key'); - 123 - RELATION 命名范围 最终生成的SQL语句是: 在应用开发过程中,使用最多的操作还是数据查询操作,凭借ThinkPHP的连贯操作的特性,可以使得查 询操作变得更优雅和清晰,命名范围功能则是给模型操作定义了一系列的封装,让你更方便的操作数据。 命名范围功能的优势在于可以一次定义多次调用,并且在项目中也能起到分工配合的规范,避免开发人员 在写CURD操作的时候出现问题,项目经理只需要合理的规划命名范围即可。 定义属性 要使用命名范围功能,主要涉及到模型类的 _scope 属性定义和 scope 连贯操作方法的使用。 我们首先定义_scope属性: 1. $this->comment('查询考试前十名分数') 2. ->field('username,score') 3. ->limit(10) 4. ->order('score desc') 5. ->select(); 1. SELECT username,score FROM think_score ORDER BY score desc LIMIT 10 /* 查询考试 前十名分数 */ 1. namespace Home\Model; 2. use Think\Model; 3. class NewsModel extends Model { 4. protected $_scope = array( 5. // 命名范围normal 6. 'normal'=>array( 7. 'where'=>array('status'=>1), 8. ), 9. // 命名范围latest 10. 'latest'=>array( 11. 'order'=>'create_time DESC', 12. 'limit'=>10, 13. ), - 124 - _scope 属性是一个数组,每个数组项表示定义一个命名范围,命名范围的定义格式为: 命名范围标识:可以是任意的字符串,用于标识当前定义的命名范围名称。 命名范围支持的属性包括: 属性 描述 where 查询条件 field 查询字段 order 结果排序 table 查询表名 limit 结果限制 page 结果分页 having having查询 group group查询 lock 查询锁定 distinct 唯一查询 cache 查询缓存 每个命名范围的定义可以包括这些属性中一个或者多个。 方法调用 属性定义完成后,接下来就是使用 scope 方法进行命名范围的调用了,每调用一个命名范围,就相当于 执行了命名范围中定义的相关操作选项对应的连贯操作方法。 14. ); 15. } 1. '命名范围标识'=>array( 2. '属性1'=>'值1', 3. '属性2'=>'值2', 4. ... 5. ) - 125 - 调用某个命名范围 最简单的调用方式就直接调用某个命名范围,例如: 生成的SQL语句分别是: 调用多个命名范围 也可以支持同时调用多个命名范围定义,例如: 或者简化为: 生成的SQL都是: 如果两个命名范围的定义存在冲突,则后面调用的命名范围定义会覆盖前面的相同属性的定义。 如果调用的命名范围标识不存在,则会忽略该命名范围,例如: 上面的命名范围中new是不存在的,因此只有normal命名范围生效,生成的SQL语句是: 默认命名范围 系统支持默认命名范围功能,如果你定义了一个default命名范围,例如: 1. $Model = D('News'); // 这里必须使用D方法 因为命名范围在模型里面定义 2. $Model->scope('normal')->select(); 3. $Model->scope('latest')->select(); 1. SELECT * FROM think_news WHERE status=1 2. SELECT * FROM think_news ORDER BY create_time DESC LIMIT 10 1. $Model->scope('normal')->scope('latest')->select(); 1. $Model->scope('normal,latest')->select(); 1. SELECT * FROM think_news WHERE status=1 ORDER BY create_time DESC LIMIT 10 1. $Model->scope('normal,new')->select(); 1. SELECT * FROM think_news WHERE status=1 1. protected $_scope = array( 2. // 默认的命名范围 3. 'default'=>array( 4. 'where'=>array('status'=>1), 5. 'limit'=>10, - 126 - 那么调用default命名范围可以直接使用: 而无需再传入命名范围标识名 虽然这两种方式是等效的。 命名范围调整 如果你需要在normal命名范围的基础上增加额外的调整,可以使用: 生成的SQL语句是: 当然,也可以在两个命名范围的基础上进行调整,例如: 生成的SQL是: 自定义命名范围 又或者,干脆不用任何现有的命名范围,我直接传入一个命名范围: 这样,生成的SQL变成: 与连贯操作混合使用 命名范围一样可以和之前的连贯操作混合使用,例如定义了命名范围_scope属性: 6. ), 7. ); 1. $Model->scope()->select(); 1. $Model->scope('default')->select(); 1. $Model->scope('normal',array('limit'=>5))->select(); 1. SELECT * FROM think_news WHERE status=1 LIMIT 5 1. $Model->scope('normal,latest',array('limit'=>5))->select(); 1. SELECT * FROM think_news WHERE status=1 ORDER BY create_time DESC LIMIT 5 1. $Model->scope(array('field'=>'id,title','limit'=>5,'where'=>'status=1','order'=> 'create_time DESC'))->select(); 1. SELECT id,title FROM think_news WHERE status=1 ORDER BY create_time DESC LIMIT 5 1. protected $_scope = array( 2. 'normal'=>array( 3. 'where'=>array('status'=>1), - 127 - CURD操作 然后在使用的时候,可以这样调用: 这样,生成的SQL变成: 如果定义的命名范围和连贯操作的属性有冲突,则后面调用的会覆盖前面的。 如果是这样调用: 生成的SQL则是: 动态调用 除了采用scope方法调用命名范围外,我们还支持直接调用命名范围名称的方式来动态调用,例如: 查询操作也可以采用: 的方式调用。 normal(array('limit'=>5)) 表示调用normal命名范围,并且传入额外 的 array('limit'=>5) 参数。 由于采用的是 __call 魔术方法机制,因此这样调用的前提是你定义的命名范围名称没有和现有操作方法 冲突。 ThinkPHP提供了灵活和方便的数据操作方法,对数据库操作的四个基本操作(CURD):创建、更新、 读取和删除的实现是最基本的,也是必须掌握的,在这基础之上才能熟悉更多实用的数据操作方法。 CURD操作通常是可以和连贯操作配合完成的。 4. 'field'=>'id,title', 5. 'limit'=>10, 6. ), 7. ); 1. $Model->scope('normal')->limit(8)->order('id desc')->select(); 1. SELECT id,title FROM think_news WHERE status=1 ORDER BY id desc LIMIT 8 1. $Model->limit(8)->scope('normal')->order('id desc')->select(); 1. SELECT id,title FROM think_news WHERE status=1 ORDER BY id desc LIMIT 10 1. $Model->scope('normal',array('limit'=>5))->select(); 1. $Model->normal(array('limit'=>5))->select(); - 128 - 数据创建 在进行数据操作之前,我们往往需要手动创建需要的数据,例如对于提交的表单数据: 创建数据对象 ThinkPHP可以帮助你快速地创建数据对象,最典型的应用就是自动根据表单数据创建数据对象,这个优 势在一个数据表的字段非常之多的情况下尤其明显。 很简单的例子: Create方法支持从其它方式创建数据对象,例如,从其它的数据对象,或者数组等 甚至还可以支持从对象创建新的数据对象 创建完成的数据可以直接读取和修改,例如: 1. // 获取表单的POST数据 2. $data['name'] = $_POST['name']; 3. $data['email'] = $_POST['email']; 4. // 更多的表单数据值获取 5. //…… 1. // 实例化User模型 2. $User = M('User'); 3. // 根据表单提交的POST数据创建数据对象 4. $User->create(); 1. $data['name'] = 'ThinkPHP'; 2. $data['email'] = 'ThinkPHP@gmail.com'; 3. $User->create($data); 1. // 从User数据对象创建新的Member数据对象 2. $User = stdClass(); 3. $User->name = 'ThinkPHP'; 4. $User->email = 'ThinkPHP@gmail.com'; 5. $Member = M("Member"); 6. $Member->create($User); 1. $data['name'] = 'ThinkPHP'; 2. $data['email'] = 'ThinkPHP@gmail.com'; - 129 - 数据操作状态 create方法的第二个参数可以指定创建数据的操作状态,默认情况下是自动判断是写入还是更新操作。 也可以显式指定操作状态,例如: 系统内置的数据操作包括 Model::MODEL_INSERT (或者1)和 Model::MODEL_UPDATE (或者2),当没 有指定的时候,系统根据数据源是否包含主键数据来自动判断,如果存在主键数据,就当 成 Model::MODEL_UPDATE 操作。 不同的数据操作状态可以定义不同的数据验证和自动完成机制,所以,你可以自定义自己需要的数据操作 状态,例如,可以设置登录操作的数据状态(假设为3): 事实上,create方法所做的工作远非这么简单,在创建数据对象的同时,完成了一系列的工作,我们来看 下create方法的工作流程就能明白: 步骤 说明 返回 1 获取数据源(默认是POST数组) 2 验证数据源合法性(非数组或者对象会过滤) 失败则返回false 3 检查字段映射 4 判断数据状态(新增或者编辑,指定或者自动判断) 5 数据自动验证 失败则返回false 3. $User->create($data); 4. // 创建完成数据对象后可以直接读取数据 5. echo $User->name; 6. echo $User->email; 7. // 也可以直接修改创建完成的数据 8. $User->name = 'onethink'; // 修改name字段数据 9. $User->status = 1; // 增加新的字段数据 1. $Member = M("User"); 2. // 指定更新数据操作状态 3. $Member->create($_POST,Model::MODEL_UPDATE); 1. $Member = M("User"); 2. // 指定更新数据操作状态 3. $Member->create($_POST,3); - 130 - 6 表单令牌验证 失败则返回false 7 表单数据赋值(过滤非法字段和字符串处理) 8 数据自动完成 9 生成数据对象(保存在内存) 因此,我们熟悉的令牌验证、自动验证和自动完成功能,其实都必须通过create方法才能生效。 如果没有定义自动验证的话,create方法的返回值是创建完成的数据对象数组,例如: 输出结果为: Create方法创建的数据对象是保存在内存中,并没有实际写入到数据库中,直到使用 add 或者 save 方 法才会真正写入数据库。 因此在没有调用add或者save方法之前,我们都可以改变create方法创建的数据对象,例如: 如果只是想简单创建一个数据对象,并不需要完成一些额外的功能的话,可以使用data方法简单的创建数 据对象。 使用如下: 1. $data['name'] = 'thinkphp'; 2. $data['email'] = 'thinkphp@gmail.com'; 3. $data['status'] = 1; 4. $User = M('User'); 5. $data = $User->create($data); 6. dump($data); 1. array (size=3) 2. 'name' => string 'thinkphp' (length=8) 3. 'email' => string 'thinkphp@gmail.com' (length=18) 4. 'status'=> int 1 1. $User = M('User'); 2. $User->create(); //创建User数据对象 3. $User->status = 1; // 设置默认的用户状态 4. $User->create_time = time(); // 设置用户的创建时间 5. $User->add(); // 把用户对象写入数据库 1. // 实例化User模型 2. $User = M('User'); 3. // 创建数据后写入到数据库 - 131 - Data方法也支持传入数组和对象,使用data方法创建的数据对象不会进行自动验证和过滤操作,请自行处 理。但在进行add或者save操作的时候,数据表中不存在的字段以及非法的数据类型(例如对象、数组等 非标量数据)是会自动过滤的,不用担心非数据表字段的写入导致SQL错误的问题。 支持的连贯操作 在执行create方法之前,我们可以调用相关的连贯操作方法,配合完成数据创建操作。 create方法支持的连贯操作方法包括: 连贯操作 作用 支持的参数类型 field 用于定义合法的字段 字符串和数组 validate 用于数据自动验证 数组 auto 用于数据自动完成 数组 token 用于令牌验证 布尔值 更多的用法参考后续的内容。 字段合法性过滤 如果在create方法之前调用field方法,则表示只允许创建指定的字段数据,其他非法字段将会被过滤,例 如: 输出结果为: 4. $data['name'] = 'ThinkPHP'; 5. $data['email'] = 'ThinkPHP@gmail.com'; 6. $User->data($data)->add(); 1. $data['name'] = 'thinkphp'; 2. $data['email'] = 'thinkphp@gmail.com'; 3. $data['status'] = 1; 4. $data['test'] = 'test'; 5. $User = M('User'); 6. $data = $User->field('name,email')->create($data); 7. dump($data); 1. array (size=2) 2. 'name' => string 'thinkphp' (length=8) - 132 - 数据写入 最终只有 name 和 email 字段的数据被允许写入, status 和 test 字段直接被过滤了,哪怕status也是 数据表中的合法字段。 如果我们有自定义模型类,对于数据新增和编辑操作的话,我们还可以直接在模型类里面通过设 置 insertFields 和 updateFields 属性来定义允许的字段,例如: ThinkPHP的数据写入操作使用add方法,使用示例如下: 或者使用data方法连贯操作 如果在add之前已经创建数据对象的话(例如使用了create或者data方法),add方法就不需要再传入数 据了。 使用create方法的例子: 3. 'email' => string 'thinkphp@gmail.com' (length=18) 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model{ 4. protected $insertFields = 'name,email'; // 新增数据的时候允许写入name和email 字段 5. protected $updateFields = 'email'; // 编辑数据的时候只允许写入email字段 6. } 1. $User = M("User"); // 实例化User对象 2. $data['name'] = 'ThinkPHP'; 3. $data['email'] = 'ThinkPHP@gmail.com'; 4. $User->add($data); 1. $User = M("User"); // 实例化User对象 2. $User->data($data)->add(); 1. $User = M("User"); // 实例化User对象 2. // 根据表单提交的POST数据创建数据对象 3. if($User->create()){ 4. $result = $User->add(); // 写入数据到数据库 5. if($result){ 6. // 如果主键是自动增长型 成功后返回值就是最新插入的值 7. $insertId = $result; - 133 - create方法并不算是连贯操作,因为其返回值可能是布尔值,所以必须要进行严格判断。 支持的连贯操作 在执行add方法之前,我们可以调用相关的连贯操作方法,配合完成数据写入操作。 写入操作支持的连贯操作方法包括: 连贯操作 作用 支持的参数类型 table 用于定义要操作的数据表名称 字符串和数组 data 用于指定要写入的数据对象 数组和对象 field 用于定义要写入的字段 字符串和数组 relation 用于关联查询(需要关联模型支持) 字符串 validate 用于数据自动验证 数组 auto 用于数据自动完成 数组 filter 用于数据过滤 字符串 scope 用于命名范围 字符串、数组 bind 用于数据绑定操作 数组 token 用于令牌验证 布尔值 comment 用于SQL注释 字符串 字段过滤 如果写入了数据表中不存在的字段数据,则会被直接过滤,例如: 其中test字段是不存在的,所以写入数据的时候会自动过滤掉。 8. } 9. } 1. $data['name'] = 'thinkphp'; 2. $data['email'] = 'thinkphp@gmail.com'; 3. $data['test'] = 'test'; 4. $User = M('User'); 5. $User->data($data)->add(); - 134 - 在3.2.2版本以上,如果开启调试模式的话,则会抛出异常,提示: 非法数据对象:[test=>test] 如果在add方法之前调用field方法,则表示只允许写入指定的字段数据,其他非法字段将会被过滤,例 如: 最终只有name字段的数据被允许写入,email和test字段直接被过滤了,哪怕email也是数据表中的合法 字段。 字段内容过滤 通过filter方法可以对数据的值进行过滤处理,例如: 写入数据库的时候会把name字段的值转化为 thinkphp 。 filter方法的参数是一个回调类型,支持函数或者闭包定义。 Mysql的特殊操作 如果是mysql数据库的话,系统还提供了针对Mysql的操作方法,包括批量插入数据和替换操作,如: 同时在数据插入时允许更新操作: 其中add方法增加$replace参数(是否添加数据时允许覆盖),true表示覆盖,默认为false 1. $data['name'] = 'thinkphp'; 2. $data['email'] = 'thinkphp@gmail.com'; 3. $data['test'] = 'test'; 4. $User = M('User'); 5. $User->field('name')->data($data)->add(); 1. $data['name'] = 'thinkphp'; 2. $data['email'] = 'thinkphp@gmail.com'; 3. $User = M('User'); 4. $User->data($data)->filter('strip_tags')->add(); 1. // 批量添加数据 2. $dataList[] = array('name'=>'thinkphp','email'=>'thinkphp@gamil.com'); 3. $dataList[] = array('name'=>'onethink','email'=>'onethink@gamil.com'); 4. $User->addAll($dataList); 1. add($data='',$options=array(),$replace=false) - 135 - 数据读取 在ThinkPHP中读取数据的方式很多,通常分为读取数据、读取数据集和读取字段值。 数据查询方法支持的连贯操作方法有: 连贯操作 作用 支持的参数类型 where 用于查询或者更新条件的定义 字符串、数组和对象 table 用于定义要操作的数据表名称 字符串和数组 alias 用于给当前数据表定义别名 字符串 field 用于定义要查询的字段(支持字段排除) 字符串和数组 order 用于对结果排序 字符串和数组 group 用于对查询的group支持 字符串 having 用于对查询的having支持 字符串 join 用于对查询的join支持 字符串和数组 union 用于对查询的union支持 字符串、数组和对象 distinct 用于查询的distinct支持 布尔值 lock 用于数据库的锁机制 布尔值 cache 用于查询缓存 支持多个参数 relation 用于关联查询(需要关联模型支持) 字符串 result 用于返回数据转换 字符串 scope 用于命名范围 字符串、数组 bind 用于数据绑定操作 数组 comment 用于SQL注释 字符串 注意:某些情况下有些连贯操作是无效的,例如limit方法对find方法是无效的。 读取数据 - 136 - 读取数据是指读取数据表中的一行数据(或者关联数据),主要通过 find 方法完成,例如: find方法查询数据的时候可以配合相关的连贯操作方法,其中最关键的则是where方法,如何使用where 方法我们会在查询语言章节中详细描述。 如果查询出错,find方法返回false,如果查询结果为空返回NULL,查询成功则返回一个关联数组(键值 是字段名或者别名)。 如果上面的查询成功的话,会输出: 即使满足条件的数据不止一个,find方法也只会返回第一条记录(可以通过order方法排序后查 询)。 还可以用data方法获取查询后的数据对象(查询成功后) 读取数据集 读取数据集其实就是获取数据表中的多行记录(以及关联数据),使用 select 方法,使用示例: 如果查询出错,select的返回值是false,如果查询结果为空,则返回NULL,否则返回二维数组。 读取字段值 读取字段值其实就是获取数据表中的某个列的多个或者单个数据,最常用的方法是 getField 方法。 1. $User = M("User"); // 实例化User对象 2. // 查找status值为1name值为think的用户数据 3. $data = $User->where('status=1 AND name="thinkphp"')->find(); 4. dump($data); 1. array (size=3) 2. 'name' => string 'thinkphp' (length=8) 3. 'email' => string 'thinkphp@gmail.com' (length=18) 4. 'status'=> int 1 1. $User = M("User"); // 实例化User对象 2. // 查找status值为1name值为think的用户数据 3. $User->where('status=1 AND name="thinkphp"')->find(); 4. dump($User->data()); 1. $User = M("User"); // 实例化User对象 2. // 查找status值为1的用户数据 以创建时间排序 返回10条数据 3. $list = $User->where('status=1')->order('create_time')->limit(10)->select(); - 137 - 示例如下: 默认情况下,当只有一个字段的时候,返回满足条件的数据表中的该字段的第一行的值。 如果需要返回整个列的数据,可以用: 如果传入多个字段的话,默认返回一个关联数组: 这样返回的list是一个数组,键名是用户的id字段的值,键值是用户的昵称nickname。 如果传入多个字段的名称,例如: 返回的是一个二维数组,类似select方法的返回结果,区别的是这个二维数组的键名是用户的id(准确的 说是getField方法的第一个字段名)。 如果我们传入一个字符串分隔符: 那么返回的结果就是一个数组,键名是用户id,键值是 nickname:email 的输出字符串。 getField方法还可以支持限制数量,例如: 1. $User = M("User"); // 实例化User对象 2. // 获取ID为3的用户的昵称 3. $nickname = $User->where('id=3')->getField('nickname'); 1. $User->getField('id',true); // 获取id数组 2. //返回数据格式如array(1,2,3,4,5)一维数组,其中value就是id列的每行的值 1. $User = M("User"); // 实例化User对象 2. // 获取所有用户的ID和昵称列表 3. $list = $User->getField('id,nickname'); 4. //两个字段的情况下返回的是array(`id`=>`nickname`)的关联数组,以id的值为key,nicknam e字段值为value 1. $list = $User->getField('id,nickname,email'); 2. //返回的数组格式是array(`id`=>array(`id`=>value,`nickname`=>value,`email`=>value) )是一个二维数组,key还是id字段的值,但value是整行的array数组,类似于select()方法的结 果遍历将id的值设为数组key 1. $list = $User->getField('id,nickname,email',':'); 1. $this->getField('id,name',5); // 限制返回5条记录 2. $this->getField('id',3); // 获取id数组 限制3条记录 - 138 - 数据更新 可以配合使用order方法使用。更多的查询方法可以参考查询语言章节。 ThinkPHP的数据更新操作包括更新数据和更新字段方法。 更新数据 更新数据使用 save 方法,例如: 也可以改成对象方式来操作: 数据对象赋值的方式,save方法无需传入数据,会自动识别。 注意:save方法的返回值是影响的记录数,如果返回false则表示更新出错,因此一定要用恒等来判断是否 更新失败。 为了保证数据库的安全,避免出错更新整个数据表,如果没有任何更新条件,数据对象本身也不包含主键 字段的话,save方法不会更新任何数据库的记录。 因此下面的代码不会更改数据库的任何记录 除非使用下面的方式: 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $data['name'] = 'ThinkPHP'; 4. $data['email'] = 'ThinkPHP@gmail.com'; 5. $User->where('id=5')->save($data); // 根据条件更新记录 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $User->name = 'ThinkPHP'; 4. $User->email = 'ThinkPHP@gmail.com'; 5. $User->where('id=5')->save(); // 根据条件更新记录 1. $User->save($data); 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $data['id'] = 5; 4. $data['name'] = 'ThinkPHP'; - 139 - 如果id是数据表的主键的话,系统自动会把主键的值作为更新条件来更新其他字段的值。 数据更新方法支持的连贯操作方法有: 连贯操作 作用 支持的参数类型 where 用于查询或者更新条件的定义 字符串、数组和对象 table 用于定义要操作的数据表名称 字符串和数组 alias 用于给当前数据表定义别名 字符串 field 用于定义允许更新的字段 字符串和数组 order 用于对数据排序 字符串和数组 lock 用于数据库的锁机制 布尔值 relation 用于关联更新(需要关联模型支持) 字符串 scope 用于命名范围 字符串、数组 bind 用于数据绑定操作 数组 comment 用于SQL注释 字符串 字段和数据过滤 和add方法一样,save方法支持使用 field 方法过滤字段和 filter 方法过滤数据,例如: 当使用field('email')的时候,只允许更新email字段的值(采用strip_tags方法过滤),name字段的值将 不会被修改。 还有一种方法是通过create或者data方法创建要更新的数据对象,然后进行保存操作,这样save方法的参 数可以不需要传入。 5. $data['email'] = 'ThinkPHP@gmail.com'; 6. $User->save($data); // 根据条件保存修改的数据 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $data['name'] = 'test'; 4. $data['email'] = 'test@gmail.com'; 5. $User->where('id=5')->field('email')->filter('strip_tags')->save($data); // 根据 条件保存修改的数据 - 140 - 数据删除 使用create方法的例子: 更新字段 如果只是更新个别字段的值,可以使用 setField 方法。 使用示例: setField方法支持同时更新多个字段,只需要传入数组即可,例如: 而对于统计字段(通常指的是数字类型)的更新,系统还提供了 setInc 和 setDec 方法。 ThinkPHP删除数据使用delete方法,例如: 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $data['name'] = 'ThinkPHP'; 4. $data['email'] = 'ThinkPHP@gmail.com'; 5. $User->where('id=5')->data($data)->save(); // 根据条件保存修改的数据 1. $User = M("User"); // 实例化User对象 2. // 根据表单提交的POST数据创建数据对象 3. $User->create(); 4. $User->save(); // 根据条件保存修改的数据 1. $User = M("User"); // 实例化User对象 2. // 更改用户的name值 3. $User-> where('id=5')->setField('name','ThinkPHP'); 1. $User = M("User"); // 实例化User对象 2. // 更改用户的name和email的值 3. $data = array('name'=>'ThinkPHP','email'=>'ThinkPHP@gmail.com'); 4. $User-> where('id=5')->setField($data); 1. $User = M("User"); // 实例化User对象 2. $User->where('id=5')->setInc('score',3); // 用户的积分加3 3. $User->where('id=5')->setInc('score'); // 用户的积分加1 4. $User->where('id=5')->setDec('score',5); // 用户的积分减5 5. $User->where('id=5')->setDec('score'); // 用户的积分减1 - 141 - 表示删除主键为5的数据,delete方法可以删除单个数据,也可以删除多个数据,这取决于删除条件,例 如: delete方法的返回值是删除的记录数,如果返回值是false则表示SQL出错,返回值如果为0表示没有删除 任何数据。 也可以用order和limit方法来限制要删除的个数,例如: 为了避免错删数据,如果没有传入任何条件进行删除操作的话,不会执行删除操作,例如: 不会删除任何数据,如果你确实要删除所有的记录,除非使用下面的方式: 数据删除方法支持的连贯操作方法有: 连贯操作 作用 支持的参数类型 where 用于查询或者更新条件的定义 字符串、数组和对象 table 用于定义要操作的数据表名称 字符串和数组 alias 用于给当前数据表定义别名 字符串 order 用于对数据排序 字符串和数组 lock 用于数据库的锁机制 布尔值 relation 用于关联删除(需要关联模型支持) 字符串 scope 用于命名范围 字符串、数组 1. $Form = M('Form'); 2. $Form->delete(5); 1. $User = M("User"); // 实例化User对象 2. $User->where('id=5')->delete(); // 删除id为5的用户数据 3. $User->delete('1,2,5'); // 删除主键为1,2和5的用户数据 4. $User->where('status=0')->delete(); // 删除所有状态为0的用户数据 1. // 删除所有状态为0的5 个用户数据 按照创建时间排序 2. $User->where('status=0')->order('create_time')->limit('5')->delete(); 1. $User = M("User"); // 实例化User对象 2. $User->delete(); 1. $User = M("User"); // 实例化User对象 2. $User->where('1')->delete(); - 142 - ActiveRecord bind 用于数据绑定操作 数组 comment 用于SQL注释 字符串 ThinkPHP实现了ActiveRecords模式的ORM模型,采用了非标准的ORM模型:表映射到类,记录映射 到对象。最大的特点就是使用方便和便于理解(因为采用了对象化),提供了开发的最佳体验,从而达到 敏捷开发的目的。 下面我们用AR模式来换一种方式重新完成CURD操作。 创建数据 如果使用了create方法创建数据对象的话,仍然可以在创建完成后进行赋值 查询记录 AR模式的数据查询比较简单,因为更多情况下面查询条件都是以主键或者某个关键的字段。这种类型的 查询,ThinkPHP有着很好的支持。 先举个最简单的例子,假如我们要查询主键为8的某个用户记录,如 果按照之前的方式,我们可能会使用下面的方法: 1. $User = M("User"); // 实例化User对象 2. // 然后直接给数据对象赋值 3. $User->name = 'ThinkPHP'; 4. $User->email = 'ThinkPHP@gmail.com'; 5. // 把数据对象添加到数据库 6. $User->add(); 1. $User = D("User"); 2. $User->create(); // 创建User数据对象,默认通过表单提交的数据进行创建 3. // 增加或者更改其中的属性 4. $User->status = 1; 5. $User->create_time = time(); 6. // 把数据对象添加到数据库 7. $User->add(); 1. $User = M("User"); // 实例化User对象 2. // 查找id为8的用户数据 3. $User->where('id=8')->find(); - 143 - 用AR模式的话可以直接写成: 如果要根据某个字段查询,例如查询姓名为ThinkPHP的可以用: 这个作为查询语言来说是最为直观的,如果查询成功,查询的结果直接保存在当前的数据对象中,在进行 下一次查询操作之前,我们都可以提取,例如获取查询的结果数据: 如果要查询数据集,可以直接使用: 更新记录 在完成查询后,可以直接修改数据对象然后保存到数据库。 上面这种方式仅仅是示例,不代表保存操作之前一定要先查询。因为下面的方式其实是等效的: 删除记录 可以删除当前查询的数据对象 或者直接根据主键进行删除 1. $User->find(8); 1. $User = M("User"); // 实例化User对象 2. $User->getByName("ThinkPHP"); 1. echo $User->name; 2. echo $User->email; 1. // 查找主键为1、3、8的多个数据 2. $userList = $User->select('1,3,8'); 1. $User->find(1); // 查找主键为1的数据 2. $User->name = 'TOPThink'; // 修改数据对象 3. $User->save(); // 保存当前数据对象 1. $User->id = 1; 2. $User->name = 'TOPThink'; // 修改数据对象 3. $User->save(); // 保存当前数据对象 1. $User->find(2); 2. $User->delete(); // 删除当前的数据对象 1. $User->delete(8); // 删除主键为8的数据 - 144 - 字段映射 ThinkPHP的字段映射功能可以让你在表单中隐藏真正的数据表字段,而不用担心放弃自动创建表单对象 的功能,假设我们的User表里面有username和email字段,我们需要映射成另外的字段,定义方式如 下: 这样,在表单里面就可以直接使用name和mail名称作为表单数据提交了。我们使用 create 方法创建数 据对象的时候,会自动转换成定义的实际数据表字段。 字段映射还可以支持对主键的映射。 使用字段映射后,默认不会对读取的数据会自动处理, 输出结果类似: 这个时候取出的data数据包含的是实际的username和email字段。 如果我们需要在数据获取的时候自动处理的话,设置开启 READ_DATA_MAP 参数, 2. $User->delete('5,6'); // 删除主键为5、6的多个数据 1. namespace Home\Model; 2. use Think\Model; 3. Class UserModel extends Model{ 4. protected $_map = array( 5. 'name' =>'username', // 把表单中name映射到数据表的username字段 6. 'mail' =>'email', // 把表单中的mail映射到数据表的email字段 7. ); 8. } 1. // 实例化User模型 2. $User = D('User'); 3. $data = $User->find(3); 4. dump($data); 1. array(size=4) 2. 'id' => int 3 3. 'username'=> string 'thinkphp'(length=8) 4. 'email' => string 'thinkphp@gmail.com' (length=18) 5. 'status' => int 1 - 145 - 查询语言 查询方式 这个时候,输出结果类似: 或者直接使用 parseFieldsMap 方法进行转换处理,例如: 通过上面的两种方式后,无论是find还是select方法读取后的data数据中就包含了name和mail字段数据 了,而不再有username和email字段数据了。 ThinkPHP可以支持直接使用字符串作为查询条件,但是大多数情况推荐使用数组或者对象来作为查询条 件,因为会更加安全。 一、使用字符串作为查询条件 这是最传统的方式,但是安全性不高,例如: 最后生成的SQL语句是 采用字符串查询的时候,我们可以配合使用字符串条件的安全预处理机制。 二、使用数组作为查询条件 1. 'READ_DATA_MAP'=>true 1. array(size=4) 2. 'id' => int 3 3. 'name' => string 'thinkphp'(length=8) 4. 'mail' => string 'thinkphp@gmail.com' (length=18) 5. 'status' => int 1 1. // 实例化User模型 2. $User = D('User'); 3. $data = $User->find(3); 4. $data = $User->parseFieldsMap($data); 1. $User = M("User"); // 实例化User对象 2. $User->where('type=1 AND status=1')->select(); 1. SELECT * FROM think_user WHERE type=1 AND status=1 - 146 - 这种方式是最常用的查询方式,例如: 最后生成的SQL语句是 如果进行多字段查询,那么字段之间的默认逻辑关系是 逻辑与 AND,但是用下面的规则可以更改默认的 逻辑判断,通过使用 _logic 定义查询逻辑: 最后生成的SQL语句是 三、使用对象方式来查询 这里以stdClass内置对象为例: 最后生成的SQL语句和上面一样 使用对象方式查询和使用数组查询的效果是相同的,并且是可以互换的,大多数情况下,我们建议采用数 组方式更加高效。 1. $User = M("User"); // 实例化User对象 2. $condition['name'] = 'thinkphp'; 3. $condition['status'] = 1; 4. // 把查询条件传入查询方法 5. $User->where($condition)->select(); 1. SELECT * FROM think_user WHERE `name`='thinkphp' AND status=1 1. $User = M("User"); // 实例化User对象 2. $condition['name'] = 'thinkphp'; 3. $condition['account'] = 'thinkphp'; 4. $condition['_logic'] = 'OR'; 5. // 把查询条件传入查询方法 6. $User->where($condition)->select(); 1. SELECT * FROM think_user WHERE `name`='thinkphp' OR `account`='thinkphp' 1. $User = M("User"); // 实例化User对象 2. // 定义查询条件 3. $condition = new stdClass(); 4. $condition->name = 'thinkphp'; 5. $condition->status= 1; 6. $User->where($condition)->select(); 1. SELECT * FROM think_user WHERE `name`='thinkphp' AND status=1 - 147 - 表达式查询 在使用数组和对象方式查询的时候,如果传入了不存在的查询字段是会被自动过滤的,例如: 因为数据库的test字段是不存在的,所以系统会自动检测并过滤掉 $condition['test'] = 'test' 这一 查询条件。 如果是3.2.2版本以上,当开启调试模式的话,则会抛出异常,显示: 错误的查询条件 。 上面的查询条件仅仅是一个简单的相等判断,可以使用查询表达式支持更多的SQL查询语法,也是 ThinkPHP查询语言的精髓,查询表达式的使用格式: $map['字段名'] = array('表达式','查询条件'); 表达式不分大小写,支持的查询表达式有下面几种,分别表示的含义是: 表达式 含义 协助记忆 EQ 等于(=) equal NEQ 不等于(<>) not equal GT 大于(>) greater EGT 大于等于(>=) equal or greater LT 小于(<) less than ELT 小于等于(<=) equal or less than LIKE 模糊查询 [NOT] BETWEEN (不在)区间查询 [NOT] IN (不在)IN 查询 EXP 表达式查询,支持SQL语法 expression 1. $User = M("User"); // 实例化User对象 2. $condition['name'] = 'thinkphp'; 3. $condition['status'] = 1; 4. $condition['test'] = 'test'; 5. // 把查询条件传入查询方法 6. $User->where($condition)->select(); - 148 - 表达式查询的用法示例如下: EQ :等于(=) 例如: 和下面的查询等效 表示的查询条件就是 id = 100 NEQ: 不等于(<>) 例如: 表示的查询条件就是 id <> 100 GT:大于(>) 例如: 表示的查询条件就是 id > 100 EGT:大于等于(>=) 例如: 表示的查询条件就是 id >= 100 LT:小于(<) 例如: 表示的查询条件就是 id < 100 ELT: 小于等于(<=) 1. $map['id'] = array('eq',100); 1. $map['id'] = 100; 1. $map['id'] = array('neq',100); 1. $map['id'] = array('gt',100); 1. $map['id'] = array('egt',100); 1. $map['id'] = array('lt',100); - 149 - 例如: 表示的查询条件就是 id <= 100 [NOT] LIKE: 同sql的LIKE 例如: 查询条件就变成 name like 'thinkphp%' 如果配置了DB_LIKE_FIELDS参数的话,某些字段也会自动进 行模糊查询。例如设置了: 的话,使用 查询条件就会变成 title like '%thinkphp%' 支持数组方式,例如 生成的查询条件就是: [NOT] BETWEEN :同sql的[not] between 查询条件支持字符串或者数组,例如: 和下面的等效: 查询条件就变成 id BETWEEN 1 AND 8 [NOT] IN: 同sql的[not] in 查询条件支持字符串或者数组,例如: 1. $map['id'] = array('elt',100); 1. $map['name'] = array('like','thinkphp%'); 1. 'DB_LIKE_FIELDS'=>'title|content' 1. $map['title'] = 'thinkphp'; 1. $map['a'] =array('like',array('%thinkphp%','%tp'),'OR'); 2. $map['b'] =array('notlike',array('%thinkphp%','%tp'),'AND'); 1. (a like '%thinkphp%' OR a like '%tp') AND (b not like '%thinkphp%' AND b not lik e '%tp') 1. $map['id'] = array('between','1,8'); 1. $map['id'] = array('between',array('1','8')); - 150 - 快捷查询 和下面的等效: 查询条件就变成 id NOT IN (1,5, 8) EXP:表达式 支持更复杂的查询情况 例如: 可以改成: exp查询的条件不会被当成字符串,所以后面的查询条件可以使用任何SQL支持的语法,包括使用函数和 字段名称。查询表达式不仅可用于查询条件,也可以用于数据更新,例如: 快捷查询方式是一种多字段查询的简化写法,可以进一步简化查询条件的写法,在多个字段之间用|分割表 示OR查询,用&分割表示AND查询,可以实现下面的查询,例如: 一、不同字段相同的查询条件 上面的查询其实可以等效于 1. $map['id'] = array('not in','1,5,8'); 1. $map['id'] = array('not in',array('1','5','8')); 1. $map['id'] = array('in','1,3,8'); 1. $map['id'] = array('exp',' IN (1,3,8) '); 1. $User = M("User"); // 实例化User对象 2. // 要修改的数据对象属性赋值 3. $data['name'] = 'ThinkPHP'; 4. $data['score'] = array('exp','score+1');// 用户的积分加1 5. $User->where('id=5')->save($data); // 根据条件保存修改的数据 1. $User = M("User"); // 实例化User对象 2. $map['name|title'] = 'thinkphp'; 3. // 把查询条件传入查询方法 4. $User->where($map)->select(); 1. $User = M("User"); // 实例化User对象 2. $map['name'] = 'thinkphp'; - 151 - 区间查询 查询条件就变成 name= 'thinkphp' OR title = 'thinkphp' 二、不同字段不同的查询条件 上面的查询等效于: '_multi'=>true 必须加在数组的最后,表示当前是多条件匹配,这样查询条件就变成 status= 1 AND title = 'thinkphp' ,查询字段支持更多的,例如: 等效于: 查询条件就变成 status= 1 AND score >0 AND title = 'thinkphp' 注意:快捷查询方式中“|”和“&”不能同时使用。 3. $map['title'] = 'thinkphp'; 4. $map['_logic'] = 'OR'; 5. // 把查询条件传入查询方法 6. $User->where($map)->select(); 1. $User = M("User"); // 实例化User对象 2. $map['status&title'] =array('1','thinkphp','_multi'=>true); 3. // 把查询条件传入查询方法 4. $User->where($map)->select(); 1. $User = M("User"); // 实例化User对象 2. $map['status'] = 1; 3. $map['title'] = 'thinkphp'; 4. // 把查询条件传入查询方法 5. $User->where($map)->select(); 1. $map['status&score&title'] =array('1',array('gt','0'),'thinkphp','_multi'=>true) ; 1. $map['status'] = 1; 2. $map['score'] = array('gt',0); 3. $map['title'] = 'thinkphp'; - 152 - 组合查询 ThinkPHP支持对某个字段的区间查询,例如: 得到的查询条件是: ( id > 1) AND ( id < 10) 得到的查询条件是: ( id > 3) OR ( id < 10) 得到的查询条件是: ( id != 6) AND ( id > 3) 最后一个可以是AND、 OR或者 XOR运算符,如果不写,默认是AND运算。 区间查询的条件可以支持普通查询的所有表达式,也就是说类似LIKE、GT和EXP这样的表达式都可以支 持。另外区间查询还可以支持更多的条件,只要是针对一个字段的条件都可以写到一起,例如: 最后的查询条件是: ( name LIKE '%a%') OR ( name LIKE '%b%') OR ( name LIKE '%c%') OR ( name = 'ThinkPHP') 组合查询的主体还是采用数组方式查询,只是加入了一些特殊的查询支持,包括字符串模式查询 ( _string )、复合查询( _complex )、请求字符串查询( _query ),混合查询中的特殊查询每次 查询只能定义一个,由于采用数组的索引方式,索引相同的特殊查询会被覆盖。 一、字符串模式查询 数组条件可以和字符串条件(采用_string 作为查询条件)混合使用,例如: 1. $map['id'] = array(array('gt',1),array('lt',10)) ; 1. $map['id'] = array(array('gt',3),array('lt',10), 'or') ; 1. $map['id'] = array(array('neq',6),array('gt',3),'and'); 1. $map['name'] = array(array('like','%a%'), array('like','%b%'), array('like','%c %'), 'ThinkPHP','or'); 1. $User = M("User"); // 实例化User对象 2. $map['id'] = array('neq',1); 3. $map['name'] = 'ok'; 4. $map['_string'] = 'status=1 AND score>10'; 5. $User->where($map)->select(); - 153 - 统计查询 最后得到的查询条件就成了: 二、请求字符串查询方式 请求字符串查询是一种类似于URL传参的方式,可以支持简单的条件相等判断。 得到的查询条件是: 三、复合查询 复合查询相当于封装了一个新的查询条件,然后并入原来的查询条件之中,所以可以完成比较复杂的查询 条件组装。 例如: 查询条件是 复合查询使用了_complex作为子查询条件来定义,配合之前的查询方式,可以非常灵活的制定更加复杂 的查询条件。 很多查询方式可以相互转换,例如上面的查询条件可以改成: 最后生成的SQL语句是一致的。 在应用中我们经常会用到一些统计数据,例如当前所有(或者满足某些条件)的用户数、所有用户的最大 积分、用户的平均成绩等等,ThinkPHP为这些统计操作提供了一系列的内置方法,包括: 方法 说明 1. ( `id` != 1 ) AND ( `name` = 'ok' ) AND ( status=1 AND score>10 ) 1. $map['id'] = array('gt','100'); 2. $map['_query'] = 'status=1&score=100&_logic=or'; 1. `id`>100 AND (`status` = '1' OR `score` = '100') 1. $where['name'] = array('like', '%thinkphp%'); 2. $where['title'] = array('like','%thinkphp%'); 3. $where['_logic'] = 'or'; 4. $map['_complex'] = $where; 5. $map['id'] = array('gt',1); 1. ( id > 1) AND ( ( name like '%thinkphp%') OR ( title like '%thinkphp%') ) 1. $where['id'] = array('gt',1); 2. $where['_string'] = ' (name like "%thinkphp%") OR ( title like "%thinkphp") '; - 154 - SQL查询 Count 统计数量,参数是要统计的字段名(可选) Max 获取最大值,参数是要统计的字段名(必须) Min 获取最小值,参数是要统计的字段名(必须) Avg 获取平均值,参数是要统计的字段名(必须) Sum 获取总分,参数是要统计的字段名(必须) 用法示例: 获取用户数: 或者根据字段统计: 获取用户的最大积分: 获取积分大于0的用户的最小积分: 获取用户的平均积分: 统计用户的总成绩: 并且所有的统计查询均支持连贯操作的使用。 ThinkPHP内置的ORM和ActiveRecord模式实现了方便的数据存取操作,而且新版增加的连贯操作功能 更是让这个数据操作更加清晰,但是ThinkPHP仍然保留了原生的SQL查询和执行操作支持,为了满足复 杂查询的需要和一些特殊的数据操作,SQL查询的返回值因为是直接返回的Db类的查询结果,没有做任何 1. $User = M("User"); // 实例化User对象 1. $userCount = $User->count(); 1. $userCount = $User->count("id"); 1. $maxScore = $User->max('score'); 1. $minScore = $User->where('score>0')->min('score'); 1. $avgScore = $User->avg('score'); 1. $sumScore = $User->sum('score'); - 155 - 的处理。 主要包括下面两个方法: 1、query方法 query方法用于执行SQL查询操作,如果数据非法或者查询错误则返回false,否则返回查询结果数据集 (同select方法)。 使用示例: 如果你当前采用了分布式数据库,并且设置了读写分离的话,query方法始终是在读服务器执行,因 此query方法对应的都是读操作,而不管你的SQL语句是什么。 可以在query方法中使用表名的简化写法,便于动态更改表前缀,例如: 和上面的写法等效,会自动读取当前设置的表前缀。 2、execute方法 execute用于更新和写入数据的sql操作,如果数据非法或者查询错误则返回false ,否则返回影响的记录 数。 使用示例: 如果你当前采用了分布式数据库,并且设置了读写分离的话,execute方法始终是在写服务器执行, 因此execute方法对应的都是写操作,而不管你的SQL语句是什么。 也可以在execute方法中使用表名的简化写法,便于动态更改表前缀,例如: 1. $Model = new Model() // 实例化一个model对象 没有对应任何数据表 2. $Model->query("select * from think_user where status=1"); 1. $Model = new Model() // 实例化一个model对象 没有对应任何数据表 2. $Model->query("select * from __PREFIX__user where status=1"); 3. // 3.2.2版本以上还可以直接使用 4. $Model->query("select * from __USER__ where status=1"); 1. $Model = new Model() // 实例化一个model对象 没有对应任何数据表 2. $Model->execute("update think_user set name='thinkPHP' where status=1"); 1. $Model = new Model() // 实例化一个model对象 没有对应任何数据表 - 156 - 动态查询 子查询 和上面的写法等效,会自动读取当前设置的表前缀。 借助PHP5语言的特性,ThinkPHP实现了动态查询,核心模型的动态查询方法包括下面几种: 方法名 说明 举例 getBy 根据字段的值查询数据 例如,getByName,getByEmail getFieldBy 根据字段查询并返回某个字段的值 例如,getFieldByName 一、getBy动态查询 该查询方式针对数据表的字段进行查询。例如,User对象拥有id,name,email,address 等属性,那么我们 就可以使用下面的查询方法来直接根据某个属性来查询符合条件的记录。 暂时不支持多数据字段的动态查询方法,请使用find方法和select方法进行查询。 二、getFieldBy动态查询 针对某个字段查询并返回某个字段的值,例如 表示根据用户的name获取用户的id值。 从3.0版本开始新增了子查询支持,有两种使用方式: 1、使用select方法 当select方法的参数为false的时候,表示不进行查询只是返回构建SQL,例如: 2. $Model->execute("update __PREFIX__user set name='thinkPHP' where status=1"); 3. // 3.2.2版本以上还可以直接使用 4. $Model->execute("update __USER__ set name='thinkPHP' where status=1"); 1. $user = $User->getByName('liu21st'); 2. $user = $User->getByEmail('liu21st@gmail.com'); 3. $user = $User->getByAddress('中国深圳'); 1. $userId = $User->getFieldByName('liu21st','id'); 1. // 首先构造子查询SQL 2. $subQuery = $model->field('id,name')->table('tablename')->group('field')->where( $where)->order('status')->select(false); - 157 - 自动验证 当select方法传入false参数的时候,表示不执行当前查询,而只是生成查询SQL。 2、使用buildSql方法 调用buildSql方法后不会进行实际的查询操作,而只是生成该次查询的SQL语句(为了避免混淆,会在 SQL两边加上括号),然后我们直接在后续的查询中直接调用。 构造的子查询SQL可用于ThinkPHP的连贯操作方法,例如table where等。 自动验证是ThinkPHP模型层提供的一种数据验证方法,可以在使用create创建数据对象的时候自动进行 数据验证。 验证规则 数据验证可以进行数据类型、业务规则、安全判断等方面的验证操作。 数据验证有两种方式: 1. 静态方式:在模型类里面通过$_validate属性定义验证规则。 2. 动态方式:使用模型类的validate方法动态创建自动验证规则。 无论是什么方式,验证规则的定义是统一的规则,定义格式为: 说明 验证字段 (必须) 需要验证的表单字段名称,这个字段不一定是数据库字段,也可以是表单的一些辅助字段,例如确认密码 和验证码等等。有个别验证规则和字段无关的情况下,验证字段是可以随意设置的,例如expire有效期规 1. $subQuery = $model->field('id,name')->table('tablename')->group('field')->where( $where)->order('status')->buildSql(); 1. // 利用子查询进行查询 2. $model->table($subQuery.' a')->where()->order()->select() 1. array( 2. array(验证字段1,验证规则,错误提示,[验证条件,附加规则,验证时间]), 3. array(验证字段2,验证规则,错误提示,[验证条件,附加规则,验证时间]), 4. ...... 5. ); - 158 - 则是和表单字段无关的。如果定义了字段映射的话,这里的验证字段名称应该是实际的数据表字段而不是 表单字段。 验证规则 (必须) 要进行验证的规则,需要结合附加规则,如果在使用正则验证的附加规则情况下,系统还内置了一些常用 正则验证的规则,可以直接作为验证规则使用,包括:require 字段必须、email 邮箱、url URL地址、 currency 货币、number 数字。 提示信息 (必须) 用于验证失败后的提示信息定义 验证条件 (可选) 包含下面几种情况: self::EXISTS_VALIDATE 或者0 存在字段就验证(默认) self::MUST_VALIDATE 或者1 必须验证 self::VALUE_VALIDATE或者2 值不为空的时候验证 附加规则 (可选) 配合验证规则使用,包括下面一些规则: 规则 说明 regex 正则验证,定义的验证规则是一个正则表达式(默认) function 函数验证,定义的验证规则是一个函数名 callback 方法验证,定义的验证规则是当前模型类的一个方法 confirm 验证表单中的两个字段是否相同,定义的验证规则是一个字段名 equal 验证是否等于某个值,该值由前面的验证规则定义 notequal 验证是否不等于某个值,该值由前面的验证规则定义(3.1.2版本新增) in 验证是否在某个范围内,定义的验证规则可以是一个数组或者逗号分割的字符串 notin 验证是否不在某个范围内,定义的验证规则可以是一个数组或者逗号分割的字符串 (3.1.2版本新增) length 验证长度,定义的验证规则可以是一个数字(表示固定长度)或者数字范围(例如3,12 表示长度从3到12的范围) between 验证范围,定义的验证规则表示范围,可以使用字符串或者数组,例如1,31或者 array(1,31) - 159 - notbetween 验证不在某个范围,定义的验证规则表示范围,可以使用字符串或者数组(3.1.2版本新 增) expire 验证是否在有效期,定义的验证规则表示时间范围,可以到时间,例如可以使用 2012- 1-15,2013-1-15 表示当前提交有效期在2012-1-15到2013-1-15之间,也可以使用时 间戳定义 ip_allow 验证IP是否允许,定义的验证规则表示允许的IP地址列表,用逗号分隔,例如 201.12.2.5,201.12.2.6 ip_deny 验证IP是否禁止,定义的验证规则表示禁止的ip地址列表,用逗号分隔,例如 201.12.2.5,201.12.2.6 unique 验证是否唯一,系统会根据字段目前的值查询数据库来判断是否存在相同的值,当表单 数据中包含主键字段时unique不可用于判断主键字段本身 验证时间(可选) self::MODEL_INSERT或者1新增数据时候验证 self::MODEL_UPDATE或者2编辑数据时候验证 self::MODEL_BOTH或者3全部情况下验证(默认) 这里的验证时间需要注意,并非只有这三种情况,你可以根据业务需要增加其他的验证时间。 静态定义 在模型类里面预先定义好该模型的自动验证规则,我们称为静态定义。 举例说明,我们在模型类里面定义了 $_validate 属性如下: 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model{ 4. protected $_validate = array( 5. array('verify','require','验证码必须!'), //默认情况下用正则进行验证 6. array('name','','帐号名称已经存在!',0,'unique',1), // 在新增的时候验证name字 段是否唯一 7. array('value',array(1,2,3),'值的范围不正确!',2,'in'), // 当值不为空的时候判 断是否在一个范围内 8. array('repassword','password','确认密码不正确',0,'confirm'), // 验证确认密码 是否和密码一致 9. array('password','checkPwd','密码格式不正确',0,'function'), // 自定义函数验 - 160 - 定义好验证规则后,就可以在使用create方法创建数据对象的时候自动调用: 在进行自动验证的时候,系统会对定义好的验证规则进行依次验证。如果某一条验证规则没有通过,则会 报错,getError方法返回的错误信息(字符串)就是对应字段的验证规则里面的错误提示信息。 静态定义方式因为必须定义模型类,所以只能用D函数实例化模型 默认情况下,create方法是对表单提交的POST数据进行自动验证,如果你的数据来源不是表单post,仍 然也可以进行自动验证,方法改进如下: 一般情况下,create方法会自动判断当前是新增数据还是编辑数据(主要是通过表单的隐藏数据添加主键 信息),你也可以明确指定当前创建的数据对象是新增还是编辑,例如: 证密码格式 10. ); 11. } 1. $User = D("User"); // 实例化User对象 2. if (!$User->create()){ 3. // 如果创建失败 表示验证没有通过 输出错误提示信息 4. exit($User->getError()); 5. }else{ 6. // 验证通过 可以进行其他数据操作 7. } 1. $User = D("User"); // 实例化User对象 2. $data = getData(); // 通过getData方法获取数据源的(数组)数据 3. if (!$User->create($data)){ 4. // 对data数据进行验证 5. exit($User->getError()); 6. }else{ 7. // 验证通过 可以进行其他数据操作 8. } 1. $User = D("User"); // 实例化User对象 2. if (!$User->create($_POST,1)){ // 指定新增数据 3. // 如果创建失败 表示验证没有通过 输出错误提示信息 4. exit($User->getError()); 5. }else{ - 161 - create方法的第二个参数就用于指定自动验证规则中的验证时间,也就是说create方法的自动验证只会验 证符合验证时间的验证规则。 我们在上面提到这里的验证时间并非只有这几种情况,你可以根据业务需要增加其他的验证时间,例如, 你可以给登录操作专门指定验证时间为4。我们定义验证规则如下: 那么,我们就可以在登录的时候使用 动态验证 如果采用动态验证的方式,就比较灵活,可以根据不同的需要,在操作同一个模型的时候使用不同的验证 规则,例如上面的静态验证方式可以改为: 6. // 验证通过 可以进行其他数据操作 7. } 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model{ 4. protected $_validate = array( 5. array('verify','require','验证码必须!'), // 都有时间都验证 6. array('name','checkName','帐号错误!',1,'function',4), // 只在登录时候验证 7. array('password','checkPwd','密码错误!',1,'function',4), // 只在登录时候验 证 8. ); 9. } 1. $User = D("User"); // 实例化User对象 2. if (!$User->create($_POST,4)){ // 登录验证数据 3. // 验证没有通过 输出错误提示信息 4. exit($User->getError()); 5. }else{ 6. // 验证通过 执行登录操作 7. } 1. $rules = array( 2. array('verify','require','验证码必须!'), //默认情况下用正则进行验证 3. array('name','','帐号名称已经存在!',0,'unique',1), // 在新增的时候验证name字 段是否唯一 4. array('value',array(1,2,3),'值的范围不正确!',2,'in'), // 当值不为空的时候判 - 162 - 动态验证不依赖模型类的定义,所以通常用M函数实例化模型就可以 错误信息多语言支持 如果你希望支持多语言的错误信息提示,那么可以在验证规则里面如下定义: 其中 VERIFY_CODE_MUST 和 ACCOUNT_EXISTS 是我们在语言包里面定义的多语言变量。 如果是采用动态验证方式,则比较简单,直接在定义验证规则的时候使用L方法即可,例如: 批量验证 系统支持数据的批量验证功能,只需要在模型类里面设置patchValidate属性为true( 默认为false), 断是否在一个范围内 5. array('repassword','password','确认密码不正确',0,'confirm'), // 验证确认密码 是否和密码一致 6. array('password','checkPwd','密码格式不正确',0,'function'), // 自定义函数验 证密码格式 7. ); 8. 9. $User = M("User"); // 实例化User对象 10. if (!$User->validate($rules)->create()){ 11. // 如果创建失败 表示验证没有通过 输出错误提示信息 12. exit($User->getError()); 13. }else{ 14. // 验证通过 可以进行其他数据操作 15. } 1. protected $_validate = array( 2. array('verify','require','{%VERIFY_CODE_MUST}'), 3. array('name','','{%ACCOUNT_EXISTS}',0,'unique',1), 4. ); 1. $rules = array( 2. array('verify','require',L('VERIFY_CODE_MUST')), 3. array('name','',L('ACCOUNT_EXISTS'),0,'unique',1), 4. ); 1. protected $patchValidate = true; - 163 - 自动完成 设置批处理验证后, getError() 方法返回的错误信息是一个数组,返回格式是: 前端可以根据需要需要自行处理,例如转换成json格式返回: 自动完成是ThinkPHP提供用来完成数据自动处理和过滤的方法,使用create方法创建数据对象的时候会 自动完成数据处理。 因此,在ThinkPHP使用create方法来创建数据对象是更加安全的方式,而不是直接通过add或者save方 法实现数据写入。 规则定义 自动完成通常用来完成默认字段写入,安全字段过滤以及业务逻辑的自动处理等,和自动验证的定义方式 类似,自动完成的定义也支持静态定义和动态定义两种方式。 1. 静态方式:在模型类里面通过$_auto属性定义处理规则。 2. 动态方式:使用模型类的auto方法动态创建自动处理规则。 两种方式的定义规则都采用: 说明 完成字段(必须) 需要进行处理的数据表实际字段名称。 1. array("字段名1"=>"错误提示1","字段名2"=>"错误提示2"... ) 1. $User = D("User"); // 实例化User对象 2. if (!$User->create()){ 3. // 如果创建失败 表示验证没有通过 输出错误提示信息 4. $this->ajaxReturn($User->getError()); 5. }else{ 6. // 验证通过 可以进行其他数据操作 7. } 1. array( 2. array(完成字段1,完成规则,[完成条件,附加规则]), 3. array(完成字段2,完成规则,[完成条件,附加规则]), 4. ...... 5. ); - 164 - 完成规则(必须) 需要处理的规则,配合附加规则完成。 完成时间(可选) 设置自动完成的时间,包括: 设置 说明 self::MODEL_INSERT或者1 新增数据的时候处理(默认) self::MODEL_UPDATE或者2 更新数据的时候处理 self::MODEL_BOTH或者3 所有情况都进行处理 附加规则(可选) 包括: 规则 说明 function 使用函数,表示填充的内容是一个函数名 callback 回调方法 ,表示填充的内容是一个当前模型的方法 field 用其它字段填充,表示填充的内容是一个其他字段的值 string 字符串(默认方式) ignore 为空则忽略(3.1.2新增) 静态定义 预先在模型类里面定义好自动完成的规则,我们称之为静态定义。例如,我们在模型类定义 _auto 属 性: 1. namespace Home\Model; 2. use Think\Model; 3. class UserModel extends Model{ 4. protected $_auto = array ( 5. array('status','1'), // 新增的时候把status字段设置为1 6. array('password','md5',3,'function') , // 对password字段在新增和编辑的时 候使md5函数处理 7. array('name','getName',3,'callback'), // 对name字段在新增和编辑的时候回 调getName方法 8. array('update_time','time',2,'function'), // 对update_time字段在更新的时 - 165 - 然后,就可以在使用create方法创建数据对象的时候自动处理: 如果你没有定义任何自动验证规则的话,则不需要判断create方法的返回值: 或者更简单的使用: create方法默认情况下是根据表单提交的post数据生成数据对象,我们也可以根据其他的数据源来生成数 据对象,你也可以明确指定当前创建的数据对象自动处理的时间是新增还是编辑数据,例如: create方法的第二个参数就用于指定自动完成规则中的完成时间,也就是说create方法的自动处理规则只 会处理符合完成时间的自动完成规则。 create方法在创建数据的时候,已经自动过滤了非数据表字段数据 信息,因此不需要担心表单会提交其他的非法字段信息而导致数据对象写入出错,甚至还可以自动过滤不 希望用户在表单提交的字段信息(详见字段合法性过滤)。 3.1.2版本开始新增了ignore完成规则,这一规则表示某个字段如果留空的话则忽略,通常可用于修改用 户资料时候密码的输入,定义如下: 候写入当前时间戳 9. ); 10. } 1. $User = D("User"); // 实例化User对象 2. if (!$User->create()){ // 创建数据对象 3. // 如果创建失败 表示验证没有通过 输出错误提示信息 4. exit($User->getError()); 5. }else{ 6. // 验证通过 写入新增数据 7. $User->add(); 8. } 1. $User = D("User"); // 实例化User对象 2. $User->create(); // 生成数据对象 3. $User->add(); // 新增用户数据 1. $User = D("User"); // 实例化User对象 2. $User->create()->add(); // 生成数据对象并写入数据 1. $User = D("User"); // 实例化User对象 2. $userData = getUserData(); // 通过方法获取用户数据 3. $User->create($userData,2); // 根据userData数据创建数据对象,并指定为更新数据 4. $User->add(); - 166 - 表示password字段编辑的时候留空则忽略。 动态完成 除了静态定义之外,我们也可以采用动态完成的方式来解决不同的处理规则。 修改数据对象 在使用create方法创建好数据对象之后,此时的数据对象保存在内存中,因此仍然可以操作数据对象,包 括修改或者增加数据对象的值,例如: 一旦调用了add方法(或者save方法),创建在内存中的数据对象就会失效,如果希望创建好的数据对象 在后面的数据处理中再次调用,可以保存数据对象先,例如: 不过要记得,如果你修改了内存中的数据对象并不会自动更新保存的数据对象,因此下面的用法是错误 的: 1. array('password','',2,'ignore') 1. $rules = array ( 2. array('status','1'), // 新增的时候把status字段设置为1 3. array('password','md5',3,'function') , // 对password字段在新增和编辑的时候使m d5函数处理 4. array('update_time','time',2,'function'), // 对update_time字段在更新的时候写 入当前时间戳 5. ); 6. $User = M('User'); 7. $User->auto($rules)->create()->add(); 1. $User = D("User"); // 实例化User对象 2. $User->create(); // 生成数据对象 3. $User->status = 2; // 修改数据对象的status属性 4. $User->register_time = NOW_TIME; // 增加register_time属性 5. $User->add(); // 新增用户数据 1. $User = D("User"); // 实例化User对象 2. $data = $User->create(); // 保存生成的数据对象 3. $User->add(); 1. $User = D("User"); // 实例化User对象 2. $data = $User->create(); // 保存生成的数据对象 3. $User->status = 2; // 修改数据对象的status属性 - 167 - 参数绑定 上面的代码我们修改了数据对象,但是仍然写入的是之前保存的数据对象,因此对数据对象的更改操作将 会无效。 参数绑定是指绑定一个参数到预处理的SQL语句中的对应命名占位符或问号占位符指定的变量,并且可以 提高SQL处理的效率,需要数据库驱动类的支持,目前只有 PDO和Sqlsrv驱动 支持参数绑定功能。 手动绑定 参数手动绑定需要调用连贯操作的bind方法,例如: 目前不支持 ?方式 进行占位符,无论是PDO还是Sqlsrv驱动均统一使用 :var 方式进行占位符,驱动内 部会自动进行处理。 参数绑定的参数可以是条件或者要data数据中的参数,CURD操作均可以支持参数绑定bind方法。 可以支持指定绑定变量的类型参数,例如: 也可以批量绑定,例如: 自动绑定 4. $User->register_time = NOW_TIME; // 增加register_time属性 5. $User->add($data); 1. $Model = M('User'); 2. $where['name'] = ':name'; 3. $list = $Model->where($where)->bind(':name',I('name'))->select(); 1. $Model = M('User'); 2. $where['id'] = ':id'; 3. $list = $Model->where($where)->bind(':id',I('id'),\PDO::PARAM_INT)->select(); 1. $Model = M('User'); 2. $where['id'] = ':id'; 3. $where['name'] = ':name'; 4. $bind[':id'] = array(I('id'),\PDO::PARAM_INT); 5. $bind[':name'] = array(I('name'),\PDO::PARAM_STR); 6. $list = $Model->where($where)->bind($bind)->select(); - 168 - 虚拟模型 对于某些操作的情况(例如模型的写入和更新方法),可以支持参数的自动绑定,例如: 首先需要开启 DB_BIND_PARAM配置参数: 然后,我们在使用 会自动对写入的数据进行参数绑定操作。其操作等效于: 自动绑定不支持参数类型等额外设置,如果有必要请使用上面的手动绑定方式。 虚拟模型是指虽然是模型类,但并不会真正的操作数据库的模型。有些时候,我们建立模型类但又不需要 进行数据库操作,仅仅是借助模型类来封装一些业务逻辑,那么可以借助虚拟模型来完成。虚拟模型不会 自动连接数据库,因此也不会自动检测数据表和字段信息,有两种方式可以定义虚拟模型: 第一种:继承Model类 设置autoCheckFields属性为false后,就会关闭字段信息的自动检测,因为ThinkPHP采用的是惰性数据 库连接,只要你不进行数据库查询操作,是不会连接数据库的。 第二种:不继承Model类 1. 'DB_BIND_PARAM' => true 1. $Model = M('User'); 2. $Model->name = 'thinkphp'; 3. $Model->email = 'thinkphp@qq.com'; 4. $Model->add(); 1. $Model = M('User'); 2. $Model->name = ':name'; 3. $Model->email = ':email'; 4. $bind[':name'] = 'thinkphp'; 5. $bind[':email'] = 'thinkphp@qq.com'; 6. $Model->bind($bind)->add(); 1. namespace Home\Model; 2. Class UserModel extends \Think\Model { 3. Protected $autoCheckFields = false; 4. } 1. namespace Home\Model; - 169 - 模型分层 这种方式下面自定义模型类就是一个单纯的业务逻辑类,不能再使用模型的CURD操作方法,但是可以实 例化其他的模型类进行相关操作,也可以在需要的时候直接实例化Db类进行数据库操作。 ThinkPHP支持模型的分层 ,除了Model层之外,我们可以项目的需要设计和创建其他的模型层。 通常情况下,不同的分层模型仍然是继承系统的\Think\Model类或其子类,所以,其基本操作和Model 类的操作是一致的。 例如在Home模块的设计中需要区分数据层、逻辑层、服务层等不同的模型层,我们可以在模块目录下面 创建 Model 、 Logic 和 Service 目录,把对用户表的所有模型操作分成三层: 数据层:Home\Model\UserModel 用于定义数据相关的自动验证和自动完成和数据存取接口 逻辑层:Home\Logic\UserLogic 用于定义用户相关的业务逻辑 服务层:Home\Service\UserService 用于定义用户相关的服务接口等 三个模型层的定义如下: Model类:Home\Model\UserModel.class.php 实例化方法: D('User'); Logic类:Home\Logic\UserLogic.class.php 实例化方法: D('User','Logic'); Api类:Home\Api\UserApi.class.php 2. Class UserModel { 3. } 1. namespace Home\Model; 2. class UserModel extends \Think\Model{ 3. 4. } 1. namespace Home\Logic; 2. class UserLogic extends \Think\Model{ 3. 4. } 1. namespace Home\Api; - 170 - 视图模型 实例化方法: D('User','Api'); D方法默认操作的模型层由 DEFAULT_M_LAYER 参数配置,我们可以改变默认操作的模型层为Logic层,例 如: 这样,当我们调用: 的时候其实是实例化的 UserLogic 类,而不是 UserModel 类。 视图定义 视图通常是指数据库的视图,视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列 带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义 视图的查询所引用的表,并且在引用视图时动态生成。对其中所引用的基础表来说,视图的作用类似于筛 选。定义视图的筛选可以来自当前或其它数据库的一个或多个表,或者其它视图。分布式查询也可用于定 义使用多个异类源数据的视图。如果有几台不同的服务器分别存储组织中不同地区的数据,而您需要将这 些服务器上相似结构的数据组合起来,这种方式就很有用。 视图在有些数据库下面并不被支持,但是 ThinkPHP模拟实现了数据库的视图,该功能可以用于多表联合查询。非常适合解决HAS_ONE 和 BELONGS_TO 类型的关联查询。 要定义视图模型,只需要继承Think\Model\ViewModel,然后设置viewFields属性即可。 例如下面的例子,我们定义了一个BlogView模型对象,其中包括了Blog模型的id、name、title和User 模型的name,以及Category模型的title字段,我们通过创建BlogView模型来快速读取一个包含了User 名称和类别名称的Blog记录(集)。 2. class UserApi extends \Think\Model{ 3. 4. } 1. 'DEFAULT_M_LAYER' => 'Logic', // 默认的模型层名称 1. $User = D('User'); 1. namespace Home\Model; 2. use Think\Model\ViewModel; 3. class BlogViewModel extends ViewModel { 4. public $viewFields = array( 5. 'Blog'=>array('id','name','title'), - 171 - 我们来解释一下定义的格式代表了什么。 $viewFields 属性表示视图模型包含的字段,每个元素定义了某个数据表或者模型的字段。 例如: 表示BlogView视图模型要包含Blog模型中的id、name和title字段属性,这个其实很容易理解,就和数据 库的视图要包含某个数据表的字段一样。而Blog相当于是给Blog模型对应的数据表定义了一个别名。 默认情况下会根据定义的名称自动获取表名,如果希望指定数据表,可以使用: 如果希望给当前数据表定义另外的别名,可以使用 BlogView视图模式除了包含Blog模型之外,还包含了Category和User模型,下面的定义: 和上面类似,表示BlogView视图模型还要包含Category模型的title字段,因为视图模型里面已经存在了 一个title字段,所以我们通过 把Category模型的title字段映射为 category_name 字段,如果有多个字段,可以使用同样的方式添加。 可以通过_on来给视图模型定义关联查询条件,例如: 理解之后,User模型的定义方式同样也就很容易理解了。 6. 'Category'=>array('title'=>'category_name', '_on'=>'Blog.category_id=Catego ry.id'), 7. 'User'=>array('name'=>'username', '_on'=>'Blog.user_id=User.id'), 8. ); 9. } 1. 'Blog'=>array('id','name','title'); 1. '_table'=>"test_user" 2. // 3.2.2版本以上还可以使用简化定义(自动获取表前缀) 3. '_table'=>"__USER__" 1. '_as'=>'myBlog' 1. 'Category'=>array('title'=>'category_name'); 1. 'title'=>'category_name' 1. '_on'=>'Blog.category_id=Category.id' 1. Blog.categoryId=Category.id AND Blog.userId=User.id - 172 - 最后,我们把视图模型的定义翻译成SQL语句就更加容易理解视图模型的原理了。假设我们不带任何其他 条件查询全部的字段,那么查询的SQL语句就是 视图模型的定义并不需要先单独定义其中的模型类,系统会默认按照系统的规则进行数据表的定位。如果 Blog模型并没有定义,那么系统会自动根据当前模型的表前缀和后缀来自动获取对应的数据表。也就是 说,如果我们并没有定义Blog模型类,那么上面的定义后,系统在进行视图模型的操作的时候会根据 Blog这个名称和当前的表前缀设置(假设为Think_ )获取到对应的数据表可能是think_blog。 ThinkPHP还可以支持视图模型的JOIN类型定义,我们可以把上面的视图定义改成: 需要注意的是,这里的_type定义对下一个表有效,因此要注意视图模型的定义顺序。Blog模型的 针对的是下一个模型Category而言,通过上面的定义,我们在查询的时候最终生成的SQL语句就变成: 1. Select 2. Blog.id as id, 3. Blog.name as name, 4. Blog.title as title, 5. Category.title as category_name, 6. User.name as username 7. from think_blog Blog JOIN think_category Category JOIN think_user User 8. where Blog.category_id=Category.id AND Blog.user_id=User.id 1. public $viewFields = array( 2. 'Blog'=>array('id','name','title','_type'=>'LEFT'), 3. 'Category'=>array('title'=>'category_name','_on'=>'Category.id=Blog.category _id','_type'=>'RIGHT'), 4. 'User'=>array('name'=>'username','_on'=>'User.id=Blog.user_id'), 5. ); 1. '_type'=>'LEFT' 1. Select 2. Blog.id as id, 3. Blog.name as name, 4. Blog.title as title, 5. Category.title as category_name, 6. User.name as username 7. from think_blog Blog LEFT JOIN think_category Category ON Blog.category_id=Categ ory.id RIGHT JOIN think_user User ON Blog.user_id=User.id - 173 - 关联模型 我们可以在试图模型里面定义特殊的字段,例如下面的例子定义了一个统计字段 视图查询 接下来,我们就可以和使用普通模型一样对视图模型进行操作了 。 看起来和普通的模型操作并没有什么大的区别,可以和使用普通模型一样进行查询。如果发现查询的结果 存在重复数据,还可以使用group方法来处理。 我们可以看到,即使不定义视图模型,其实我们也可以通过方法来操作,但是显然非常繁琐。 而定义了视图模型之后,所有的字段会进行自动处理,添加表别名和字段别名,从而简化了原来视图的复 杂查询。如果不使用视图模型,也可以用连贯操作的JOIN方法实现相同的功能。 关联关系 通常我们所说的关联关系包括下面三种: 1. 'Category'=>array('title'=>'category_name','COUNT(Blog.id)'=>'count','_on'=>'Cat egory.id=Blog.category_id'), 1. $Model = D("BlogView"); 2. $Model->field('id,name,title,category_name,username')->where('id>10')->order('id desc')->select(); 1. $Model->field('id,name,title,category_name,username')->order('id desc')->group(' id')->select(); 1. $Model = D("Blog"); 2. $Model->table('think_blog Blog,think_category Category,think_user User') 3. ->field('Blog.id,Blog.name,Blog.title,Category.title as category_name,User.name as username') 4. ->order('Blog.id desc') 5. ->where('Blog.category_id=Category.id AND Blog.user_id=User.id') 6. ->select(); 1. 一对一关联 :ONE_TO_ONE,包括HAS_ONE 和 BELONGS_TO 2. 一对多关联 :ONE_TO_MANY,包括HAS_MANY 和 BELONGS_TO 3. 多对多关联 :MANY_TO_MANY - 174 - 关联关系必然有一个参照表,例如: 有一个员工档案管理系统项目,这个项目要包括下面的一些数据表:基本信息表、员工档案表、部门 表、项目组表、银行卡表(用来记录员工的银行卡资料)。 这些数据表之间存在一定的关联关系,我们以员工基本信息表为参照来分析和其他表之间的关联: 每个员工必然有对应的员工档案资料,所以属于HAS_ONE关联; 每个员工必须属于某个部门,所以属于BELONGS_TO关联; 每个员工可以有多个银行卡,但是每张银行卡只可能属于一个员工,因此属于HAS_MANY关联; 每个员工可以同时在多个项目组,每个项目组同时有多个员工,因此属于MANY_TO_MANY关联; 分析清楚数据表之前的关联关系后,我们才可以进行关联定义和关联操作。 关联定义 ThinkPHP可以很轻松的完成数据表的关联CURD操作,目前支持的关联关系包括下面四种: HAS_ONE、BELONGS_TO、HAS_MANY和MANY_TO_MANY。 一个模型根据业务模型的复杂程度可以同时定义多个关联,不受限制,所有的关联定义都统一在模型类的 $_link 成员变量里面定义,并且可以支持动态定义。要支持关联操作,模型类必须继 承 Think\Model\RelationModel 类,关联定义的格式是: 下面我们首先来分析下各个关联方式的定义: HAS_ONE 1. namespace Home\Model; 2. use Think\Model\RelationModel; 3. class UserModel extends RelationModel{ 4. protected $_link = array( 5. '关联1' => array( 6. '关联属性1' => '定义', 7. '关联属性N' => '定义', 8. ), 9. '关联2' => array( 10. '关联属性1' => '定义', 11. '关联属性N' => '定义', 12. ), 13. '关联3' => HAS_ONE, // 快捷定义 14. ... 15. ); 16. } - 175 - HAS_ONE关联表示当前模型拥有一个子对象,例如,每个员工都有一个人事档案。我们可以建立一个用 户模型UserModel,并且添加如下关联定义: 上面是最简单的方式,表示其遵循了系统内置的数据库规范,完整的定义方式是: 关联HAS_ONE支持的关联属性有: mapping_type :关联类型 这个在HAS_ONE 关联里面必须使用HAS_ONE 常量定义。 class_name :要关联的模型类名 例如,class_name 定义为Profile的话则表示和另外的Profile模型类关联,这个Profile模型类是无需定义 的,系统会自动定位到相关的数据表进行关联。 mapping_name :关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。如果mapping_name没有定义 的话,会取class_name的定义作为mapping_name。如果class_name也没有定义,则以数组的索引作为 mapping_name。 1. namespace Home\Model; 2. use Think\Model\RelationModel; 3. class UserModel extends RelationModel{ 4. protected $_link = array( 5. 'Profile'=> self::HAS_ONE, 6. ); 7. } 1. namespace Home\Model; 2. use Think\Model\RelationModel; 3. class UserModel extends RelationModel{ 4. protected $_link = array( 5. 'Profile'=>array( 6. 'mapping_type' => self::HAS_ONE, 7. 'class_name' => 'Profile', 8. // 定义更多的关联属性 9. …… 10. ), 11. ); 12. } - 176 - foreign_key : 关联的外键名称 外键的默认规则是当前数据对象名称_id,例如: UserModel对应的可能是表think_user (注意:think 只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关 联的时候显式定义 foreign_key 。 condition : 关联条件 关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过定义关联的condition属性。 mapping_fields : 关联要查询的字段 默认情况下,关联查询的关联数据是关联表的全部字段,如果只是需要查询个别字段,可以定义关联的 mapping_fields属性。 as_fields :直接把关联的字段值映射成数据对象中的某个字段 这个特性是ONE_TO_ONE 关联特有的,可以直接把关联数据映射到数据对象中,而不是作为一个关联数 据。当关联数据的字段名和当前数据对象的字段名称有冲突时,还可以使用映射定义。 BELONGS_TO Belongs_to 关联表示当前模型从属于另外一个父对象,例如每个用户都属于一个部门。我们可以做如下 关联定义。 完整方式定义为: 关联BELONGS_TO定义支持的关联属性有: 属性 描述 class_name 要关联的模型类名 1. 'Dept' => self::BELONGS_TO 1. 'Dept' => array( 2. 'mapping_type' => self::BELONGS_TO, 3. 'class_name' => 'Dept', 4. 'foreign_key' => 'userId', 5. 'mapping_name' => 'dept', 6. // 定义更多的关联属性 7. …… 8. ), - 177 - mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导 致关联数据获取的冲突。 foreign_key 关联的外键名称 mapping_fields 关联要查询的字段 condition 关联条件 parent_key 自引用关联的关联字段 默认为parent_id 自引用关联是一种比较特殊的关联,也就 是关联表就是当前表。 as_fields 直接把关联的字段值映射成数据对象中的某个字段 HAS_MANY HAS_MANY 关联表示当前模型拥有多个子对象,例如每个用户有多篇文章,我们可以这样来定义: 完整定义方式为: 关联HAS_MANY定义支持的关联属性有: 属性 描述 class_name 要关联的模型类名 mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导 致关联数据获取的冲突。 foreign_key 关联的外键名称 parent_key 自引用关联的关联字段 默认为parent_id 1. 'Article' => self::HAS_MANY 1. 'Article' => array( 2. 'mapping_type' => self::HAS_MANY, 3. 'class_name' => 'Article', 4. 'foreign_key' => 'userId', 5. 'mapping_name' => 'articles', 6. 'mapping_order' => 'create_time desc', 7. // 定义更多的关联属性 8. …… 9. ), - 178 - condition 关联条件 关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过 定义关联的condition属性。 mapping_fields 关联要查询的字段 默认情况下,关联查询的关联数据是关联表的全部字段,如果只 是需要查询个别字段,可以定义关联的mapping_fields属性。 mapping_limit 关联要返回的记录数目 mapping_order 关联查询的排序 外键的默认规则是当前数据对象名称_id,例如:UserModel对应的可能是表think_user (注意:think 只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关 联的时候定义 foreign_key 。 MANY_TO_MANY MANY_TO_MANY 关联表示当前模型可以属于多个对象,而父对象则可能包含有多个子对象,通常两者 之间需要一个中间表类约束和关联。例如每个用户可以属于多个组,每个组可以有多个用户: 完整定义方式为: MANY_TO_MANY支持的关联属性定义有: 属性 描述 class_name 要关联的模型类名 mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则 会导致关联数据获取的冲突。 foreign_key 关联的外键名称 外键的默认规则是当前数据对象名称_id 1. 'Group' => self::MANY_TO_MANY 1. 'Group' => array( 2. 'mapping_type' => self::MANY_TO_MANY, 3. 'class_name' => 'Group', 4. 'mapping_name' => 'groups', 5. 'foreign_key' => 'userId', 6. 'relation_foreign_key' => 'groupId', 7. 'relation_table' => 'think_group_user' //此处应显式定义中间表名称,且不能 使用C函数读取表前缀 8. ) - 179 - relation_foreign_key 关联表的外键名称 默认的关联表的外键名称是表名_id mapping_limit 关联要返回的记录数目 mapping_order 关联查询的排序 relation_table 多对多的中间关联表名称 多对多的中间表默认表规则是:数据表前缀_关联操作的主表名_关联表名 如果think_user 和 think_group 存在一个对应的中间表,默认的表名应该是 如果是由group来操作关联 表,中间表应该是 think_group_user,如果是从user表来操作,那么应该是think_user_group,也就是 说,多对多关联的设置,必须有一个Model类里面需要显式定义中间表,否则双向操作会出错。 中间表 无需另外的id主键(但是这并不影响中间表的操作),通常只是由 user_id 和 group_id 构成。 默认会通 过当前模型的getRelationTableName方法来自动获取,如果当前模型是User,关联模型是Group,那 么关联表的名称也就是使用 user_group这样的格式,如果不是默认规则,需要指定relation_table属 性。 3.2.2版本开始,relation_table定义支持简化写法,例如: 关联查询 由于性能问题,新版取消了自动关联查询机制,而统一使用relation方法进行关联操作,relation方法不 但可以启用关联还可以控制局部关联操作,实现了关联操作一切尽在掌握之中。 输出$user结果可能是类似于下面的数据: 我们可以看到,用户的关联数据已经被映射到数据对象的属性里面了。其中Profile就是关联定义的 1. 'relation_table'=>'__USER_GROUP__' 1. $User = D("User"); 2. $user = $User->relation(true)->find(1); 1. array( 2. 'id' => 1, 3. 'account' => 'ThinkPHP', 4. 'password' => '123456', 5. 'Profile' => array( 6. 'email' => 'liu21st@gmail.com', 7. 'nickname' => '流年', 8. ), 9. ) - 180 - mapping_name属性。 如果我们按照下面的方式定义了as_fields属性的话, 查询的结果就变成了下面的结果 email和nickname两个字段已经作为user数据对象的字段来显示了。 如果关联数据的字段名和当前数据对象的字段有冲突的话,怎么解决呢? 我们可以用下面的方式来变化下定义: 表示关联表的nickname字段映射成当前数据对象的username字段。 默认会把所有定义的关联数据都查询出来,有时候我们并不希望这样,就可以给relation方法传入参数来 控制要关联查询的。 关联查询一样可以支持select方法,如果要查询多个数据,并同时获取相应的关联数据,可以改成: 1. protected $_link = array( 2. 'Profile'=>array( 3. 'mapping_type' => self::HAS_ONE, 4. 'class_name' => 'Profile', 5. 'foreign_key' => 'userId', 6. 'as_fields' => 'email,nickname', 7. ), 8. ); 1. array( 2. 'id' => 1, 3. 'account' => 'ThinkPHP', 4. 'password' => 'name', 5. 'email' => 'liu21st@gmail.com', 6. 'nickname' => '流年', 7. ) 1. 'as_fields' => 'email,nickname:username', 1. $User = D("User"); 2. $user = $User->relation('Profile')->find(1); 1. $User = D("User"); 2. $list = $User->relation(true)->Select(); - 181 - 如果希望在完成的查询基础之上 再进行关联数据的查询,可以使用 事实上,除了当前的参考模型User外,其他的关联模型是不需要创建的。 关联操作 除了关联查询外,系统也支持关联数据的自动写入、更新和删除 关联写入 这样就会自动写入关联的Profile数据。 同样,可以使用参数来控制要关联写入的数据: 关联更新 数据的关联更新和关联写入类似 1. $User = D("User"); 2. $user = $User->find(1); 3. // 表示对当前查询的数据对象进行关联数据获取 4. $profile = $User->relationGet("Profile"); 1. $User = D("User"); 2. $data = array(); 3. $data["account"] = "ThinkPHP"; 4. $data["password"] = "123456"; 5. $data["Profile"] = array( 6. 'email' =>'liu21st@gmail.com', 7. 'nickname' =>'流年', 8. ); 9. $result = $User->relation(true)->add($data); 1. $result = $User->relation("Profile")->add($data); 1. $User = D("User"); 2. $data["account"] = "ThinkPHP"; 3. $data["password"] = "123456"; 4. $data["Profile"] = array( 5. 'email' =>'liu21st@gmail.com', 6. 'nickname' =>'流年', 7. ); - 182 - 高级模型 Relation(true)会关联保存User模型定义的所有关联数据,如果只需要关联保存部分数据,可以使用: 这样就只会同时更新关联的Profile数据。 关联保存的规则: HAS_ONE: 关联数据的更新直接赋值 HAS_MANY: 的关联数据如果传入主键的值 则表示更新 否则就表示新增 MANY_TO_MANY: 的数据更新是删除之前的数据后重新写入 关联删除 高级模型提供了更多的查询功能和模型增强功能,利用了模型类的扩展机制实现。如果需要使用高级模型 的下面这些功能,记得需要继承Think\Model\AdvModel类或者采用动态模型。 我们下面的示例都假设UserModel类继承自Think\Model\AdvModel类。 字段过滤 基础模型类内置有数据自动完成功能,可以对字段进行过滤,但是必须通过Create方法调用才能生效。高 级模型类的字段过滤功能却可以不受create方法的调用限制,可以在模型里面定义各个字段的过滤机制, 包括写入过滤和读取过滤。 字段过滤的设置方式只需要在Model类里面添加 $_filter 属性,并且加入过滤因子,格式如下: 8. $result = $User-> relation(true)->where('id=3')->save($data); 1. $result = $User->relation("Profile")->save($data); 1. //删除用户ID为3的记录的同时删除关联数据 2. $result = $User->relation(true)->delete("3"); 3. // 如果只需要关联删除部分数据,可以使用 4. $result = $User->relation("Profile")->delete("3"); 1. namespace Home\Model; 2. use Think\Model\AdvModel; 3. class UserModel extends AdvModel{ 4. } 1. protected $_filter = array( - 183 - 过滤的规则是一个函数,如果设置传入整个数据对象,那么函数的参数就是整个数据对象,默认是传入数 据对象中该字段的值。 举例说明,例如我们需要在发表文章的时候对文章内容进行安全过滤,并且希望在读取的时候进行截取前 面255个字符,那么可以设置: 其中,contentWriteFilter是自定义的对字符串进行安全过滤的函数,而contentReadFilter是自定义的 一个对内容进行截取的函数。通常我们可以在项目的公共函数文件里面定义这些函数。 序列化字段 序列化字段是新版推出的新功能,可以用简单的数据表字段完成复杂的表单数据存储,尤其是动态的表单 数据字段。 要使用序列化字段的功能,只需要在模型中定义serializeField属性,定义格式如下: Info是数据表中的实际存在的字段,保存到其中的值是name、email和address三个表单字段的序列化结 果。序列化字段功能可以在数据写入的时候进行自动序列化,并且在读出数据表的时候自动反序列化,这 一切都无需手动进行。 下面还是是User数据表为例,假设其中并不存在name、email和address字段,但是设计了一个文本类型 的info字段,那么下面的代码是可行的: 2. '过滤的字段'=>array('写入过滤规则','读取过滤规则',是否传入整个数据对象), 3. ) 1. protected $_filter = array( 2. 'content'=>array('contentWriteFilter','contentReadFilter'), 3. ) 1. protected $serializeField = array( 2. 'info' => array('name', 'email', 'address'), 3. ); 1. $User = D("User"); // 实例化User对象 2. // 然后直接给数据对象赋值 3. $User->name = 'ThinkPHP'; 4. $User->email = 'ThinkPHP@gmail.com'; 5. $User->address = '上海徐汇区'; 6. // 把数据对象添加到数据库 name email和address会自动序列化后保存到info字段 7. $User->add(); 8. 查询用户数据信息 9. $User->find(8); - 184 - 文本字段 ThinkPHP支持数据模型中的个别字段采用文本方式存储,这些字段就称为文本字段,通常可以用于某些 Text或者Blob字段,或者是经常更新的数据表字段。 要使用文本字段非常简单,只要在模型里面定义blobFields属性就行了。例如,我们需要对Blog模型的 content字段使用文本字段,那么就可以使用下面的定义: 系统在查询和写入数据库的时候会自动检测文本字段,并且支持多个字段的定义。 需要注意的是:对于定义的文本字段并不需要数据库有对应的字段,完全是另外的。而且,暂时不 支持对文本字段的搜索功能。 只读字段 只读字段用来保护某些特殊的字段值不被更改,这个字段的值一旦写入,就无法更改。 要使用只读字段的 功能,我们只需要在模型中定义readonlyField属性 例如,上面定义了当前模型的name和email字段为只读字段,不允许被更改。也就是说当执行save方法 之前会自动过滤到只读字段的值,避免更新到数据库。 下面举个例子说明下: 10. // 查询结果会自动把info字段的值反序列化后生成name、email和address属性 11. // 输出序列化字段 12. echo $User->name; 13. echo $User->email; 14. echo $User->address; 1. Protected $blobFields = array('content'); 1. protected $readonlyField = array('name', 'email'); 1. $User = D("User"); // 实例化User对象 2. $User->find(8); 3. // 更改某些字段的值 4. $User->name = 'TOPThink'; 5. $User->email = 'Topthink@gmail.com'; 6. $User->address = '上海静安区'; 7. // 保存更改后的用户数据 8. $User->save(); - 185 - 事实上,由于我们对name和email字段设置了只读,因此只有address字段的值被更新了,而name和 email的值仍然还是更新之前的值。 悲观锁和乐观锁 业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针 对某个时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据 再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的 机制,在这里,也就是所谓的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。 ThinkPHP支持两种锁机制:即通常所说的 “ 悲观锁( Pessimistic Locking ) ”和 “ 乐观锁( Optimistic Locking ) ” 。 悲观锁( Pessimistic Locking ) 悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处 理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数 据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统 中实现了加锁机制,也无法保证外部系统不会修改数据)。 通常是使用for update子句来实现悲观锁机 制。 ThinkPHP支持悲观锁机制,默认情况下,是关闭悲观锁功能的,要在查询和更新的时候启用悲观锁功 能,可以通过使用之前提到的查询锁定方法,例如: 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实 现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这 样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上 进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出 数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终 处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。乐观锁机制在一定 程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为 数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 ThinkPHP也可以支持乐观锁机制,要启用乐观锁,只需要继承高级模型类并定义模型的optimLock属 性,并且在数据表字段里面增加相应的字段就可以自动启用乐观锁机制了。默认的optimLock属性是 lock_version,也就是说如果要在User表里面启用乐观锁机制,只需要在User表里面增加lock_version 字段,如果有已经存在的其它字段作为乐观锁用途,可以修改模型类的optimLock属性即可。如果存在 optimLock属性对应的字段,但是需要临时关闭乐观锁机制,把optimLock属性设置为false就可以了。 1. $User->lock(true)->save($data);// 使用悲观锁功能 - 186 - 延迟更新 我们经常需要给某些数据表添加一些需要经常更新的统计字段,例如用户的积分、文件的下载次数等等, 而当这些数据更新的频率比较频繁的时候,数据库的压力也随之增大不少,我们可以利用高级模型的延迟 更新功能缓解。 延迟更新功能是指我们可以给统计字段的更新设置一个延迟时间,在这个时间段内所有的更新会被累积缓 存起来,然后定时地统一更新数据库。这比较适合某个字段经常需要递增或者递减,并且对实时性要求没 有那么严格的情况。 我们先来看递增的情况,如果我们需要给会员累积积分,可以使用 上面的操作更新了两次用户积分,并且都实时保存到数据库 如果我们使用延迟更新方法,例如下面对用户 的积分延迟更新60秒 那么60秒内执行的所有积分更新操作都会被延迟,实际会在60秒后统一更新积分到数据库,而不是每次 都更新数据库。临时积分会被累积并缓存起来,最后到了延迟更新时间,再统一更新。相当于在60秒后执 行了: 效果是等效。区别在于用户数据库中的积分不是实时的。 同样,还可以使用setLazyDec进行延迟更新操 作。 数据分表 对于大数据量的应用,经常会对数据进行分表,有些情况是可以利用数据库的分区功能,但并不是所有的 数据库或者版本都支持,因此我们可以利用ThinkPHP内置的数据分表功能来实现。帮助我们更方便的进 行数据的分表和读取操作。 和数据库分区功能不同,内置的数据分表功能需要根据分表规则手动创建相应的数据表。 在需要分表的模型中定义partition属性即可。 1. $User = D("User"); // 实例化User对象 2. $User->where('id=3')->setInc("score",10);// 用户的积分加10 3. $User->where('id=3')->setInc("score",30);// 用户的积分加30 1. $User->where('id=3')->setLazyInc("score",10,60); 2. $User->where('id=3')->setLazyInc("score",30,60); 3. $User->where('id=3')->setLazyInc("score",10,60); 1. $User->where('id=3')->setInc("score",50); 1. protected $partition = array( 2. 'field' => 'name',// 要分表的字段 通常数据会根据某个字段的值按照规则进行分表 - 187 - Mongo模型 定义好了分表属性后,我们就可以来进行CURD操作了,唯一不同的是,获取当前的数据表不再使用 getTableName方法,而是使用getPartitionTableName方法,而且必须传入当前的数据。然后根据数 据分析应该实际操作哪个数据表。因此,分表的字段值必须存在于传入的数据中,否则会进行联合查询。 返回类型 系统默认的数据库查询返回的是数组,我们可以给单个数据设置返回类型,以满足特殊情况的需要,例 如: 返回自定义的User类,类的架构方法的参数是传入的数据。例如: Mongo模型是专门为Mongo数据库驱动而支持的Model扩展,如果需要操作Mongo数据库的话,自定 义的模型类必须继承Think\Model\MongoModel。 Mongo模型为操作Mongo数据库提供了更方便的实用功能和查询用法,包括: 1. 对MongoId对象和非对象主键的全面支持; 2. 保持了动态追加字段的特性; 3. 数字自增字段的支持; 4. 执行SQL日志的支持; 3. 'type' => 'md5',// 分表的规则 包括id year mod md5 函数 和首字母 4. 'expr' => 'name',// 分表辅助表达式 可选 配合不同的分表规则 5. 'num' => 'name',// 分表的数目 可选 实际分表的数量 6. ); 1. $User = M("User"); // 实例化User对象 2. // 返回结果是一个数组数据 3. $data = $User->find(6); 4. // 返回结果是一个stdClass对象 5. $data = $User->returnResult($data, "object"); 6. // 还可以返回自定义的类 7. $data = $User->returnResult($data, "User"); 1. Class User { 2. public function __construct($data){ 3. // 对$data数据进行处理 4. } 5. } - 188 - 5. 字段自动检测的支持; 6. 查询语言的支持; 7. MongoCode执行的支持; 主键 系统很好的支持Mongo的主键类型,Mongo默认的主键名是 _id,也可以通过设置pk属性改变主键名称 (也许你需要用其他字段作为数据表的主键),例如: 主键支持三种类型(通过_idType属性设置),分别是: 类型 描述 self::TYPE_OBJECT 或者1 (默认类型) 采用MongoId对象,写入或者查询的时候传入数字或者字符会自动 转换,获取的时候会自动转换成字符串。 self::TYPE_INT或者 2 整形,支持自动增长,通过设置_autoInc 属性 self::TYPE_STRING 或者3 字符串hash 设置主键类型示例: 字段检测 MongoModel默认关闭字段检测,是为了保持Mongo的动态追加字段的特性,如果你的应用不需要使 用Mongo动态追加字段的特性,可以设置 autoCheckFields 为true即可开启字段检测功能,提高安全 性。一旦开启字段检测功能后,系统会自动查找当前数据表的第一条记录来获取字段列表。 1. namespace Home\Model; 2. use Think\Model\MongoModel; 3. Class UserModel extends MongoModel { 4. Protected $pk = 'id'; 5. } 1. namespace Home\Model; 2. use Think\Model\MongoModel; 3. Class UserModel extends MongoModel { 4. Protected $_idType = self::TYPE_INT; 5. protected $_autoinc = true; 6. } - 189 - 如果你关闭字段检测功能的话,将不能使用查询的字段排除功能。 连贯操作 MongoModel中有部分连贯操作暂时不支持,包括:group、union、join、having、lock和distinct操 作。其他连贯操作都可以很好的支持,例如: 查询支持 Mongo数据库的查询条件和其他数据库有所区别。 1. 首先,支持所有的普通查询和快捷查询; 2. 表达式查询增加了一些针对MongoDb的查询用法; 3. 统计查询目前只能支持count操作,其他的可能要自己通过MongoCode来实现了; MongoModel的组合查询支持 MongoModel提供了MongoCode方法,可以支持MongoCode方式的查询或者操作。 表达式查询 表达式查询采用下面的方式: 因为MongoDb的特性,MongoModel的表达式查询和其他的数据库有所区别,增加了一些新的用法。 表达式不分大小写,支持的查询表达式和Mongo原生的查询语法对照如下: 查询表达式 含义 Mongo原生查询条件 neq 或者ne 不等于 $ne lt 小于 $lt lte 或者elt 小于等于 $lte gt 大于 $gt gte 或者egt 大于等于 $gte 1. $Model = new Think\Model\MongoModel("User"); 2. $Model->field("name,email,age")->order("status desc")->limit("10,8")->select(); 1. _string 采用MongoCode查询 2. _query 和其他数据库的请求字符串查询相同 3. _complex MongoDb暂不支持 1. $map['字段名'] = array('表达式','查询条件'); - 190 - like 模糊查询 用MongoRegex正则模拟 无 mod 取模运算 $mod in in查询 $in nin或者not in not in查询 $nin all 满足所有条件 $all between 在某个的区间 无 not between 不在某个区间 无 exists 字段是否存在 $exists size 限制属性大小 $size type 限制字段类型 $type regex MongoRegex正则查询 MongoRegex实现 exp 使用MongoCode查询 无 注意,在使用like查询表达式的时候,和mysql的方式略有区别,对应关系如下: Mysql模糊查询 Mongo模糊查询 array('like','%thinkphp%'); array('like','thinkphp'); array('like','thinkphp%'); array('like','^thinkphp'); array('like','%thinkphp'); array('like','thinkphp$'); LIKE: 同sql的LIKE 例如: 查询条件就变成 name like 'thinkphp%' 设置支持 Mongo的数据更新设置用于数据保存和写入操作,可以支持: 表达式 含义 Mongo原生用 法 inc 数字字段增长或减少 $inc set 字段赋值 $set 1. $map['name'] = array('like','^thinkphp'); - 191 - unset 删除字段值 $unset push 追加一个值到字段(必须是数组类型)里面去 $push pushall 追加多个值到字段(必须是数组类型)里面去 $pushall addtoset 增加一个值到字段(必须是数组类型)内,而且只有当这个值不在数组内 才增加 $addtoset pop 根据索引删除字段(必须是数组字段)中的一个值 $pop pull 根据值删除字段(必须是数组字段)中的一个值 $pull pullall 一次删除字段(必须是数组字段)中的多个值 $pullall 例如, 其他 MongoModel增加了几个方法 mongoCode 执行MongoCode getMongoNextId ([字段名]) 获取自增字段的下一个ID,可用于数字主键或者其他需要自增的字段,参 数为空的时候表示或者主键的。 Clear 清空当前数据表方法 1. $data['id'] = 5; 2. $data['score'] = array('inc',2); 3. $Model->save($data); - 192 - 视图 模板定义 每个模块的模板文件是独立的,为了对模板文件更加有效的管理,ThinkPHP对模板文件进行目录划分, 默认的模板文件定义规则是: 视图目录/[模板主题/]控制器名/操作名+模板后缀 默认的视图目录是模块的View目录(模块可以有多个视图文件目录,这取决于你的应用需要),框架的默 认视图文件后缀是 .html 。 新版模板主题默认是空(表示不启用模板主题功能)。 在每个模板主题下面,是以模块下面的控制器名为目录,然后是每个控制器的具体操作模板文件,例如: User控制器的add操作 对应的模板文件就应该是: View/User/add.html 如果你的默认视图层不是View,例如: 那么,对应的模板文件就变成了: Template/User/add.html 。 模板文件的默认后缀的情况是 .html ,也可以通过 TMPL_TEMPLATE_SUFFIX 来配置成其他的。例 如,我们可以配置: 定义后,User控制器的add操作 对应的模板文件就变成是: View/User/add.tpl 如果觉得目录结构太深,可以通过设置 TMPL_FILE_DEPR 参数来配置简化模板的目录层次,例如设置: 默认的模板文件就变成了: View/User_add.html 除了采用系统默认的模板定位规则之外,我们可以在模块的配置文件中单独定义视图目录,例如: 把视图目录指定到最外层的Theme目录下面,而不是放到当前模块的View目录下面。 原来的 ./Application/Home/User/add.html 变成了 ./Theme/Home/User/add.html 。 利用这一功能可以让很多系统的模板目录独立出来更加方便修改。 注意:在3.2.2版本中,如果设置 1. 'DEFAULT_V_LAYER' => 'Template', // 设置默认的视图层名称 1. 'TMPL_TEMPLATE_SUFFIX'=>'.tpl' 1. 'TMPL_FILE_DEPR'=>'_' 1. 'VIEW_PATH'=>'./Theme/' - 193 - 模板主题 模板赋值 了 VIEW_PATH ,则不需要再创建模块子目录了。原来的 ./Application/Home/User/add.html 直 接变成了 ./Theme/User/add.html 。 一个模块如果需要支持多套模板文件的话,就可以使用模板主题功能。 默认情况下,没有开启模板主题功 能,如果需要开启,设置 DEFAULT_THEME 参数即可: 采用模板主题后,需要在视图目录下面创建对应的主题目录,和不启用模板主题的情况相比,模板文件只 是多了一层目录: 在视图渲染输出之前,我们可以通过动态设置来改变需要使用的模板主题。 如果要在模板中输出变量,必须在在控制器中把变量传递给模板,系统提供了assign方法对模板变量赋 值,无论何种变量类型都统一使用assign赋值。 assign方法必须在 display和show方法 之前调用,并且系统只会输出设定的变量,其它变量不会输出 (系统变量例外),一定程度上保证了变量的安全性。 系统变量可以通过特殊的标签输出,无需赋值模板变量 赋值后,就可以在模板文件中输出变量了,如果使用的是内置模板的话,就可以这样输出: {$name} 如果要同时输出多个模板变量,可以使用下面的方式: 1. // 设置默认的模板主题 2. 'DEFAULT_THEME' => 'default' 1. View/User/add.html // 没有启用模板主题之前 2. View/default/User/add.html // 启用模板主题之后 1. // 在控制器中动态改变模板主题 2. $this->theme('blue')->display('add'); 1. $this->assign('name',$value); 2. // 下面的写法是等效的 3. $this->name = $value; - 194 - 模板渲染 这样,就可以在模板文件中同时输出name、email和phone三个变量。 模板变量的输出根据不同的模板引擎有不同的方法,我们在后面会专门讲解内置模板引擎的用法。如果你 使用的是PHP本身作为模板引擎的话 ,就可以直接在模板文件里面输出了: 如果采用内置的模板引擎,可以使用: {$name} [ {$email} {$phone} ] 输出同样的内容。 关于更多的模板标签使用,我们会在后面模板标签中详细讲解。 模板定义后就可以渲染模板输出,系统也支持直接渲染内容输出,模板赋值必须在模板渲染之前操作。 渲染模板 渲染模板输出最常用的是使用display方法,调用格式: display('[模板文件]'[,'字符编码'][,'输出类型']) 模板文件的写法支持下面几种: 用法 描述 不带任何参数 自动定位当前操作的模板文件 [模块@][控制器:][操作] 常用写法,支持跨模块 模板主题可以和theme方法配合 完整的模板文件名 直接使用完整的模板文件名(包括模板后缀) 下面是一个最典型的用法,不带任何参数: 表示系统会按照默认规则自动定位模板文件,其规则是: 如果当前没有启用模板主题则定位到: 当前模块/默认视图目录/当前控制器/当前操作.html 如果有启用 1. $array['name'] = 'thinkphp'; 2. $array['email'] = 'liu21st@gmail.com'; 3. $array['phone'] = '12335678'; 4. $this->assign($array); 1. // 不带任何参数 自动定位当前操作的模板文件 2. $this->display(); - 195 - 模板主题则定位到: 当前模块/默认视图目录/当前主题/当前控制器/当前操作.html 如果有更改TMPL_FILE_DEPR设置(假设 'TMPL_FILE_DEPR'=>'_' )的话,则上面的自动定位规则变 成: 当前模块/默认视图目录/当前控制器_当前操作.html 和 当前模块/默认视图目录/当前主题/当前 控制器_当前操作.html 。 所以通常display方法无需带任何参数即可输出对应的模板,这是模板输出的最简单的用法。 通常默认的视图目录是View 如果没有按照模板定义规则来定义模板文件(或者需要调用其他控制器下面的某个模板),可以使用: 表示调用当前模块下面的edit模板 表示调用Member模块下面的read模板。 如果我们使用了模板主题功能,那么也可以支持跨主题调用,使用: 表示调用blue主题下面的User控制器的edit模板。 如果你不希望每个主题都重复定义一些相同的模版文件的话,还可以启用差异主题定义方式,设置: 设置后,如果blue主题下面不存在edit模板的话,就会自动定位到默认主题中的edit模板。 渲染输出不需要写模板文件的路径和后缀,确切地说,这里面的控制器和操作并不一定需要有实际对应的 控制器和操作,只是一个目录名称和文件名称而已,例如,你的项目里面可能根本没有Public控制器,更 没有Public控制器的menu操作,但是一样可以使用 输出这个模板文件。理解了这个,模板输出就清晰了。 display方法支持在渲染输出的时候指定输出编码和类型,例如,可以指定编码和类型: 1. // 指定模板输出 2. $this->display('edit'); 1. $this->display('Member:read'); 1. $this->theme('blue')->display('User:edit'); 1. 'TMPL_LOAD_DEFAULTTHEME'=>true 1. $this->display('Public:menu'); 1. $this->display('read', 'utf-8', 'text/xml'); - 196 - 获取模板地址 表示输出XML页面类型(配合你的应用需求可以输出很多类型)。 事情总有特例,如果的模板目录是自定义的,或者根本不需要按模块进行分目录存放,那么默认的 display渲染规则就不能处理,这个时候,我们就需要使用另外一种方式来应对,直接传入模板文件名即 可,例如: 这种方式需要指定模板路径和后缀,这里的Template/Public目录是位于当前项目入口文件位置下面。如 果是其他的后缀文件,也支持直接输出,例如: $this->display('./Template/Public/menu.tpl'); 只要 ./Template/Public/menu.tpl 是一个实际存在的模板文件。 要注意模板文件位置是相对于项目的入口文件,而不是模板目录。 为了更方便的输出模板文件,新版封装了一个T函数用于生成模板文件名。 用法: T([资源://][模块@][主题/][控制器/]操作,[视图分层]) T函数的返回值是一个完整的模板文件名,可以直接用于display和fetch方法进行渲染输出。 例如: 1. $this->display('./Template/Public/menu.html'); 1. T('Public/menu'); 2. // 返回 当前模块/View/Public/menu.html 3. T('blue/Public/menu'); 4. // 返回 当前模块/View/blue/Public/menu.html 5. T('Public/menu','Tpl'); 6. // 返回 当前模块/Tpl/Public/menu.html 7. T('Public/menu'); 8. // 如果TMPL_FILE_DEPR 为 _ 返回 当前模块/Tpl/Public_menu.html 9. T('Public/menu'); 10. // 如果TMPL_TEMPLATE_SUFFIX 为.tpl 返回 当前模块/Tpl/Public/menu.tpl 11. T('Admin@Public/menu'); 12. // 返回 Admin/View/Public/menu.html 13. T('Extend://Admin@Public/menu'); - 197 - 获取内容 模板引擎 在display方法中直接使用T函数: T函数可以输出不同的视图分层模板。 如果需要获取渲染模板的输出内容而不是直接输出,可以使用fetch方法。 fetch方法的用法和display基本一致(只是不需要指定输出编码和输出类型): fetch('模板文件') 模板文件的调用方法和display方法完全一样,区别就在于fetch方法渲染后不是直接输出,而是返回渲染 后的内容,例如: 使用fetch方法获取渲染内容后,你可以进行过滤和替换等操作,或者用于对输出的复杂需求。 渲染内容 如果你没有定义任何模板文件,或者把模板内容存储到数据库中的话,你就需要使用show方法来渲染输 出了,show方法的调用格式: show('渲染内容'[,'字符编码'][,'输出类型']) 例如, $this->show($content); 也可以指定编码和类型: $this->show($content, 'utf-8', 'text/xml'); show方法中的内容也可以支持模板解析。 系统支持原生的PHP模板,而且本身内置了一个基于XML的高效的编译型模板引擎,系统默认使用的模板 引擎是内置模板引擎,关于这个模板引擎的标签详细使用可以参考模版引擎部分。 14. // 返回 Extend/Admin/View/Public/menu.html (Extend目录取决于AUTOLOAD_NAMESPACE中 的配置) 1. // 使用T函数输出模板 2. $this->display(T('Admin@Public/menu')); 1. $content = $this->fetch('Member:edit'); - 198 - 内置的模板引擎也可以直接支持在模板文件中采用PHP原生代码和模板标签的混合使用,如果需要完全使 用PHP本身作为模板引擎,可以配置: 'TMPL_ENGINE_TYPE' =>'PHP' 可以达到最佳的效率。 如果你使用了其他的模板引擎,只需要设置TMPL_ENGINE_TYPE参数为相关的模板引擎名称即可。 - 199 - 模板 变量输出 本章的内容主要讲述了如何使用内置的模板引擎来定义模板文件,以及使用加载文件、模板布局和模板继 承等高级功能。 ThinkPHP内置了一个基于XML的性能卓越的模板引擎 ThinkTemplate,这是一个专门为ThinkPHP服务 的内置模板引擎。ThinkTemplate是一个使用了XML标签库技术的编译型模板引擎,支持两种类型的模 板标签,使用了动态编译和缓存技术,而且支持自定义标签库。其特点包括: 支持XML标签库和普通标签的混合定义; 支持直接使用PHP代码书写; 支持文件包含; 支持多级标签嵌套; 支持布局模板功能; 一次编译多次运行,编译和运行效率非常高; 模板文件和布局模板更新,自动更新模板缓存; 系统变量无需赋值直接输出; 支持多维数组的快速输出; 支持模板变量的默认值; 支持页面代码去除Html空白; 支持变量组合调节器和格式化功能; 允许定义模板禁用函数和禁用PHP语法; 通过标签库方式扩展。 每个模板文件在执行过程中都会生成一个编译后的缓存文件,其实就是一个可以运行的PHP文件。模板缓 存默认位于项目的Runtime/模块/Cache目录下面,以模板文件的md5编码作为缓存文件名保存的。如果 在模板标签的使用过程中发现问题,可以尝试通过查看模板缓存文件找到问题所在。 内置的模板引擎支持普通标签和XML标签方式两种标签定义,分别用于不同的目的: 标签类型 描述 普通标签 主要用于输出变量和做一些基本的操作 XML标签 主要完成一些逻辑判断、控制和循环输出,并且可扩展 这种方式的结合保证了模板引擎的简洁和强大的有效融合。 - 200 - 在模板中输出变量的方法很简单,例如,在控制器中我们给模板变量赋值: 然后就可以在模板中使用: 模板编译后的结果就是: 这样,运行的时候就会在模板中显示: Hello,ThinkPHP! 注意模板标签的 { 和 $ 之间不能有任何的空格,否则标签无效。所以,下面的标签 将不会正常输出name变量,而是直接保持不变输出: Hello,{ $name}! 普通标签默认开始标记是 { ,结束标记是 } 。也可以通过设置 TMPL_L_DELIM 和 TMPL_R_DELIM 进行更 改。例如,我们在项目配置文件中定义: 那么,上面的变量输出标签就应该改成: 后面的内容我们都以默认的标签定义来说明。 模板标签的变量输出根据变量类型有所区别,刚才我们输出的是字符串变量,如果是数组变量, 那么,在模板中我们可以用下面的方式输出: 1. $name = 'ThinkPHP'; 2. $this->assign('name',$name); 3. $this->display(); 1. Hello,{$name}! 1. Hello,! 1. Hello,{ $name}! 1. 'TMPL_L_DELIM'=>'<{', 2. 'TMPL_R_DELIM'=>'}>', 1. Hello,<{$name}>! 1. $data['name'] = 'ThinkPHP'; 2. $data['email'] = 'thinkphp@qq.com'; 3. $this->assign('data',$data); 1. Name:{$data.name} 2. Email:{$data.email} - 201 - 系统变量 或者用下面的方式也是有效: 当我们要输出多维数组的时候,往往要采用后面一种方式。 如果data变量是一个对象(并且包含有name和email两个属性),那么可以用下面的方式输出: 或者 系统变量输出 普通的模板变量需要首先赋值后才能在模板中输出,但是系统变量则不需要,可以直接在模板中输出,系 统变量的输出通常以{$Think 打头,例如: 支持输出 $_SERVER 、 $_ENV 、 $_POST 、 $_GET 、 $_REQUEST 、 $_SESSION 和 $_COOKIE 变 量。 常量输出 还可以输出常量 或者直接使用 配置输出 1. Name:{$data['name']} 2. Email:{$data['email']} 1. Name:{$data:name} 2. Email:{$data:email} 1. Name:{$data->name} 2. Email:{$data->email} 1. {$Think.server.script_name} // 输出$_SERVER['SCRIPT_NAME']变量 2. {$Think.session.user_id} // 输出$_SESSION['user_id']变量 3. {$Think.get.pageNumber} // 输出$_GET['pageNumber']变量 4. {$Think.cookie.name} // 输出$_COOKIE['name']变量 1. {$Think.const.MODULE_NAME} 1. {$Think.MODULE_NAME} - 202 - 使用函数 输出配置参数使用: 语言变量 输出语言变量可以使用: 我们往往需要对模板输出变量使用函数,可以使用: 编译后的结果是: 如果函数有多个参数需要调用,则使用: 表示date函数传入两个参数,每个参数用逗号分割,这里第一个参数是 y-m-d ,第二个参数是前面要输 出的 create_time 变量,因为该变量是第二个参数,因此需要用###标识变量位置,编译后的结果是: 如果前面输出的变量在后面定义的函数的第一个参数,则可以直接使用: 表示输出 虽然也可以使用: 但完全没用这个必要。 1. {$Think.config.db_charset} 2. {$Think.config.url_model} 1. {$Think.lang.page_error} 2. {$Think.lang.var_error} 1. {$data.name|md5} 1. 1. {$create_time|date="y-m-d",###} 1. 1. {$data.name|substr=0,3} 1. 1. {$data.name|substr=###,0,3} - 203 - 默认值输出 使用运算符 还可以支持多个函数过滤,多个函数之间用“|”分割即可,例如: 编译后的结果是: 函数会按照从左到右的顺序依次调用。 如果你觉得这样写起来比较麻烦,也可以直接这样写: 变量输出使用的函数可以支持内置的PHP函数或者用户自定义函数,甚至是静态方法。 我们可以给变量输出提供默认值,例如: 对系统变量依然可以支持默认值输出,例如: 默认值和函数可以同时使用,例如: 我们可以对模板输出使用运算符,包括对“+”“ –” “*” “/”和“%”的支持。 例如: 运算符 使用示例 + {$a+$b} - {$a-$b} * {$a*$b} / {$a/$b} 1. {$name|md5|strtoupper|substr=0,3} 1. 1. {:substr(strtoupper(md5($name)),0,3)} 1. {$user.nickname|default="这家伙很懒,什么也没留下"} 1. {$Think.get.name|default="名称为空"} 1. {$Think.get.name|getName|default="名称为空"} - 204 - 标签库 % {$a%$b} ++ {$a++} 或 {++$a} -- {$a--} 或 {--$a} 综合运算 {$a+$b*10+$c} 在使用运算符的时候,不再支持点语法和常规的函数用法,例如: 内置的模板引擎除了支持普通变量的输出之外,更强大的地方在于标签库功能。 标签库类似于Java的Struts中的JSP标签库,每一个标签库是一个独立的标签库文件,标签库中的每一个 标签完成某个功能,采用XML标签方式(包括开放标签和闭合标签)。 标签库分为内置和扩展标签库,内置标签库是Cx标签库。 导入标签库 使用taglib标签导入当前模板中需要使用的标签库,例如: 如果没有定义html标签库的话,则导入无效。 也可以导入多个标签库,使用: 导入标签库后,就可以使用标签库中定义的标签了,假设article标签库中定义了read标签: 1. {$user.score+10} //错误的 2. {$user['score']+10} //正确的 3. {$user['score']*$user['level']} //正确的 4. {$user['score']|myFun*10} //错误的 5. {$user['score']+myFun($user['level'])} //正确的 1. 1. 1. 2. {$data.id}:{$data.title} 3. - 205 - 在上面的标签中, ... 就是闭合标签,起始和结束标签必须成对出 现。 如果是 就是开放标签。 闭合和开放标签取决于标签库中的定义,一旦定义后就不能混淆使用,否则就会出现错误。 内置标签 内置标签库无需导入即可使用,并且不需要加XML中的标签库前缀,ThinkPHP内置的标签库是Cx标签 库,所以,Cx标签库中的所有标签,我们可以在模板文件中直接使用,我们可以这样使用: 如果Cx不是内置标签的话,可能就需要这么使用了: 更多的Cx标签库中的标签用法,参考内置标签。 内置标签库可以简化模板中标签的使用,所以,我们还可以把其他的标签库定义为内置标签库(前提是多 个标签库没有标签冲突的情况),例如: 配置后,上面的标签用法就可以改为: 标签库预加载 标签库预加载是指无需手动在模板文件中导入标签库即可使用标签库中的标签,通常用于某个标签库需要 被大多数模板使用的情况。 在应用或者模块的配置文件中添加: 1. 2. {$data.id}:{$data.title} 3. 1. 'TAGLIB_PRE_LOAD' => 'article,html' - 206 - 模板继承 设置后,模板文件就不再需要使用 但是仍然可以在模板中调用: 模板继承是一项更加灵活的模板布局方式,模板继承不同于模板布局,甚至来说,应该在模板布局的上 层。模板继承其实并不难理解,就好比类的继承一样,模板也可以定义一个基础模板(或者是布局),并 且其中定义相关的区块(block),然后继承(extend)该基础模板的子模板中就可以对基础模板中定义 的区块进行重载。 因此,模板继承的优势其实是设计基础模板中的区块(block)和子模板中替换这些区块。 每个区块由 标签组成。 下面就是基础模板中的一个典型的区块设计(用于设计网站标 题): block标签必须指定name属性来标识当前区块的名称,这个标识在当前模板中应该是唯一的,block标签 中可以包含任何模板内容,包括其他标签和变量,例如: 你甚至还可以在区块中加载外部文件: 一个模板中可以定义任意多个名称标识不重复的区块,例如下面定义了一个 base.html 基础模板: 1. 1. 2. {$data.id}:{$data.title} 3. 1. 网站标题 1. {$web_title} 1. 1. 2. 3. 4. 标题 5. 6. 7. 菜单 - 207 - 然后我们在子模板(其实是当前操作的入口模板)中使用继承: 可以看到,子模板中使用了extend标签定义需要继承的模板,extend标签的用法和include标签一样,你 也可以加载其他模板: 或者使用绝对文件路径加载 8. 左边分栏 9. 主内容 10. 右边分栏 11. 底部 12. 13. 1. 2. {$title} 3. 4. 首页 5. 资讯 6. 论坛 7. 8. 9. 10. 11. {$vo.title}
12. {$vo.content} 13.
14.
15. 16. 最新资讯: 17. 18. {$new.title}
19.
20.
21. 22. @ThinkPHP2012 版权所有 23. 1. - 208 - 修改定界符 在当前子模板中,只能定义区块而不能定义其他的模板内容,否则将会直接忽略,并且只能定义基础模板 中已经定义的区块。 例如,如果采用下面的定义: 导航部分将是无效的,不会显示在模板中。 在子模板中,可以对基础模板中的区块进行重载定义,如果没有重新定义的话,则表示沿用基础模板中的 区块定义,如果定义了一个空的区块,则表示删除基础模板中的该区块内容。 上面的例子,我们就把left 区块的内容删除了,其他的区块都进行了重载。 子模板中的区块定义顺序是随意的,模板继承的用法关键在于基础模板如何布局和设计规划了,如果结合 原来的布局功能,则会更加灵活。 模板文件可以包含普通模板标签和XML模板标签,标签的定界符都可以重新配置。 普通标签 内置模板引擎的普通模板标签默认以{ 和 } 作为开始和结束标识,并且在开始标记紧跟标签的定义,如果 之间有空格或者换行则被视为非模板标签直接输出。 例如: {$name} 、 {$vo.name} 、 {$vo['name']|strtoupper} 都属于普通模板标签。 要更改普遍模板的起始标签和结束标签,请使用下面的配置参数: 例如在项目配置文件中增加下面的配置: 普通标签的定界符就被修改了,原来的 {$name} 和 {$vo.name} 必须使用 <{$name}> 和 1. 1. {$title} 2. 首页 3. 资讯 4. 论坛 1. TMPL_L_DELIM //模板引擎普通标签开始标记 2. TMPL_R_DELIM //模板引擎普通标签结束标记 1. 'TMPL_L_DELIM' => '<{', 2. 'TMPL_R_DELIM' => '>}' - 209 - 三元运算 <{$vo.name}> 才能生效了。 如果你定制了普通标签的定界符,记得修改下默认的系统模板。 XML标签 普通模板标签主要用于模板变量输出和模板注释。如果要使用其它功能,请使用XML模板标签。XML模 板标签可以用于模板变量输出、文件包含、条件控制、循环输出等功能,而且完全可以自己扩展功能。如 果你觉得XML标签无法在正在使用的编辑器里面无法编辑,还可以更改XML标签库的起始和结束标签, 请修改下面的配置参数: 例如在项目配置文件中增加下面的配置: 原来的 就必须改成 注意:XML标签和普通标签的定界符不能冲突,否则会导致解析错误。 模板可以支持三元运算符,例如: 1. TAGLIB_BEGIN //标签库标签开始标签 2. TAGLIB_END //标签库标签结束标记 1. 'TAGLIB_BEGIN'=>'[', 2. 'TAGLIB_END'=>']', 1. 2. 相等 3. 4. 不相等 5. 1. [eq name="name" value="value"] 2. 相等 3. [else/] 4. 不相等 5. [/eq] - 210 - 包含文件 注意:三元运算符中暂时不支持点语法。 在当前模版文件中包含其他的模版文件使用include标签,标签用法: 使用模版表达式 模版表达式的定义规则为:模块@主题/控制器/操作 例如: 为了兼容3.1的写法,也可以支持: 可以一次包含多个模版,例如: 注意,包含模版文件并不会自动调用控制器的方法,也就是说包含的其他模版文件中的变量赋值需 要在当前操作中完成。 使用模版文件 可以直接包含一个模版文件名(包含完整路径),例如: 传入参数 1. {$status?'正常':'错误'} 2. {$info['status']?$info['msg']:$info['error']} 1. 1. // 包含头部模版header 2. // 包含菜单模版menu 3. // 包含blue主题下面的menu模版 1. 2. 3. 1. 1. - 211 - 内置标签 无论你使用什么方式包含外部模板,Include标签支持在包含文件的同时传入参数,例如,下面的例子我 们在包含header模板的时候传入了title和keywords变量: 就可以在包含的header.html文件里面使用title和keywords变量,如下: 注意:由于模板解析的特点,从入口模板开始解析,如果外部模板有所更改,模板引擎并不会重新 编译模板,除非在调试模式下或者缓存已经过期。如果部署模式下修改了包含的外部模板文件后, 需要把模块的缓存目录清空,否则无法生效。 变量输出使用普通标签就足够了,但是要完成其他的控制、循环和判断功能,就需要借助模板引擎的标签 库功能了,系统内置标签库的所有标签无需引入标签库即可直接使用。 内置标签包括: 标签名 作用 包含属性 include 包含外部模板文件(闭合) file import 导入资源文件(闭合 包括js css load别名) file,href,type,value,basepath volist 循环数组数据输出 name,id,offset,length,key,mod foreach 数组或对象遍历输出 name,item,key for For循环数据输出 name,from,to,before,step switch 分支判断输出 name case 分支判断输出(必须和switch配套使用) value,break default 默认情况输出(闭合 必须和switch配套使用) 无 compare 比较输出(包括eq neq lt gt egt elt heq nheq等别 name,value,type 1. 1. 2. 3. [title] 4. 5. - 212 - Volist标签 compare 名) name,value,type range 范围判断输出(包括in notin between notbetween 别名) name,value,type present 判断是否赋值 name notpresent 判断是否尚未赋值 name empty 判断数据是否为空 name notempty 判断数据是否不为空 name defined 判断常量是否定义 name notdefined 判断常量是否未定义 name define 常量定义(闭合) name,value assign 变量赋值(闭合) name,value if 条件判断输出 condition elseif 条件判断输出(闭合 必须和if标签配套使用) condition else 条件不成立输出(闭合 可用于其他标签) 无 php 使用php代码 无 volist标签通常用于查询数据集(select方法)的结果输出,通常模型的select方法返回的结果是一个二维 数组,可以直接使用volist标签进行输出。 在控制器中首先对模版赋值: 在模版定义如下,循环输出用户的编号和姓名: Volist标签的name属性表示模板赋值的变量名称,因此不可随意在模板文件中改变。id表示当前的循环变 量,可以随意指定,但确保不要和name属性冲突,例如: 1. $User = M('User'); 2. $list = $User->limit(10)->select(); 3. $this->assign('list',$list); 1. 2. {$vo.id}:{$vo.name}
3.
- 213 - 支持输出查询结果中的部分数据,例如输出其中的第5~15条记录 输出偶数记录 Mod属性还用于控制一定记录的换行,例如: 为空的时候输出提示: empty属性不支持直接传入html语法,但可以支持变量输出,例如: 然后在模板中使用: 输出循环变量 1. 2. {$data.id}:{$data.name}
3.
1. 2. {$vo.name} 3. 1. 2. {$vo.name} 3. 1. 2. {$vo.name} 3.
4.
1. 2. {$vo.id}|{$vo.name} 3. 1. $this->assign('empty','没有数据'); 2. $this->assign('list',$list); 1. 2. {$vo.id}|{$vo.name} 3. 1. - 214 - Foreach标签 For标签 如果没有指定key属性的话,默认使用循环变量i,例如: 如果要输出数组的索引,可以直接使用key变量,和循环变量不同的是,这个key是由数据本身决定,而不 是循环控制的,例如: 模板中可以直接使用函数设定数据集,而不需要在控制器中给模板变量赋值传入数据集变量,如: foreach标签类似与volist标签,只是更加简单,没有太多额外的属性,例如: {$vo.id}:{$vo.name} name表示数据源 item表示循环变量。 可以输出索引,如下: 也可以定义索引的变量名 2. {$k}.{$vo.name} 3. 1. 2. {$i}.{$vo.name} 3. 1. 2. {$key}.{$vo.name} 3. 1. 2. {$vo.name} 3. 1. 2. {$key}|{$vo.id}:{$vo.name} 3. 1. 2. {$k}|{$vo.id}:{$vo.name} 3. - 215 - Switch标签 用法: 开始值、结束值、步进值和循环变量都可以支持变量,开始值和结束值是必须,其他是可选。 comparison 的默认值是lt;;name的默认值是i,步进值的默认值是1,举例如下: 解析后的代码是 用法: 使用方法如下: 其中name属性可以使用函数以及系统变量,例如: 1. 2. 1. 2. {$i} 3. 1. for ($i=1;$i<100;$i+=1){ 2. echo $i; 3. } 1. 2. 输出内容1 3. 输出内容2 4. 默认情况 5. 1. 2. value1 3. value2 4. default 5. 1. 2. admin 3. default - 216 - 比较标签 对于case的value属性可以支持多个条件的判断,使用”|”进行分割,例如: 表示如果$_GET["type"] 是gif、png或者jpg的话,就判断为图像格式。 Case标签还有一个break属性,表示是否需要break,默认是会自动添加break,如果不要break,可以使 用: 也可以对case的value属性使用变量,例如: 使用变量方式的情况下,不再支持多个条件的同时判断。 比较标签用于简单的变量比较,复杂的判断条件可以用if标签替换,比较标签是一组标签的集合,基本上 用法都一致,如下: 系统支持的比较标签以及所表示的含义分别是: 4. 1. 2. 图像格式 3. 其他格式 4. 1. 2. admin 3. admin 4. default 5. 1. 2. admin 3. member 4. default 5. 1. <比较标签 name="变量" value="值"> 2. 内容 3. - 217 - 标签 含义 eq或者 equal 等于 neq 或者notequal 不等于 gt 大于 egt 大于等于 lt 小于 elt 小于等于 heq 恒等于 nheq 不恒等于 他们的用法基本是一致的,区别在于判断的条件不同,并且所有的比较标签都可以和else标签一起使用。 例如,要求name变量的值等于value就输出,可以使用: 或者 也可以支持和else标签混合使用: 当 name变量的值大于5就输出 当name变量的值不小于5就输出 比较标签中的变量可以支持对象的属性或者数组,甚至可以是系统变量,例如: 当vo对象的属性(或者 数组,或者自动判断)等于5就输出 1. value 1. value 1. 2. 相等 3. 4. 不相等 5. 1. value 1. value - 218 - 范围判断标签 当vo对象的属性等于5就输出 当$vo['name']等于5就输出 而且还可以支持对变量使用函数 当vo对象的属性值的字符串长度等于5就输出 变量名可以支持系统变量的方式,例如: 通常比较标签的值是一个字符串或者数字,如果需要使用变量,只需要在前面添加“$”标志: 当vo对象 的属性等于$a就输出 所有的比较标签可以统一使用compare标签(其实所有的比较标签都是compare标签的别名),例如: 当name变量的值等于5就输出 等效于 其中type属性的值就是上面列出的比较标签名称 范围判断标签包括in notin between notbetween四个标签,都用于判断变量是否中某个范围。 1. 2. {$vo.name} 3. 1. 2. {$vo.name} 3. 1. 2. {$vo.name} 3. 1. {$vo.name} 1. 相等不相等 1. {$vo.name} 1. value 1. value - 219 - IN和NOTIN 用法: 假设我们中控制器中给id赋值为1: 我们可以使用in标签来判断模板变量是否在某个范围内,例如: 最后会输出: id在范围内 。 如果判断不在某个范围内,可以使用: id不在范围内 可以把上面两个标签合并成为: name属性还可以支持直接判断系统变量,例如: 更多的系统变量用法可以参考系统变量部分。 value属性也可以使用变量,例如: $range变量可以是数组,也可以是以逗号分隔的字符串。 value属性还可以使用系统变量,例如: 1. $id = 1; 2. $this->assign('id',$id); 1. 2. id在范围内 3. 1. 2. id在范围内 3. 4. id不在范围内 5. 1. 2. $_GET['id'] 在范围内 3. 1. 2. id在范围内 3. 1. 2. id在范围内 - 220 - BETWEEN 和 NOTBETWEEN 可以使用between标签来判断变量是否在某个区间范围内,可以使用: 同样,可以使用notbetween标签来判断变量不在某个范围内: 当使用between标签的时候,value只需要一个区间范围,也就是只支持两个值,后面的值无效,例如 实际判断的范围区间是 1~3 ,而不是 1~10 ,也可以支持字符串判断,例如: name属性可以直接使用系统变量,例如: value属性也可以使用变量,例如: 变量的值可以是字符串或者数组,还可以支持系统变量。 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 3. 1. 2. 输出内容1 - 221 - IF标签 RANGE 也可以直接使用range标签,替换前面的判断用法: 其中type属性的值可以用in/notin/between/notbetween,其它属性的用法和IN或者BETWEEN一致。 用法示例: 在condition属性中可以支持eq等判断表达式,同上面的比较标签,但是不支持带有”>”、”<”等符 号的用法,因为会混淆模板解析,所以下面的用法是错误的: 必须改成: 除此之外,我们可以在condition属性里面使用php代码,例如: condition属性可以支持点语法和对象语法,例如: 自动判断user变量是数组还是对象 3. 1. 2. 输出内容1 3. 1. value1 2. value2 3. value3 4. 1. value1 2. value2 3. 1. value1 2. value2 3. 1. ThinkPHP 2. other Framework 3. 1. ThinkPHP - 222 - Present标签 或者知道user变量是对象 由于if标签的condition属性里面基本上使用的是php语法,尽可能使用判断标签和Switch标签会更加简 洁,原则上来说,能够用switch和比较标签解决的尽量不用if标签完成。因为switch和比较标签可以使用 变量调节器和系统变量。如果某些特殊的要求下面,IF标签仍然无法满足要求的话,可以使用原生php代 码或者PHP标签来直接书写代码。 present标签用于判断某个变量是否已经定义,用法: 如果判断没有赋值,可以使用: 可以把上面两个标签合并成为: present标签的name属性可以直接使用系统变量,例如: 2. other Framework 3. 1. ThinkPHP 2. other Framework 3. 1. 2. name已经赋值 3. 1. 2. name还没有赋值 3. 1. 2. name已经赋值 3. 4. name还没有赋值 5. 1. 2. $_GET['name']已经赋值 3. - 223 - Empty标签 Defined标签 empty标签用于判断某个变量是否为空,用法: 如果判断没有赋值,可以使用: 可以把上面两个标签合并成为: name属性可以直接使用系统变量,例如: DEFINED标签用于判断某个常量是否有定义,用法如下: name属性的值要注意严格大小写 如果判断没有被定义,可以使用: 1. 2. name为空值 3. 1. 2. name不为空 3. 1. 2. name为空 3. 4. name不为空 5. 1. 2. $_GET['name']为空值 3. 1. 2. NAME常量已经定义 3. - 224 - Assign标签 Define标签 可以把上面两个标签合并成为: ASSIGN标签用于在模板文件中赋值变量,用法如下: 在运行模板的时候,赋值了一个 var 的变量,值是 123 。 name属性支持系统变量,例如: 表示在模板中给 $_GET['id'] 赋值了 123 value属性也支持变量,例如: 或者直接把系统变量赋值给var变量,例如: 相当于,执行了: $var = $_GET['name']; DEFINE标签用于中模板中定义常量,用法如下: 在运行模板的时候,就会定义一个 MY_DEFINE_NAME 的常量。 1. 2. NAME常量未定义 3. 1. 2. NAME常量已经定义 3. 4. NAME常量未定义 5. 1. 1. 1. 1. 1. - 225 - 标签嵌套 import标签 value属性可以支持变量(包括系统变量),例如: 或者 模板引擎支持标签的多层嵌套功能,可以对标签库的标签指定可以嵌套。 系统内置的标签中,volist、switch、if、elseif、else、foreach、compare(包括所有的比较标签)、 (not)present、(not)empty、(not)defined等标签都可以嵌套使用。例如: 上面的标签可以用于输出双重循环。 嵌套层次是由标签库中的标签定义的时候的level属性决定的。 传统方式的导入外部JS和CSS文件的方法是直接在模板文件使用: 系统提供了专门的标签来简化上面的导入: 第一个是import标签 ,导入方式采用类似ThinkPHP的import函数的命名空间方式,例如: Type属性默认是js, 所以下面的效果是相同的: 1. 1. 1. 2. 3. {$sub.name} 4. 5. 1.