> CakePHP中文手册 > 单一HTML文件 包含全部手册内容

基本概念

Section1 简介

    本章节篇幅很短,简单的介绍一下MVC概念以及在cake中是如何实现的。如果你对MVC模式很陌生,这个章节将会非常适合你。我们由常规的MVC概念开始我们的讨论,然后用我们的方式在CakePHP中使用MVC模式,并且展示几个简单的示例。

Section 2 MVC模式

[TODO 考虑到MVC模式是必需过桥技能,所以建议还是先看一下相关文章,原文很简单,最后补上望海涵]

Section 3 Cake目录结构一览

当你解压缩Cake后,会发现有3个主目录

  • app
  • cake
  • vendors

cake目录下是核心lib,通常情况下你不要接触它们。

app目录下是你的程序文件所存放的位置。将app目录和cake目录分开,从而可以使多个应用共享一个cake类库。同样的也使得CakePHP的升级变得很容易:你只需要下载最新的版本并且覆盖原来的类库就可以了。不用担心会覆盖掉你的应用程序文件。

你可以使用vendors目录来存放一些第三方类库。后面的章节将会讲到更多关于vendors的内容,一个最基本的概念就是你可以非常方便的通过vendor()方法来调用vendor类库。

让我们来看一下完整的目录结构:

/app
    /config          - 配置文件目录,包括Database, ACL等

    /controllers     - Controllers文件 
        /components  - Components文件

    /index.php       - 允许你将app目录部署为DocumentRoot (译注:参见Apache相关配置)

    /models          - Model文件
 
    /plugins         - Plugins文件

    /tmp             - Cache和日志存放处 

    /vendors         - 你的应用中使用到的第三方类库

    /views          - 视图文件
        /elements    - 视图元素文件
        /errors      - 自定义错误页面
        /helpers     - Helpers文件
        /layouts     - 页面布局文件
        /pages       - 静态页面文件

    /webroot         - web根目录
        /css
        /files
        /img
        /js

/cake                - 核心类库,请不要随意修改任何文件,除非你确信你有这个能力

index.php           

/vendors             - 服务器端的第三方类库

安装CakePHP

Section 1 简介

现在你已经了解了每一件事情,包括系统的结构和整个类库的目的,或者说你略过了上述章节,因为你对这些资料并不感兴趣只是希望快点开始工作。Anyway 你已经摩拳擦掌准备完毕了。

这个章节来介绍了哪些是必须被安装到服务器上的,不同的配置方案,如何下载并安装CakePHP,访问默认页面,以及一些troubleshooting以备一些意外情况的发生。

Section 2 安装必需

[TODO 一些译者认为比较基础的内容暂时优先级设为低 完美版释出时会补上]

Section 3 安装CakePHP

[TODO]

Section 4 设置CakePHP

第一种设置方案仅推荐使用在开发环境下,因为它不是最安全的方案。第二种方案考虑了更多的安全性问题因而可以放心的使用在生产环境下。

注意:/app/tmp 目录必须具有写权限

开发环境设置

对于开发环境来说,我们可以把整个Cake目录放置在DocumentRoot下:

/wwwroot
    /cake
        /app
        /cake
        /vendors
        .htaccess
        index.php
			

这样的设置方案下,你的URL会如下这般(假设你使用了mod_rewrite):
www.example.com/cake/controllerName/actionName/param1/param2

生产环境设置

使用生产环境配置,你必须拥有修改Web Server DocumentRoot的权限。那样的话,整个域就如同一个CakePHP应用一般。

生产环境配置使用如下目录结构

../path_to_cake_install
    /app
        /config
        /controllers
        /models
        /plugins
        /tmp
        /vendors
        /views
        /webroot <-- 这将是你新的DocumentRoot
        .htaccess
        index.php
    /cake
    /vendors
    .htaccess
    index.php
			

建议修改Apache的配置文件如下:
DocumentRoot /path_to_cake/app/webroot

这样的配置下,你的URL将会如下这般:
http://www.example.com/controllerName/actionName/param1/param2

高级设置

在有些情况下,你希望能够将Cake应用的目录放在磁盘不同目录下。这可能是因为虚拟主机的限制,或者你希望多个app应用能够共享同一个Cake Lib。

对于一个Cake应用,有3个主要组成部分:

   1.
      CakePHP核心Lib - /cake目录下      
      
   2.      
      你的应用的代码(例如:controllers, models, layouts and views) - /app目录下      
      
   3.      
      你的应用web目录下的文件 (例如 图片 js脚本以及css样式文件等) - /app/webroot 目录下	
			

这3个目录都可以放置在磁盘的任意位置,但是webroot目录必须是web server可以访问的。你甚至可以把webroot目录移出app目录,只要你告诉Cake你把它们放到哪里去了。

你需要修改/app/webroot/index.php来完成配置(和Cake一起分发)。你需要修改3个常量:ROOT, APP_DIR, and CAKE_CORE_INCLUDE_PATH。

   1.
      ROOT 为包含app目录的根路径      
   2.
      APP_DIR app目录路径      
   3.
      CAKE_CORE_INCLUDE_PATH Cake核心Lib目录
      
      你的应用web目录下的文件 (例如 图片 js脚本以及css样式文件等) - /app/webroot 目录下	
			

这是范例:

/app/webroot/index.php (partial, comments removed)
if (!defined('ROOT'))
{
    define('ROOT', dirname(dirname(dirname(__FILE__))));
}

if (!defined('APP_DIR'))
{
    define ('APP_DIR', basename(dirname(dirname(__FILE__))));
}

if (!defined('CAKE_CORE_INCLUDE_PATH'))
{
    define('CAKE_CORE_INCLUDE_PATH', ROOT);
}	
		

下面通过一个具体的例子来解释我的配置

   1.
      我希望Cake Lib能够被共享,所以放在user/lib/cake下 (译注:作者都是linux配置,windows环境下随便放放吧)
      
   2.
      Cake webroot 目录为/var/www/mysite/。      
      
   3.
      程序文件目录为/home/me/mysite。
			

下面为具体的目录结构,不再赘述

/home
    /me
        /mysite                  <-- Used to be /cake_install/app
            /config
            /controllers
            /models
            /plugins
            /tmp
            /vendors
            /views
            index.php
/var
    /www
        /mysite                  <-- Used to be /cake_install/app/webroot
            /css
            /files
            /img
            /js
            .htaccess
            css.php
            favicon.ico
            index.php
/usr
    /lib
        /cake                    <-- Used to be /cake_install/cake
            /cake
                /config
                /docs
                /libs
                /scripts
                app_controller.php
                app_model.php
                basics.php
                bootstrap.php
                dispatcher.php
            /vendors
			

我按照上面的目录结构修改/var/www/mysite/index.php如下:

我建议使用'DS'常量代替路径中的斜杠。这样会保证你不会写错导致找不到文件。(考虑跨平台)

  1. if (!defined('ROOT'))  
  2. {  
  3.     define('ROOT', DS.'home'.DS.'me');  
  4. }  
  5.   
  6. if (!defined('APP_DIR'))  
  7. {  
  8.     define ('APP_DIR''mysite');  
  9. }  
  10.   
  11. if (!defined('CAKE_CORE_INCLUDE_PATH'))  
  12. {  
  13.     define('CAKE_CORE_INCLUDE_PATH', DS.'usr'.DS.'lib'.DS.'cake');  
  14. }  
  15.           

Section 5 配置Apache的mod_rewrite

CakePHP和mod_rewrite一同工作时会是最佳体验,我们发现有部分用户在他们的系统上不能让每件事情都很好的运作起来。所以我罗列了一些可能会有帮助的信息:

   1.
      首先确认.htaccess覆写是被允许的:在apache配置文件中检查<Directory>配置下的AllowOverride是不是设置为All。

   2.
      确认你修改的是server的配置文件而不是当前用户或者特定站点的配置文件。
      
   3.
      有的时候,可能你得到的CakePHP副本缺少了必须的.htaccess文件。因为有的操作系统会将以.开头的文件视作隐藏文件,因而不会拷贝它们。确定你的CakePHP副本是我们网站上下载或者是我们的SVN repository上checkout的。
      
   4.
      确认你是否成功加载了mod_rewrite!察看配置文件中是否有'LoadModule rewrite_module libexec/httpd/mod_rewrite.so' 和 'AddModule mod_rewrite.c'。
      
   5.
      如果你把Cake安装在一个用户目录下的话(http://example.com/~myusername/),你需要修改根目录中的.htaccess文件,加上一行"RewriteBase /~myusername/"。
      
   6.
      如果你发现URL后面加上了冗长乏味的sessionID的话
       (http://example.com/posts/?CAKEPHP=4kgj577sgabvnmhjgkdiuy1956if6ska)
      你需要加上一行"php_flag session.trans_id off"就ok了。
			

Section 6 确认Cake可以出炉了

好吧,让我们来看看这个baby吧。鉴于你选择的配置方式,你可以通过 http://www.example.com or http://www.example.com/ 来访问它。你会看到CakePHP的默认主页,和一个简短的通知消息,描述了你当前的数据库连接状态。

祝贺你!你已经完成了开始第一个CakeApp的准备工作。

配置CakePHP

Section 1 数据库配置

app/config/database.php 这个文件包含了所有的数据库配置。新安装的Cake是没有datebase.php这个文件的,你需要从database.php.default复制一份过来并重新命名,内容如下:

  1. app/config/database.php  
  2. var $default = array('driver'   => 'mysql',  
  3.                      'connect'  => 'mysql_connect',  
  4.                      'host'     => 'localhost',  
  5.                      'login'    => 'user',  
  6.                      'password' => 'password',  
  7.                      'database' => 'project_name',  
  8.                      'prefix'   => '');  
  9.   
  10. 和大多数php程序一样,修改一下你的配置就ok了。  
  11.           

关于前缀要提醒一下:这里定义的前缀将会用在任何一处由Cake发起的对数据表的SQL操作中。你在这里定义过后不能再在其它地方重新定义它。假设你的服务器只给了你一个单独的database,它同样可以让你遵循Cake的表名约定。注意:对于HABTM(has-and-belongs-to-many),也就是多对多关联表,你只需要添加一次前缀prefix_apples_bananas,而不是prefix_apples_prefix_bananas。

CakePHP支持下列数据库驱动:

  • mysql
  • postgres
  • sqlite
  • pear-drivername (so you might enter pear-mysql, for example)
  • adodb-drivername

$default连接中的'connect'可以指定这个连接是否是persistent连接。参考database.php.default中的注释来决定采用何种方式。

Rails两大原则之一,就是约定大于配置,所以你定义的数据表名必须遵循下面的命名约定:

   1.
      表名为英语单词的复数形式,例如"users", "authors"或者"articles"。注意与之对应的model对象则使用单数形式做名字。
      
   2.
      表字段必须有一个主键字段叫'id'。(相信用过ORM工具的都明白个中道理吧)
      
   3.
      如果你使用外键关联表,外键字段必须定义成这个样子:'article_id'。即关联表名的单数形式加上下划线然后id。其实也很好理解,并不需要特别记忆吧。
      
   4.
      如果你定义了 'created' 'modified'这样的auditor字段的话,Cake会自动在操作的时候去填充。(译注:rails果然周全,hibernate里面的interceptor就那么容易的做到了)
		

你应该注意到,同样的还有一个$test的数据库连接配置项,这是考虑到一般开发数据库和生产数据库通常并不是同一个,所以你可以在程序通过var $useDbConfig = 'test'来切换数据库连接设置。

在你的model对象中,你可以通过这样的方式来添加任意多个附加数据库连接。

Section 2 全局配置

CakePHP的全局配置文件为/app/config/core.php。尽管我们非常讨厌配置文件,但是还是有些配置需要去做。文件中的注释对每一个配置项都有很详尽的说明。

DEBUG:这个选项定义了应用程序的Debug级别。设置为一个非零的数值Cake会输出所有的pr()和debug()函数调用,并且在Forward之前会有提示信息。设置为2或者是更高的数值,则会在页面底部输出sql语句。
在debug模式下(DEBUG被设为1或者更高),Cake会输出错误自定义错误页面,例如 "Missing Controller","Missing Action"等等。在生产环境下(DEBUG被设为0),Cake输出 "Not Found"页面,你可以自行修改该页面(app/views/errors/error404.thtml)。

CAKE_SESSION_COOKIE: 这个设置可以设定你所希望使用的cookie名字。

CAKE_SECURITY:改变这个值标示了你所希望采用Session校验级别。Cake会根据你设定的值来执行Session超时,生成新的session id,删除历史session file等操作。可以选择的值如下所示:

   1.
      high:最高安全级别,10分钟没有活动的session被置为超时,同时每一次请求都将生成新的session id
      
   2.
      medium: 20分钟没有活动的session会被置为超时
      
   3.
      low: 30分钟没有活动的session会被置为超时
		

CAKE_SESSION_SAVE:设置你的session数据储存策略:

   1.
      cake:Session被保存在Cake安装目录下的tmp目录
      
   2.
      php: Session被保存在php.ini文件设置的储存路径下
      
   3.
      database: Session保存在$default数据库连接所指定的库中
		

Section 3 路由(Route)配置

"Routing"是一个纯PHP,类似mod_rewrite的机制,来实现URL和controller/action/params的自动映射。通过这个机制Cake可以拥有简洁且可配置的URL并且可以摆脱对mod_rewrite的依赖。当然使用mod_rewrite会让你的浏览器地址栏变得更加简洁。

Routes(路由)是独立的URL和指定的controller和action映射规则。路由规则在app/config/routes.php里面配置。示例如下:

  1. Route Pattern  
  2. <?php  
  3. $Route->connect (  
  4.     'URL',  
  5.     array('controller'=>'controllername',  
  6.     'action'=>'actionname''firstparam')  
  7. );  
  8. ?>  
  9.   
  10.   
  11.    1.  
  12.       URL是一个正则表达式,表示你希望采用的URL格式  
  13.    2.  
  14.       controllername是希望调用的controller的名字  
  15.         
  16.    3.  
  17.       actionname是希望调用的controller的action名字  
  18.         
  19.    4.  
  20.       firstparam是action的第一个参数名  
  21.           

第一个参数之后的所有参数作为parameters被传递到action。

下面是一个简单的示例,所有的url都被定向到BlogController。默认的action为BlogController::index()。

  1. Route Example  
  2. <?php  
  3. $Route->connect ('/blog/:action/*'array('controller'=>'Blog''action'=>'index'));  
  4. ?>  
  5. A URL like /blog/history/05/june can then be handled like this:  
  6. Route Handling in a Controller  
  7. <?php  
  8.   
  9. class BlogController extends AppController  
  10. {  
  11.  function history($year$month=null)  
  12.  {  
  13.    // .. Display appropriate content  
  14.  }  
  15. }  
  16. ?>  
  17.           

URL中的'history'通过Blog's route中的:action匹配。URL中通过*匹配的内容都被当作parameters传递到:action对应的方法中,就像这里的$year和$month。如果URL是/blog/history/05,则history()方法只会得到一个参数,05。

下面这个例子是CakePHP默认设置的一个route来为PagesController::display('home')配置路由。Home是Cake的默认首页视图,你可以在这个位置找到并修改它/app/views/pages/home.thtml。

  1. Setting the Default Route  
  2. <?php  
  3. $Route->connect ('/'array('controller'=>'Pages''action'=>'display''home'));  
  4. ?>  
  5.           

Section 4 高级路由配置:Admin路由与Webservice

在/app/config/core.php中还有那么一些高级选项可以供你使用,你可以利用它们使你的URL精巧地适应绝大多数情况。

第一个介绍的选项是管理员路由(admin routing)。假设你有两个Controller,ProductsController 和 NewsController,你肯定希望能有一些特别的URL给具有管理权限的用户来访问Controller中一些特别的action。为了保持URL的优雅和可读性,有些程序员相比 /products/adminAdd 和 /news/adminPost 更喜欢 /admin/products/add 和 /admin/news/post。

激活这个功能,首先你需要把/app/config/core.php中CAKE_ADMIN的注释去掉。CAKE_ADMIN的默认值是'admin',不过你可以修改为任何你喜欢的名字。但是你要记住这个名字,因为你需要在每一个admin action名字前面加上这个前缀,格式为admin_actionName()。

/admin/products/add          CAKE_ADMIN = 'admin'
                             name of action in ProductsController = 'admin_add()'

/superuser/news/post         CAKE_ADMIN = 'superuser'
                             name of action in NewsController = 'superuser_post()'

/admin/posts/delete          CAKE_ADMIN = 'admin'
                             name of action in PostsController = 'admin_delete()'
		

使用admin route可以让你的业务逻辑非常井井有条。
注意!!并不是说使用了admin route同时Cake也替你完成了安全和认证,这些还是需要你自己来完成的。

类似的,你也能很快的为webservice建立路由。你需要将某个action暴露为webservice接口吗?首先,将/app/config/core.php中的WEBSERVICES选项设为'on'。和先前的admin一样,打开这个选项可以很方便的让带有如下前缀的action自动暴露为webservice接口。

  • rss
  • xml
  • rest
  • soap
  • xmlrpc

这样做的结果就是同一个action,拥有了两种不同的视图,一个是标准的HTML视图,一个是webservice接口。因此你可以非常简单的让你的程序拥有webservice的能力。

举个例子,我的程序中有这么一个逻辑,有这么一个action可以告诉用户当前办公室里有多少人正在通话。我已经有了一个HTML视图,不过我希望再提供一个XML格式的接口供我的桌面Widget(译注:就是类似yahoo widget一类的桌面小贴士)或者是掌上设备来使用。首先,我打开了webservice路由功能:

  1. /app/config/core.php (partial)  
  2. /** 
  3.  *  The define below is used to turn cake built webservices 
  4.  *  on or off. Default setting is off. 
  5.  */  
  6.     define('WEBSERVICES''on');  
  7.       

然后我在controller中构建代码如下:

  1. <?php  
  2. class PhonesController extends AppController  
  3. {  
  4.     function doWhosOnline()  
  5.     {  
  6.         // this action is where we do all the work of seeing who's on the phone...  
  7.   
  8.         // If I wanted this action to be available via Cake's xml webservices route,  
  9.         // I'd need to include a view at /app/views/posts/xml/do_whos_online.thtml.  
  10.         // Note: the default view used here is at /app/views/layouts/xml/default.thtml.  
  11.   
  12.         // If a user requests /phones/doWhosOnline, they will get an HTML version.  
  13.         // If a user requests /xml/phones/doWhosOnline, they will get the XML version.  
  14.     }  
  15. }  
  16. ?>  
  17.           

Section 5 自定义反射配置(可选)

Cake的命名约束真的是非常不错,你可以命名model为Box,controller为Boxes,于是所有的问题都解决了。但是考虑到有的情况下(特别是非英语国家)这样的命名约束不如你意的话,你可以通过自定义反射配置来满足你的需求。

/app/config/inflections.php,这里定义了Cake的一些变量,你可以来调整名字单复数约定等。你需要详细阅读文件中的注释,并且最好具备正则表达式知识,否则请误随便修改该配置文件。

Scaffolding 脚手架

Section 1 Cool!Cake的脚手架

Cake的脚手架真的非常酷,以至于你会希望将他用到你的生产环境下。我们也认为它非常的酷,但是请意识到脚手架再好也还是脚手架。它只是一大捧素材供你在项目起始阶段能够迅速的搭起整个项目的架子。所以,当你发现你需要实现自己的业务逻辑的时候就是该拆掉脚手架的时候了。

对于web应用项目,由脚手架开始项目是非常不错的办法。早期的数据库设计通常是经常会变动的。有这么一种趋势,web开发人员非常厌恶创建一下根本不会实际使用的页面。为了减少开发人员的这种压力,Cake内置了脚手架功能。脚手架会根据你当前的表结构创建基础的CRUD页面。在你的controller中使用脚手架只需要一行代码,加入一个变量就ok。

  1. <?php  
  2. class CategoriesController extends AppController  
  3. {  
  4.     var $scaffold;  
  5. }  
  6. ?>  
  7.           

有一个非常重要的事情:脚手架中,要求每一个外键字段都必须以name_id这样的形式命名。因此,当你多级目录结构的时候,你可能会有一个字段叫parent_id,这样的时候你就不能如此命名了,因为根据约定,它会去映射parent这个model,而实际上是不存在这个model的,所以最好命名为parentid。但是如果你是有一个外键关联另一个model的话(例如:titles表中有category_id作为外键),你首先要确保正确的处理关联对象(参见 正确理解关联关系, 6.2),在show/edit/new这3个视图里面会自动生成一个下拉框来展示/选择category信息。可以通过设置model中的$displayField变量来取舍那些外键字段是需要显示的。具体示例如下:

  1. <?php  
  2. class Title extends AppModel  
  3. {  
  4.     var $name = 'Title';  
  5.   
  6.     var $displayField = 'title';  
  7. }  
  8. ?>  
  9.           

Section 2 自定义脚手架视图

如果你希望脚手架的页面有一些自定义的元素,你可以自己修改模版。我们仍然不建议你在生产环境下使用脚手架视图,不过一定的自定义将会对于原形构建有这非常大的帮助。

如果你不希望修改默认提供的脚手架视图,你可以自己提供这些模版:

自定义单个Controller默认脚手架视图

PostsController的自定义脚手架视图放置位置如下:

/app/views/posts/scaffold/index.scaffold.thtml
/app/views/posts/scaffold/show.scaffold.thtml
/app/views/posts/scaffold/edit.scaffold.thtml
/app/views/posts/scaffold/new.scaffold.thtml
		

自定义全局脚手架默认视图

全局默认脚手架视图放置位置如下:

/app/views/scaffold/index.scaffold.thtml
/app/views/scaffold/show.scaffold.thtml
/app/views/scaffold/edit.scaffold.thtml
/app/views/scaffold/new.scaffold.thtml
		

当你发现你需要实现自己的业务逻辑的时候也就是该拆除脚手教的时候。
Cake中code generator有一个非常有用的功能:Bake。Bake可以为你的应用生成一个基础的架子供你填充业务逻辑。

Models

Section 1 What is a model?

什么是model呢?model就是MVC模式中的M。
Model用来做什么呢?它将领域逻辑从表现层和独立的业务逻辑中剥离出来。

Model通常是数据库的访问介质,而且更明确的指定于某一张表(译注:在rails框架中model扮演着active record的角色,因此同时承担着DAO对象的职责)。默认情况下,model使用的表名为model名字的复数形式,比如'User'这个model对应的表名为'users'。Model可以包含数据校验规则,关联关系以及针对这张表的业务逻辑。下面展示了User model在cake中的具体定义:

  1. Example User Model, saved in /app/models/user.php  
  2. <?php  
  3.   
  4. //AppModel gives you all of Cake's Model functionality  
  5.   
  6. class User extends AppModel  
  7. {  
  8.     // Its always good practice to include this variable.  
  9.     var $name = 'User'; 
  10.  
  11.     // This is used for validation, see Chapter 11. 
  12.     var $validate = array(); 
  13.  
  14.     // You can also define associations. 
  15.     // See section 6.3 for more information. 
  16.  
  17.     var $hasMany = array('Image' => 
  18.                    array('className' => 'Image')  
  19.                    );  
  20.   
  21.     // You can also include your own functions:  
  22.     function makeInactive($uid)  
  23.     {  
  24.         //Put your own logic here...  
  25.     }  
  26. }  
  27.   
  28. ?>  
  29.           

(译注:关于Model应该是贫血型,失学型还是充血型的论战持续至今尚无明显得胜的一方,本文中的这句话:Model将领域逻辑从表现层和独立的业务逻辑中剥离出来 说得还比较清晰的,起码在cake里面他就是这么遵循的)

Section 2 Model Functions

从PHP的角度来看Model,只是一个继承了AppModel的类。AppModel类在/cake目录中已经定义了,如果你希望创建一个自己的AppModel基类,你可以放在app/app_model.php位置。AppModel中定义的方法将被所有model所继承并共享。而AppModel则继承于Cake Lib中的Model类,文件为cake/libs/model.php。

本章将会讨论到大量已经在Cake Model类中定义的方法,所以这里提醒,http://api.cakephp.org 将是您非常重要的参考。

用户定义的Model函数
下面是一个用户定义的Model方法,是帖子的隐藏/显示方法:

  1. Example Model Functions  
  2. <?php  
  3. class Post extends AppModel  
  4. {  
  5.    var $name = 'Post';  
  6.   
  7.    function hide ($id=null)  
  8.    {  
  9.       if ($id)   
  10.       {  
  11.           $this->id = $id;  
  12.           $this->saveField('hidden''1');  
  13.       }  
  14.    }  
  15.   
  16.    function unhide ($id=null)  
  17.    {  
  18.       if ($id)   
  19.       {  
  20.           $this->id = $id;  
  21.           $this->saveField('hidden''0');  
  22.       }  
  23.    }  
  24. }  
  25. ?>  
  26.           

获取你需要的数据

下面是Model中一些获取你需要的数据的标准方法:

findAll 
  string $conditions 
  array $fields 
  string $order 
  int $limit 
  int $page 
  int $recursive 
		

[$conditions Type: string] 检索条件,就是sql中where子句,就像这样 $conditions = "race = 'wookie' AND thermal_detonators > 3"。

[$fields Type: array] 检索属性,就是投影,指定所有你希望返回的属性。 (译注:这里我没有使用字段这个亲SQL的名字而是用了属性这个亲对象的名字,我相信很多PHPer更熟悉基于Sql和DTO的开发模式,但是引入Model的一个初衷就是将关系数据库转换为对象操作,所以投影查询应该理解为检索对象的某些属性)

[$order Type: string] 排序属性 指定了oder by的属性名 [TODO:check whether the multiple order field be supported]

[$limit Type: int] 结果集数据条目限制

[$page Type: int] 结果集分页index,默认为返回第一页

[$recursive Type: int] 递归 当设为大于1的整数时,将返回该model关联的其他对象。(译注:如果你使用过类似于Hibernate这样的ORM工具的话,不难理解这个属性就是LazyLoad属性,但也不完全等同,数字的值代表级联查询的层次数,例如这样,user.country.city.address.id)

find 
  string $conditions 
  array $fields 
  string $order 
  int $recursive 
		

find方法和findAll方法的区别在于,findAll方法返回所有符合的结果集,find方法只返回list中的第一个结果。

findAllBy<fieldName>
  string $value 
		

这是一个非常有魔力的方法,你可以看成是一个快速按照某个属性检索数据的方法,下面是在controller中的一段示例代码:

  1. $this->Post->findByTitle('My First Blog Post');  
  2. $this->Author->findByLastName('Rogers');  
  3. $this->Property->findAllByState('AZ');  
  4. $this->Specimen->findAllByKingDOM('Animalia');  
  5.           

返回类型和find() findAll()一样,是一个array。

findNeighbours 
	string $conditions 
	array $field 
	string $value
		

这个方法首先是通过conditions过滤获取结果集,然后根据field=value查找符合的对象(仅包含$field中指定的属性),最终返回该对象以及该对象的上一个对象和下一个对象。这在诸如相册这样的应用场景中将会非常有作用,你可以方便的获取当前相片,上一张和下一张这样的结果集合。注意:该方法只能作用于数值型和日期时间型属性。下面是示例代码:

  1. class ImagesController extends AppController  
  2. {  
  3.     function view($id)  
  4.     {  
  5.         // Say we want to show the image...  
  6.   
  7.         $this->set('image'$this->Image->find("id = $id");  
  8.   
  9.         // But we also want the previous and next images...  
  10.   
  11.         $this->set('neighbours'$this->Image->findNeighbours(null, 'id'$id);  
  12.   
  13.     }  
  14. }  
  15.           

我们拿到了包含一个完整$image['Image']对象的array,以及$neighbours['prev']['Image']['id']和$neighbours['next']['Image']['id']。

field 
  string $name 
  string $conditions 
  string $order 
		

返回conditions过滤后按order排序的第一个结果中的name属性值,返回类型为string。

findCount 
  string $conditions 
		

返回conditions过滤后结果集的数量。即select count .....

generateList 
  string $conditions 
  string $order 
  int    $limit 
  string $keyPath 
  string $valuePath 
		

generateList方法可以非常快捷的获取一个key-value这样的list,其实就是其他语言中的map类型,在web领域中,下拉框可能将是该方法最大的受益者。$conditions, $order 和 $limit这3个参数的用法和findAll()没有区别,$keyPath 和 $valuePath是model中用来填充结果集中key和value的属性。举个例子,在权限操作中,角色的定义往往是id - name这样的组合,参见下面的示例代码:

  1. $this->set(  
  2.     'Roles',  
  3.     $this->Role->generateList(null, 'role_name ASC', null, '{n}.Role.id''{n}.Role.role_name')  
  4. );  
  5.   
  6. //This would return something like:  
  7. array(  
  8.     '1' => 'Account Manager',  
  9.     '2' => 'Account Viewer',  
  10.     '3' => 'System Manager',  
  11.     '4' => 'Site Visitor'  
  12. );  
  13.           
read 
  string $fields 
  string $id 
		

根据$fields中的属性名从当前对象中读出对应的值,或者是$id指定的对象中读取。注意:read()方法仅级联读取该对象的一级关联对象,不管model中的$recursive的值为多少。如果你是希望获取所有级联属性的话,请使用find()或者findAll()方法。

query 
  string $query 
  
execute 
  string $query 
		

有的时候希望执行自定义的sql语句或者是出于性能上的考虑要优化sql语句,则可以使用query(string $query)和execute(string $query)方法,这两个方法不同的地方在于execute方法无返回值,适用于执行脚本而不是查询结果集。

  1. Custom Sql Calls with query()  
  2. <?php  
  3. class Post extends AppModel  
  4. {  
  5.     var $name = 'Post';  
  6.   
  7.     function posterFirstName()  
  8.     {  
  9.         $ret = $this->query("SELECT first_name FROM posters_table 
  10.                                  WHERE poster_id = 1");  
  11.         $firstName = $ret[0]['first_name'];  
  12.         return $firstName;  
  13.     }  
  14. }  
  15. ?>  
  16.           

复合查询条件(使用array)

绝大多数的Model查询方法中都会通过各种方式来传递一组条件。最简单的方式莫过于使用一个SQL中的where子句,不过如果你希望获得更多的控制,你可以使用array。使用rray的好处是代码会非常的清晰和易读,并且可以比较方便的构造查询。而且这样的语法可以保证你的查询组成元素(属性,值,运算符等)被拆分为精巧可控的几部分。这可以保证Cake能够生成绝大部分非常有效率的查询语句,并且避免了SQL语法上易犯的错误。

先看看非常基础的基于array的查询:

简单查询条件array示例:
$conditions = array("Post.title" => "This is a post");

这个结构恐怕是不言自明了:该查询返回所有title为"This is a post"的Post对象。注意,只使用"title"作为查询的属性名也是可以的,但是在构造查询的时候,使用[ModelName.FieldName]这样的写法,一者可以让你的代码更加清晰,二者可以防止命名冲突,所以请遵循这个Good Practice。那其他的检索形式如何呢?同样很简单,比如我们想检索所有title不是"This is a post"的Post对象:

array("Post.title" => "<> This is a post")

唯一增加的就是'<>'这个表达式。Cake能够解析所有的合法SQL比较运算符,包括LIKE, BETWEEN或者是正则表达式,注意,运算符和值或者表达式之间需要有一个空格。唯一不同的操作是IN操作。我们看下具体的例子:

array("Post.title" => array("First post", "Second post", "Third post"))

为conditions增加更多filter就像往array中添加一对key-value那样简单:

  1. array  
  2. (  
  3.     "Post.title"   => array("First post""Second post""Third post"),  
  4.     "Post.created" => "> " . date('Y-m-d'strtotime("-2 weeks"))  
  5. )  
  6.       

默认情况下,Cake使用'AND'来连接多个条件;这意味着,上面的示例代码检索结果为过去2周内title为"First post", "Second post"或者"Third post"的记录。当然我们也同样可以使用其他的逻辑运算符来连接查询条件:

  1. array  
  2. ("or" =>  
  3.     array  
  4.     (  
  5.         "Post.title" => array("First post""Second post""Third post"),  
  6.         "Post.created" => "> " . date('Y-m-d'strtotime("-2 weeks"))  
  7.     )  
  8. )  
  9.       

Cake可以使用任何SQL语句中允许的逻辑运算符,如AND, OR, NOT, XOR等等,并且大小写不敏感(Conditions可以无限嵌套,但我并不推荐使用这样的magic code)。Posts和Authors两个对象间分别是hasMany/belongsTo的关系(熟悉Hibernate的同学或许更喜欢一对多 多对一的叫法)。假设你希望检索所有过去两周内发布的posts或者是包含有指定关键字的posts,并且你希望限定作者为Bob,让我们看如下代码:

  1. array   
  2. ("Author.name" => "Bob""or" => array  
  3.     (  
  4.         "Post.title" => "LIKE %magic%",  
  5.         "Post.created" => "> " . date('Y-m-d'strtotime("-2 weeks")  
  6.     )  
  7. )  
  8.           

保存数据

当需要保存model对象时(译注:或者使用持久化这个词更贴切),你需要提供如下形式的数据给sava()方法:

  1. Array  
  2. (  
  3.     [ModelName] => Array  
  4.         (  
  5.             [fieldname1] => 'value'  
  6.             [fieldname2] => 'value'  
  7.         )  
  8. )  
  9.       

为了能够让数据通过这样的形式提交给controller,最方便的办法就是使用HTML Helper来完成这个任务,因为HTML Helper会为提交的Form封装成Cake希望的这种形式。当然你可以不使用,只要页面上的元素的name被设置成date[Modelname][fieldname]形式就ok了。不过我还是要说,$html->input('Model/fieldname')是最方便的办法。
(译注:OGNL的php版,太棒了,我的意见是如果不嫌麻烦的话尽量使用类似OGNL的做法,因为tag产生的页面在设计期是无法预览的,我相信web应用前台页面的设计的复杂性并不亚于后台业务逻辑)

从页面Form中提交的数据自动被格式化并且注入到controller中的$this->data变量,所以保存从web页面提交的数据只是举手之劳。下面我们来看一段示例代码:

  1. function edit($id)  
  2. {  
  3.   
  4.    //Note: The property model is automatically loaded for us at $this->Property.  
  5.   
  6.    // Check to see if we have form data...  
  7.    if (emptyempty($this->data))  
  8.    {  
  9.         $this->Property->id = $id;  
  10.         $this->data = $this->Property->read();//populate the form fields with the current row  
  11.    }  
  12.    else  
  13.    {  
  14.       // Here's where we try to save our data. Automagic validation checking  
  15.       if ($this->Property->save($this->data['Property'])) 
  16.       { 
  17.          //Flash a message and redirect. 
  18.          $this->flash('Your information has been saved.', 
  19.                      '/properties/view/'.$this->data['Property']['id'], 2);  
  20.       }  
  21.       //if some fields are invalid or save fails the form will render  
  22.    }  
  23. }  
  24.       

注意保存数据前会触发model的校验机制,更多关于数据校验的内容请参见Chapter 12。如果你不希望进行数据校验,可以使用save($date, false)。

其他一些有用的数据保存函数

del 
  string $id 
  boolean $cascade 
	

删除指定id的model对象,或者当前model对象。
如果$cascade设为true,则会级联删除当前model所关联的所有model中,设为'dependent'的model对象。删除成功返回true

saveField 
  string $name 
  string $value 
	

保存单个属性值。

getLastInsertId

返回最后当前ID属性的nextValue。
[ToDo 扩展支持多种主键生成策略 例如sequence UUID]
	
Model中的回调函数

我们在model中增加了一些回调函数以帮助你在model操作前后能够织入一些业务逻辑(原文为sneak in,借用了AOP中的织入一词,因为从操作来看这些回调函数等同于AOP中的advice)。为了获得这样的能力,需要使用model中的一些参数并且在你的model中覆写这些方法。

beforeFind 
  string $conditions
	

beforeFind()回调函数是在model的find方法执行前的前置操作。你可以加入任何检索前的业务逻辑。你覆写该方法只要保证在前置操作成功后返回true来执行真正的find方法,返回false中断find方法就可以了。(译注:在一些复杂场景中,需多次持久化的情况下请慎用)

afterFind 
  array $results 
	

使用afterFind回调函数能够更改find方法的返回结果集,或者在检索动作完成后加上一些业务逻辑。该函数的参数$results为经过回调函数处理以后的find检索结果集。

beforeValidate 
	

beforeValidate回调函数能够在model校验数据之前更改model中的一些数据。同样也可以用来在model校验之前加入更为复杂的额外校验规则。和beforeFind一样,必须保证返回true来调用真正的操作,返回false来中断校验乃至save操作。

beforeSave 
	

和先前介绍的回调函数类似,在校验完成之后,保存动作之前加入额外的处理(如果校验失败是不会触发该回调函数的)。返回true或者false,不再赘述。
一个比较常见的beforeSave的应用场景就是在保存动作之前格式化日期属性以适应不同的数据库:

  1. // Date/time fields created by HTML Helper:  
  2. // This code would be seen in a view  
  3.   
  4. $html->dayOptionTag('Event/start');  
  5. $html->monthOptionTag('Event/start');  
  6. $html->yearOptionTag('Event/start');  
  7. $html->hourOptionTag('Event/start');  
  8. $html->minuteOptionTag('Event/start');  
  9.   
  10. /*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-*/  
  11.   
  12. // Model callback functions used to stitch date  
  13. // data together for storage  
  14. // This code would be seen in the Event model:  
  15.   
  16. function beforeSave()  
  17. {  
  18.     $this->data['Event']['start'] = $this->_getDate('Event''start');  
  19.   
  20.     return true;  
  21. }  
  22.   
  23. function _getDate($model$field)  
  24. {  
  25.     return date('Y-m-d H:i:s'mktime(  
  26.         intval($this->data[$model][$field . '_hour']),  
  27.         intval($this->data[$model][$field . '_min']),  
  28.         null,  
  29.         intval($this->data[$model][$field . '_month']),  
  30.         intval($this->data[$model][$field . '_day']),  
  31.         intval($this->data[$model][$field . '_year'])));  
  32. }  
  33.   
  34.       
afterSave
	

保存完成后执行的动作。[ToDo 如果保存出错是否会触发?]

beforeDelete

afterDelete 
	

不需要多说了吧,删除操作前后的回调函数。

Section 3 Model中的变量

当你创建一个新的model的时候,有很多特殊的变量可以设置,从而是model具备Cake赋予的相应操作。

$primaryKey
如果主键字段不为'id',COC无法发挥的时候,你可以通过该变量来指定主键字段名字。

$recursive
这个我们上面已经介绍过了,指定了model级联关联对象的深度。

想象这样一个场景,Group下的User下面还有各自的Articles。

$recursive = 0 Cake只会检索Group对象
$recursive = 1 Cake会检索包含User对象的Group对象
$recursive = 2 Cake会检索完成的包含Article的Group对象
	

$transactional
决定Model是否允许事务处理,true 或者 false。注意,仅在支持事务的数据库上有效。

$useTable
和$primaryKey一样,如果你的表名不能和model匹配的话,而且你也不想或者不能修改表名,则可以通过该变量来指定数据表名。

$validate
该变量为一个array,表示一组校验规则,详细请参见Chapter 12。

$useDbConfig
还记得我们在配制一章介绍的如何配置数据库连接变量吗?可以通过该变量非常方便的切换数据库连接,默认的是什么?猜一下,当然就是'default',rails就是让你忘却那些难以记忆的配置。

Section 4 [重头戏]关联对象

简介

CakePHP中提供的一个非常重要的功能就是关联数据表间的映射。在CakePHP中,关联表通过association(关联)来处理。关联是这些逻辑上相关的单元间的胶水一般。

CakePHP中一共有4种类型的关联:

hasOne

hasMany

belongsTo

hasAndBelongsToMany
	

一旦model间的关联关系被定义,Cake能够自动检索你当前操作的model中包含的关联对象。也就是将基于关系数据库的数据映射为基于对象的领域模型。举个例子,Post和Author之间是hasMany(一对多)关系,当我们在controller中通过$this->Post->findAll()来检索所有的Post对象时,也会同时把所有关联的Author对象检索出来。

遵循CakePHP的命名约定是正确定义关联关系的有利保证(参见附录 B)。如果你使用了CakePHP的命名约定,可以通过脚手架来可视化你的数据,因为脚手架会自动侦测并使用你定义的关联关系,这样也能某种程度上供你检查是否定义正确。当然,你也可以完全不使用命名约定来定义关联,不过我们稍候再介绍关于这种定义。现在我们仅关注使用命名约定的情况下的关联定义。命名约定中我们需要考虑的有这3个内容,外键,model名字,表名。

这里先简单的介绍一下关于这3者的要求,更为详细的请查看附录:

外键:[单数形式的model名字]_id 假设在"authors"表中有Post的外键关联,则外键字段名字应该为 "post_id"。
表名:[复数形式的model名字] 表名必须为model名字的复数形式,例如:"posts" "authors"。
Model的名字:[驼峰法命名 单数形式]。

CakePHP的脚手架希望你的关联定义的顺序和表中的外键字段的顺序是一致的。所以如果我有一个Article对象[belongsTo(属于)]另外3个对象(Author Editor Publisher)的话,我需要3个外键author_id, editor_id, publisher_id。脚手架要求你model中对应的关联和数据库中的列顺序保持一致。

为了更好的描述关联对象是如何运作的,让我们继续以Blog为应用场景来介绍。假设我们现在需要为Blog系统创建一个简单的用户管理模块。我们假设我们不需要跟踪用户情况,但是我们希望每个用户都一个个人记录(用户 [hasOne] 个人记录)。用户可以创建多条评论记录(用户 [hasMany] 评论记录)。同样的,我们的文章会被分配到多个tag,同时每个tag都包含多篇文章,也就是多对多关系 (文章 [hasAndBelongsToMany] Tag)。

hasOne 关联的定义与查询

假设你已经准备好了User和Profile两个model,让我们来定义他们之间的关联。hasOne关联的定义是通过在model中增加一个array来实现的。下面是示例代码:

  1. /app/models/user.php hasOne  
  2. <?php  
  3. class User extends AppModel  
  4. {  
  5.     var $name = 'User';  
  6.     var $hasOne = array('Profile' =>  
  7.                         array('className'    => 'Profile',  
  8.                               'conditions'   => '',  
  9.                               'order'        => '',  
  10.                               'dependent'    =>  true,  
  11.                               'foreignKey'   => 'user_id'  
  12.                         )  
  13.                   );  
  14. }  
  15. ?>  
  16.       

$hasOne变量是一个array,Cake通过该变量来构建User与Profile之间的关联。我们来看每一个元素代表的意义:

  • className (required):关联对象的类名,上面代码中我们设为'Profile'表示关联的是Profile对象。
  • conditions: 关联对象的选择条件,(译注:类似hibernate中的formula)。具体到我们的例子来看,假设我们仅关联Profile的header color为绿色的文件记录,我们可以这样定义conditions,"Profile.header_color = 'green'"。
  • order: 关联对象的排序方式。假设你希望关联的对象是经过排序的,你可以为order赋值,就如同SQL中的order by子句:"Profile.name ASC"。
  • dependent:这是个布尔值,如果为true,父对象删除时会级联删除关联子对象。在我们的Blog中,如果"Bob"这个用户被删除了,则关联的Profile都会被删除。类似一个外键约束。
  • foreignKey:指向关联Model的外键字段名。仅在你不遵循Cake的命名约定时需要设置。

现在,现在当我们使用find() findAll()检索User对象时,你会发现关联的Profile对象也被检索回来,非常的方便:

  1. $user = $this->User->read(null, '25');  
  2. print_r($user);  
  3.   
  4. //output:  
  5.   
  6. Array  
  7. (  
  8.     [User] => Array  
  9.         (  
  10.             [id] => 25  
  11.             [first_name] => John  
  12.             [last_name] => Anderson  
  13.             [username] => psychic  
  14.             [password] => c4k3roxx  
  15.         )  
  16.   
  17.     [Profile] => Array  
  18.         (  
  19.             [id] => 4  
  20.             [name] => Cool Blue  
  21.             [header_color] => aquamarine  
  22.             [user_id] = 25  
  23.         )  
  24. )  
  25.       
belongsTo关联的定义与使用

现在User对象能够得到对应的Profile对象,当然我们也应该为Profile对象定义一个关联使之能够获取它的所有者,也就是对应的User对象。在Cake中,我们使用belongsTo关联来实现:

  1. /app/models/profile.php belongsTo  
  2. <?php  
  3. class Profile extends AppModel  
  4. {  
  5.     var $name = 'Profile';  
  6.     var $belongsTo = array('User' =>  
  7.                            array('className'  => 'User',  
  8.                                  'conditions' => '',  
  9.                                  'order'      => '',  
  10.                                  'foreignKey' => 'user_id'  
  11.                            )  
  12.                      );  
  13. }  
  14. ?>  
  15.       

和hasOne关联一样,belongsTo也是一个array变量,你可以通过设置其中的值来具体定义belongsTo关联:

  • className (required): 关联对象的类名,这里我们关联的是User对象,所以应该是'User'。
  • conditions: SQL条件子句以限定关联的对象,假定我们只允许Profile关联状态为active的用户,我们可以这样写:"User.active = '1'",当然也可以是类似的其它条件。
  • order:关联对象的排序子句,假如你希望关联的对象经过排序,你可以类似"User.last_name ASC"这样来定义。
  • foreignKey:关联对象所对应的外键字段名,仅在你不遵循Cake的命名约定时需要设置。

现在当我们使用find() findAll()来检索Profile对象时,会发现关联的User对象也一同被检索回来。

  1. $profile = $this->Profile->read(null, '4');  
  2. print_r($profile);  
  3.   
  4. //output:  
  5.   
  6. Array  
  7. (  
  8.   
  9.     [Profile] => Array  
  10.         (  
  11.             [id] => 4  
  12.             [name] => Cool Blue  
  13.             [header_color] => aquamarine  
  14.             [user_id] = 25  
  15.         )  
  16.   
  17.     [User] => Array  
  18.         (  
  19.             [id] => 25  
  20.             [first_name] => John  
  21.             [last_name] => Anderson  
  22.             [username] => psychic  
  23.             [password] => c4k3roxx  
  24.         )  
  25. )  
  26.       
hasMany关联的定义与查询

我们已经为User和Profile对象建立起了双向关联,那让我们开始为User和Comment对象之间建立关联吧,先看下面的示例代码:

  1. /app/models/user.php hasMany  
  2. <?php  
  3. class User extends AppModel  
  4. {  
  5.     var $name = 'User';  
  6.     var $hasMany = array('Comment' =>  
  7.                          array('className'     => 'Comment',  
  8.                                'conditions'    => 'Comment.moderated = 1',  
  9.                                'order'         => 'Comment.created DESC',  
  10.                                'limit'         => '5',  
  11.                                'foreignKey'    => 'user_id',  
  12.                                'dependent'     => true,  
  13.                                'exclusive'     => false,  
  14.                                'finderQuery'   => ''  
  15.                          )  
  16.                   );  
  17.   
  18.     // Here's the hasOne relationship we defined earlier...  
  19.     var $hasOne = array('Profile' => 
  20.                         array('className'    => 'Profile', 
  21.                               'conditions'   => '', 
  22.                               'order'        => '', 
  23.                               'dependent'    =>  true, 
  24.                               'foreignKey'   => 'user_id'  
  25.                         )  
  26.                   );  
  27. }  
  28. ?>  
  29.       

$hasMany array用来定义User包含多条Comment这样的关联关系。还是老样子,介绍一下包含的key,但是一些和之前同样含义的key我将不再赘述详细。

  • className (required):关联对象类名。
  • conditions: 关联对象限定条件。
  • order: 关联对象排序子句。
  • limit:因为是一对多关系,所以可以通过limit来限定检索的关联对象数量。比如我们可以只关联5条评论记录。
  • foreignKey:外键字段名。仅当不遵循命名约定时起用。
  • dependent:是否级联删除。(该动作可能会造成数据的误删除,请谨慎设定)
  • exclusive:如果设为true,所有的关联对象将在一句sql中删除,model的beforeDelete回调函数不会被执行。但是如果没有复杂的逻辑在级联删除中,这样的设定会带来性能上的优势。(译注:Cake的确方便,但是使用时一定要记住控制sql语句发送数量)
  • finderQuery:定义一句完整的sql语句来检索关联对象,能够对关联规则进行最大程度上的控制。当关联关系特别复杂的时候,比如one table - many model one model - many table的情况下,Cake无法准确的替你完成映射动作,需要你自己来完成这个艰巨的任务。

现在看一下如何在检索user对象的时候一并读回comment对象集合

  1. $user = $this->User->read(null, '25');  
  2. print_r($user);  
  3.   
  4. //output:  
  5.   
  6. Array  
  7. (  
  8.     [User] => Array  
  9.         (  
  10.             [id] => 25  
  11.             [first_name] => John  
  12.             [last_name] => Anderson  
  13.             [username] => psychic  
  14.             [password] => c4k3roxx  
  15.         )  
  16.   
  17.     [Profile] => Array  
  18.         (  
  19.             [id] => 4  
  20.             [name] => Cool Blue  
  21.             [header_color] => aquamarine  
  22.             [user_id] = 25  
  23.         )  
  24.   
  25.     [Comment] => Array  
  26.         (  
  27.             [0] => Array  
  28.                 (  
  29.                     [id] => 247  
  30.                     [user_id] => 25  
  31.                     [body] => The hasMany assocation is nice to have.  
  32.                 )  
  33.   
  34.             [1] => Array  
  35.                 (  
  36.                     [id] => 256  
  37.                     [user_id] => 25  
  38.                     [body] => The hasMany assocation is really nice to have.  
  39.                 )  
  40.   
  41.             [2] => Array  
  42.                 (  
  43.                     [id] => 269  
  44.                     [user_id] => 25  
  45.                     [body] => The hasMany assocation is really, really nice to have.  
  46.                 )  
  47.   
  48.             [3] => Array  
  49.                 (  
  50.                     [id] => 285  
  51.                     [user_id] => 25  
  52.                     [body] => The hasMany assocation is extremely nice to have.  
  53.                 )  
  54.   
  55.             [4] => Array  
  56.                 (  
  57.                     [id] => 286  
  58.                     [user_id] => 25  
  59.                     [body] => The hasMany assocation is super nice to have.  
  60.                 )  
  61.   
  62.         )  
  63. )  
  64.       

你同样可以为Comment加上关联User对象的belongsTo关联,但是在文档中就不再详细描述了。

hasAndBelongsToMany关联的定义与查询

我相信你已经掌握了简单的关联定义,让我们来看最后一个,也是最为复杂的关联关系:hasAndBelongsToMany(HABTM)。这个关联会让你头大的,不过也是最有用的。(译注:我倒认为应该数据库设计上尽量的避免出现大量的多对多关联,有的时候多对多关联可以比较简单拆分为两个一对多关联。)HABTM关联也就是3张表的关联关系,关系数据库中应该说只存在多对一外键关联,所以如果要做多对多关联必然需要一张关联表来保存关联关系。

hasMany和hasAndBelongsToMany的不同处在于,hasMany关联所关联的对象只会属于本对象,不会同时属于其他对象。但是HABTM不同,所关联的对象同时会被其他对象所关联持有。比如Post和Tag之间的关联就是这种关系,一篇日志可以属于多个不同的Tag,一个Tag也会包含多篇不同的日志。

为了实现多对多关联,首先要建立那张关联关系表(参照表)。除了"tags" "posts"表以外,根据Cake的命名约定,关联表的名字应该是[复数形式的model1名字]_[复数形式的model2名字],至于两个model谁先谁后则根据字典排序法。

下面是一些示例:
Posts and Tags: posts_tags
Monkeys and IceCubes: ice_cubes_monkeys
Categories and Articles: articles_categories

关联表至少需要两个关联对象的外键字段,例如"post_id" 和 "tag_id"。当然你也可以加入一些其他的属性。

下面是生成的数据库脚本:

Here's what the SQL dumps will look like for our Posts HABTM Tags example:

--
-- Table structure for table `posts`
--

CREATE TABLE `posts` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `user_id` int(10) default NULL,
  `title` varchar(50) default NULL,
  `body` text,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  `status` tinyint(1) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- Table structure for table `posts_tags`
--

CREATE TABLE `posts_tags` (
  `post_id` int(10) unsigned NOT NULL default '0',
  `tag_id` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY  (`post_id`,`tag_id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- Table structure for table `tags`
--

CREATE TABLE `tags` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `tag` varchar(100) default NULL,
  PRIMARY KEY  (`id`)
) TYPE=MyISAM;With our tables set up, let's define the association in the Post model:

/app/models/post.php hasAndBelongsToMany
<?php
class Post extends AppModel
{
    var $name = 'Post';
    var $hasAndBelongsToMany = array('Tag' =>
                               array('className'    => 'Tag',
                                     'joinTable'    => 'posts_tags',
                                     'foreignKey'   => 'post_id',
                                     'associationForeignKey'=> 'tag_id',
                                     'conditions'   => '',
                                     'order'        => '',
                                     'limit'        => '',
                                     'uniq'         => true,
                                     'finderQuery'  => '',
                                     'deleteQuery'  => '',
                               )
                               );
}
?>
	

$hasAndBelongsToMany array是定义HABTM关联的变量,简单介绍一下需要定义的key:

  • className (required):关联对象类名。
  • joinTable:如果你没有遵循Cake的命名约定建立关联表的话,则需要设置该key来指定关联表。
  • foreignKey:注意和associationForeignKey的区别,这个是定义本model在关联表中的外键字段。当然也是仅在你没有遵循Cake命名约定的时候才需要。
  • associationForeignKey:关联表中指向关联对象的外键字段名。
  • conditions:关联对象限定条件。
  • order: 关联对象排序子句。
  • limit:关联对象检索数量限制。
  • uniq:设为true的话,重复的关联对象将被过滤掉。
  • finderQuery:完整的关联对象检索语句。
  • deleteQuery:完整的删除关联关系的sql语句。当你需要自己实现删除操作的时候可以使用该值。
  1. 最后我们来看一下代码:  
  2.   
  3. $post = $this->Post->read(null, '2');  
  4. print_r($post);  
  5.   
  6. //output:  
  7.   
  8. Array  
  9. (  
  10.     [Post] => Array  
  11.         (  
  12.             [id] => 2  
  13.             [user_id] => 25  
  14.             [title] => Cake Model Associations  
  15.             [body] => Time saving, easy, and powerful.  
  16.             [created] => 2006-04-15 09:33:24  
  17.             [modified] => 2006-04-15 09:33:24  
  18.             [status] => 1  
  19.         )  
  20.   
  21.     [Tag] => Array  
  22.         (  
  23.             [0] => Array  
  24.                 (  
  25.                     [id] => 247  
  26.                     [tag] => CakePHP  
  27.                 )  
  28.   
  29.             [1] => Array  
  30.                 (  
  31.                     [id] => 256  
  32.                     [tag] => Powerful Software  
  33.                 )  
  34.         )  
  35. )  
  36.       
保存关联对象

请记住一件非常重要的事情,当保存对象时,很多时候需要同时保存关联对象,比如当我们保存Post对象和它关联的Comment对象时,我们会同时用到Post和Comment两个model的操作。

抽象出来说,当关联的两个对象都没有持久化(即未保存在数据库中),你需要首先持久化主对象,或者是父对象。我们通过保存Post和关联的一条Comment这个场景来具体看看是如何操作的:

  1. //------------Post Comment都没有持久化------------  
  2.   
  3. /app/controllers/posts_controller.php (partial)  
  4. function add()  
  5. {  
  6.     if (!emptyempty($this->data))  
  7.     {  
  8.         //We can save the Post data:   
  9.         //it should be in $this->data['Post']  
  10.           
  11.         $this->Post->save($this->data);  
  12.   
  13.         //Now, we'll need to save the Comment data  
  14.         //But first, we need to know the ID for the   
  15.         //Post we just saved...  
  16.   
  17.         $post_id = $this->Post->getLastInsertId();  
  18.   
  19.         //Now we add this information to the save data  
  20.         //and save the comment.  
  21.   
  22.         $this->data['Comment']['post_id'] = $post_id;  
  23.   
  24.         //Because our Post hasMany Comments, we can access  
  25.         //the Comment model through the Post model:  
  26.   
  27.         $this->Post->Comment->save($this->data);  
  28.   
  29.     }  
  30. }  
  31.       

换一种情形,假设为现有的一篇Post添加一个新的Comment记录,你需要知道父对象的ID。你可以通过URL来传递这个参数或者使用一个Hidden字段来提交。

  1. /app/controllers/posts_controller.php (partial)  
  2. //Here's how it would look if the URL param is used...  
  3. function addComment($post_id)  
  4. {  
  5.     if (!emptyempty($this->data))  
  6.     {  
  7.         //You might want to make the $post_id data more safe,  
  8.         //but this will suffice for a working example..  
  9.   
  10.         $this->data['Comment']['post_id'] = $post_id;  
  11.   
  12.         //Because our Post hasMany Comments, we can access  
  13.         //the Comment model through the Post model:  
  14.   
  15.         $this->Post->Comment->save($this->data);  
  16.     }  
  17. }  
  18.       

如果你使用hidden字段来提交ID这个参数,你需要对这个隐藏元素命名(如果你使用HtmlHelper)来正确提交:

假设日志的ID我们这样来命名$post['Post']['id']

<?php echo $html->hidden('Comment/post_id', array('value' => $post['Post']['id'])); ?>

这样来命名的话,Post对象的ID可以通过$this->data['Comment']['post_id']来访问,同样的通过$this->Post->Comment->save($this->data)也能非常简单的调用。

当保存多个子对象时,采用一样的方法,只需要在一个循环中调用save()方法就可以了(但是要记住使用Model::create()方法来初始化对象)。

小结一下,无论是belongsTo, hasOne还是hasMany关联,在保存关联子对象时候都要记住把父对象的ID保存在子对象中。

保存 hasAndBelongsToMany 关联对象

如果定义关联一样,最复杂的莫过于 hasAndBelongsToMany 关联,hasOne, belongsTo, hasMany这3种关联只需要很简单的保存一下关联对象外键ID就可以了。但是 hasAndBelongsToMany 却没有那么容易了,不过我们也做了些努力,使之尽可能变得简单些。继续我们的Blog的例子,我们需要保存一个Post,并且关联一些Tag。

实际项目中你需要有一个单独的form来创建新的tag然后来关联它们,不过为了叙述简单,我们假定已经创建完毕了,只介绍如何关联它们的动作。

当我们在Cake中保存一个model,页面上tag的名字(假设你使用了HtmlHelper)应该是这样的格式 'Model/field_name' 。好了,让我们开始看页面代码:

/app/views/posts/add.thtml Form for creating posts

                <h1>Write a New Post</h1>
                
                    <table>   
                    
                    <tr>    
                    
                        <td>Title:</td>  
    
                        <td><?php echo $html->input('Post/title')?></td> 
    
                    </tr> 
                    
                    <tr>        
                    
                        <td>Body:<td> 
    
                        <td><?php echo $html->textarea('Post/title')?></td>
    
                    </tr> 
                    
                    <tr> 
                    
                    <td colspan="2"> 
    
                        <?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
                        
                        <?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
                        
                        <?php echo $html->submit('Save Post')?>
                        
                    </td> 
    
                    </tr>
                    
                    </table>
		

上述页面仅仅创建了一个Post记录,我们还需要加入些代码来关联tag:

/app/views/posts/add.thtml (Tag association code added)
<h1>Write a New Post</h1>
    <table>
        <tr>
            <td>Title:</td>
            <td><?php echo $html->input('Post/title')?></td>
        </tr>
        <tr>
            <td>Body:</td>
            <td><?php echo $html->textarea('Post/title')?></td>
        </tr>
        
        <tr>
            <td>Related Tags:</td>
            <td><?php echo $html->selectTag('Tag/Tag', $tags, null, array('multiple' => 'multiple')) ?>
            </td>
        </tr>
        
        <tr>
            <td colspan="2">
                <?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
                <?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
                <?php echo $html->submit('Save Post')?>
            </td>
        </tr>
</table>
		

我们在controller中通过调用 $this->Post->save() 来保存当前Post以及关联的tag信息,页面元素的命名必须是这样的格式 "Tag/Tag" (Cake Tag Render之后实际的Html Tag名字为 'data[ModelName][ModelName][]' 这样的格式)。提交的数据必须是单个ID,或者是一个ID的array。因为我们使用了一个复选框,所以这里提交的是一个ID的array。

$tags变量是一个array,包含了复选框所需要的tag的ID以及Name信息。

使用 bindModel() 和 unbindModel() 实时地改变关联关系

有的时候可能你会需要实时地,动态的改变model的关联关系,比如在一个异常情况下。特别是LazyLoad的问题,有的时候我们并需要一个完整的model,所以我们可以使用 bindModel() 和 unbindModel()来绑定或者解除绑定关联对象。

代码说话,Start:

  1. leader.php and follower.php  
  2. <?php  
  3.   
  4. class Leader extends AppModel  
  5. {  
  6.     var $name = 'Leader';  
  7.   
  8.     var $hasMany = array(  
  9.         'Follower' => array(  
  10.             'className' => 'Follower',  
  11.             'order'     => 'Follower.rank'  
  12.         )  
  13.     );  
  14. }  
  15.   
  16. ?>  
  17.   
  18. <?php  
  19.   
  20. class Follower extends AppModel  
  21. {  
  22.     var $name = 'Follower';  
  23. }  
  24.   
  25. ?>  
  26.           

上述两个Model,在Leader Model中,有一个hasMany关联,定义了 "Leader hasMany Followers" 这样的关系。下面我们演示如何在Controller中动态地解除这种关联绑定。

  1. leaders_controller.php (partial)  
  2. function someAction()  
  3. {  
  4.     //This fetches Leaders, and their associated Followers  
  5.     $this->Leader->findAll();  
  6.   
  7.     //Let's remove the hasMany...  
  8.     $this->Leader->unbindModel(array('hasMany' => array('Follower'))); 
  9.      
  10.     //Now a using a find function will return Leaders, with no Followers 
  11.     $this->Leader->findAll(); 
  12.  
  13.     //NOTE: unbindModel only affects the very next find function. 
  14.     //注意:unbindModel方法只作用一次,第二次find方法调用时则仍然是关联关系有效的 
  15.     //An additional find call will use the configured association information. 
  16.  
  17.     //We've already used findAll() after unbindModel(), so this will fetch  
  18.     //Leaders with associated Followers once again...  
  19.     $this->Leader->findAll();  
  20. }  
  21.           

对于其他各种关联的unbindModel()的用法是类似的,你只需要更改名字和类型就可以了,下面介绍一些基础的Usage:

通用的unbindModel()

$this->Model->unbindModel(array('associationType' => array('associatedModelClassName')));

掌握了如何动态的解除绑定之后,让我们看看如何动态的绑定关联关系。

  1. leaders_controller.php (partial)  
  2. funciton anotherAction()  
  3. {  
  4.     //There is no Leader hasMany Principles in the leader.php model file, so  
  5.     //a find here, only fetches Leaders.  
  6.     $this->Leader->findAll();  
  7.   
  8.     //Let's use bindModel() to add a new association to the Principle model:  
  9.     $this->Leader->bindModel(  
  10.         array('hasMany' => array( 
  11.                 'Principle' => array( 
  12.                     'className' => 'Principle' 
  13.                 ) 
  14.             ) 
  15.         ) 
  16.     ); 
  17.  
  18.     //Now that we're associated correctly, we can use a single find function  
  19.     //to fetch Leaders with their associated principles:  
  20.     $this->Leader->findAll();  
  21. }  
  22.           

bindModel()方法不单能创建一个关联,同样可以用来动态的修改一个关联。

  1. 下面是通常的用法:  
  2.   
  3. Generic bindModel() example  
  4. $this->Model->bindModel(  
  5.         array('associationName' => array(  
  6.                 'associatedModelClassName' => array(  
  7.                     // normal association keys go here...  
  8.                 )  
  9.             )  
  10.         )  
  11.     );  
  12.           

注意:这些的前提是你的数据库表中的外键关联等已经正确设置。