建立 Post 视图

现在已经有了数据流、模型、程序逻辑和定义数据流向的控制器。我们要建立与上面例子中的 index 动作对应的视图。

Cake 视图仅是呈现应用程序布局的片段。对于多数应用程序,视图是混合了 PHP 的 HTML,但是它也可以是 XML、CSV,甚至是二进制数据。

布局是包装视图的处理显示的代码,能够定义和切换,但是现在,我们使用默认的布局。

还记得上一节是如何使用 set() 方法给视图中的 ‘posts’ 变量赋值的吗?所传递的数据类似于:

 1 // print_r($posts) output:
 2 
 3 Array
 4 (  5     [0] => Array
 6         (  7             [Post] => Array
 8                 (  9                     [id] => 1
10                     [title] => The title 11                     [body] => This is the post body.
12                     [created] => 2008-02-13 18:34:55
13                     [modified] =>
14                 ) 15         ) 16     [1] => Array
17         ( 18             [Post] => Array
19                 ( 20                     [id] => 2
21                     [title] => A title once again 22                     [body] => And the post body follows.
23                     [created] => 2008-02-13 18:34:56
24                     [modified] =>
25                 ) 26         ) 27     [2] => Array
28         ( 29             [Post] => Array
30                 ( 31                     [id] => 3
32                     [title] => Title strikes back 33                     [body] => This is really exciting! Not.
34                     [created] => 2008-02-13 18:34:57
35                     [modified] =>
36                 ) 37         ) 38 )

Cake 的视图文件存储在 /app/View 目录下与控制器名称(去掉了 controller 后缀)同名的文件夹中(现在的情况下,要创建 /app/View/Posts 文件夹)。使用漂亮的表格格式化数据,其视图类似于:

 1 <!-- File: /app/View/Posts/index.ctp -->
 2 
 3 <h1>Blog posts</h1>
 4 <table>
 5     <tr>
 6         <th>Id</th>
 7         <th>Title</th>
 8         <th>Created</th>
 9     </tr>
10 
11     <!-- Here is where we loop through our $posts array, printing out post info -->
12 
13 <?php foreach ($posts as $post): ?>
14     <tr>
15         <td><?php echo $post['Post']['id']; ?></td>
16         <td>
17 <?php echo $this->Html->link($post['Post']['title'],
18 array('controller' => 'posts', 'action' => 'view', $post['Post']['id'])); ?>
19         </td>
20         <td><?php echo $post['Post']['created']; ?></td>
21     </tr>
22 <?php endforeach; ?>
23 <?php unset($post); ?>
24 </table>

希望这看起来挺简单。

你可能已经注意到例子中所用的那个调用 $this->HTML 的对象,这是 CakePHP 中 ‘HtmlHelper’ 的一个实例。CakePHP 带有一个生成类似链接、表单输出、JavaScript 和 Ajax 快照的视图助手集合。在 助手 一节你能学到更多如何使用它们的方法,现在要重点注意的是 link() 方法,它能用给定的标题(第一个参数)和 URL (第二个参数)生成 HTML 链接。

推荐使用数组格式在 CakePHP 中指定 URLs。在“路由”一节对此有更多的解释。使用数组格式处理 URLs 可以获得 CakePHP 服务器的路由兼容性。也可以用 /controller/action/param1/param2 的格式将 URLs 关联到应用程序。

现在,让你的浏览器指向 http://www.example.com/posts/index 。你会看到带有标题和帖子列表的格式正确的视图。

如果你点击了我们建立的这个视图中的某个链接(它将一个帖子标题链接到 URL /posts/view/some_id), 你应该会发现 CakePHP 会报告那个 action 还没有定义。 如果你没有收到这个报告,表明有些事情可能出错了,或者你竟然已经定义了它,这是很诡异的一种情形^_^。 现在,让我们在 PostsController 中建立它:

 1 class PostsController extends AppController {  2     public $helpers = array('Html', 'Form');  3 
 4     public function index() {  5          $this->set('posts', $this->Post->find('all'));  6     }  7 
 8     public function view($id = null) {  9         $this->Post->id = $id; 10         $this->set('post', $this->Post->read()); 11     } 12 }

set() 调用看上去有点眼熟。需要注意的是,我们在此优先使用 read() 而不是 find('all'),这是因为我们仅仅要得到一条帖子信息。

还要注意,我们的视图 action 获取了一个参数: 我们希望看到的帖子的 ID。 这个参数是通过 URL 请求传递给 action 的。 如果用户请求 /posts/view/3,那么值 ‘3’ 就被传递给 $id

现在让我们建立一个新的 ‘view’ action 放在 /app/View/Posts/view.ctp 文件中。

1 <!-- File: /app/View/Posts/view.ctp -->
2 
3 <h1><?php echo h($post['Post']['title']); ?></h1>
4 
5 <p><small>Created: <?php echo $post['Post']['created']; ?></small></p>
6 
7 <p><?php echo h($post['Post']['body']); ?></p>

尝试输入链接 /posts/index 或者手动访问 /posts/view/1,看看工作是否正常。.

添加帖子

虽然从数据库中读出贴子并展示给我们是个不错的开始,但是我们还需要可以添加贴子。

首先,从在 PostsController 中建立 add() 方法开始:

 1 class PostsController extends AppController {  2     public $helpers = array('Html', 'Form', 'Session');  3     public $components = array('Session');  4 
 5     public function index() {  6         $this->set('posts', $this->Post->find('all'));  7     }  8 
 9     public function view($id) { 10         $this->Post->id = $id; 11         $this->set('post', $this->Post->read()); 12 
13     } 14 
15     public function add() { 16         if ($this->request->is('post')) { 17             $this->Post->create(); 18             if ($this->Post->save($this->request->data)) { 19                 $this->Session->setFlash('Your post has been saved.'); 20                 $this->redirect(array('action' => 'index')); 21             } else { 22                 $this->Session->setFlash('Unable to add your post.'); 23             } 24         } 25     } 26 }

注解

在使用 Session 的任何一个 controller 中都需要包含 SessionComponent (和SessionHelper)。如果需要,可以将它包含在 AppController 中。

add() 实现了这样几个功能:如果表单提交的数据非空,就试图使用 Post 模型保存数据。如果因为某些原因保存未成功,则渲染视图,这给了我们向用户显示数据校验或其它警告的机会。

每个 CakePHP 请求包含一个 CakeRequest 对象,它可以通过 $this->request 访问。 request 对象包含这次请求被获取的信息,并能够用于应用程序流的控制。 在本例中,我们使用 CakeRequest::is() 方法检验请求是否是以 HTTP POST 格式提交的。

当用户使用表单向应用程序提交数据(POST),其信息可以使用 $this->request->data 获取。 可以使用 pr() 或debug() 方法输出这些数据。

我们使用了 Session 组件的 SessionComponent::setFlash() 方法发送了一条信息给 session 变量,用于在中转页上显示。 在中转页的布局中我们用 SessionHelper::flash 显示和清除这个 session 变量。 该控制器的Controller::redirect 方法用于跳转到其它 URL。 参数 array('action' => 'index') 指定了跳转到 /posts URL,等于 posts 控制器的 index 方法。 你可以参考 API 中的 Router::url() 功能,它使你可以指定为不同的 Cake 函数指定一个 URL 格式。

调用 save() 方法将检查检验错误,并在出现错误时跳过保存。 我们将在后续的章节中讨论如何处理这些错误。

数据校验

Cake 在获取表单校验的单调输出方面做了大量的工作。每个人都痛恨无止境的编写表单及其验证。CakePHP 使其变得更快更容易。

想要更好的利用校验功能,就需要在视图中使用 FormHelper 助手。FormHelper 助手默认在所有视图中都可用,用法是 $this->Form

这是我们添加的视图:

1 <!-- File: /app/View/Posts/add.ctp -->
2 
3 <h1>Add Post</h1>
4 <?php 5 echo $this->Form->create('Post'); 6 echo $this->Form->input('title'); 7 echo $this->Form->input('body', array('rows' => '3')); 8 echo $this->Form->end('Save Post'); 9 ?>

在这里,我们用 FormHelper 助手生成一个 HTML 表单的起始标签。$this->Form->create() 生成的 HTML 是:

1 <form id="PostAddForm" method="post" action="/posts/add">

如果 create() 是不带参数调用的,表示创建的表单将以 POST 形式提交给同一个控制器的 add() (或者当表单中含有 id 元素时提交给 edit())动作。

$this->Form->input() 方法用来生成同名的表单元素,这个方法的第一个参数告诉 CakePHP 前后台通讯的是哪个域,第二个参数允许指定选项:本例中,是 textarea 域的行数。这里有一点自动完成的:input() 方法将基于 model 指定的域生成不同的表单元素。

$this->Form->end() 调用生成一个 submit 元素并结束表单。如果提供给 end() 方法的第一个参数是字符串,FormHelper 生成一个相应名称的 submit 域并关闭表单标签。再说一次, 助手 一节有更多的助手信息。

现在我们返回并更新 /app/View/Posts/index.ctp 视图使其包含一个新的 “Add Post” 链接。在 <table> 之前添加如下行:

1 <?php echo $this->Html->link( 2     'Add Post',
3     array('controller' => 'posts', 'action' => 'add') 4 ); ?>

你可能会奇怪:我如何通知 Cake 我的校验要求?校验规则是定义在模型中的。让我们回顾 Post 模型并稍作调整:

 1 class Post extends AppModel {  2     public $validate = array(  3         'title' => array(  4             'rule' => 'notEmpty'
 5         ),
 6         'body' => array(  7             'rule' => 'notEmpty'
 8         )  9     ); 10 }

$validate 数组告诉 CakePHP 在 save() 方法被调用时如何校验我们的数据。这里,我指定 title 和 body 都不能为空。CakePHP 的校验功能很强劲,带有一批预置的规则(如 信用卡号码、Email 地址等),而且可以灵活地添加自己的校验规则。可以浏览 数据校验 获取更多的相关信息。

现在已经有了校验规则,试着输入空的 title 和 body,看看它们是如何工作的。当我们使用 FormHelper 的FormHelper::input() 方法创建表单,校验的错误信息会自动显示。

编辑帖子

我们已经来到了帖子编辑这一步,现在你已经是一个 CakePHP 的高手了,所以你应该操起一个模式。编写动作,然后是视图。PostsController 的 edit() 方法类似于:

 1 public function edit($id = null) {  2     $this->Post->id = $id;  3     if ($this->request->is('get')) {  4         $this->request->data = $this->Post->read();  5     } else {  6         if ($this->Post->save($this->request->data)) {  7             $this->Session->setFlash('Your post has been updated.');  8             $this->redirect(array('action' => 'index'));  9         } else { 10             $this->Session->setFlash('Unable to update your post.'); 11         } 12     } 13 }

这个动作首先检查用户提交的是不是 GET 请求。如果是,就找到 Post 并将其传递给视图。如果不是,就可能包含 POST 数据。我们将使用 POST 数据更新贴子记录,或者将校验的错误信息踢回并显示给用户。

edit 视图类似于:

1 <!-- File: /app/View/Posts/edit.ctp -->
2 
3 <h1>Edit Post</h1>
4 <?php 5     echo $this->Form->create('Post', array('action' => 'edit')); 6     echo $this->Form->input('title'); 7     echo $this->Form->input('body', array('rows' => '3')); 8     echo $this->Form->input('id', array('type' => 'hidden')); 9     echo $this->Form->end('Save Post');

这个视图输出编辑表单(带有相应数据值),所有的错误校验信息也一并提供。

有件事需要注意:如果提供的数据数组中包含 ‘id’ 域,CakePHP 认为这是要修改一个模型的数据。如果没有 ‘id’ 域,cake 会认为当 save() 方法被调用时插入一条新的模型数据(参见 add 视图)。

现在可以更新 index 视图,添加一个用于编辑的链接:

 1 <!-- File: /app/View/Posts/index.ctp  (edit links added) -->
 2 
 3 <h1>Blog posts</h1>
 4 <p><?php echo $this->Html->link("Add Post", array('action' => 'add')); ?></p>
 5 <table>
 6     <tr>
 7         <th>Id</th>
 8         <th>Title</th>
 9                 <th>Action</th>
10         <th>Created</th>
11     </tr>
12 
13 <!-- Here's where we loop through our $posts array, printing out post info --> 14 
15 <?php foreach ($posts as $post): ?> 16     <tr> 17         <td><?php echo $post['Post']['id']; ?></td> 18         <td> 19 <?php echo $this->Html->link($post['Post']['title'], array('action' => 'view', $post['Post']['id'])); ?> 20         </td> 21         <td> 22 <?php echo $this->Html->link('Edit', array('action' => 'edit', $post['Post']['id'])); ?> 23         </td> 24         <td> 25 <?php echo $post['Post']['created']; ?> 26         </td> 27     </tr> 28 <?php endforeach; ?> 29 
30 </table>

删除帖子

下面我们来看删除贴子的道道。在 PostsController 中添加 delete() 动作:

1 public function delete($id) { 2     if ($this->request->is('get')) { 3         throw new MethodNotAllowedException(); 4     } 5     if ($this->Post->delete($id)) { 6         $this->Session->setFlash('The post with id: ' . $id . ' has been deleted.'); 7         $this->redirect(array('action' => 'index')); 8     } 9 }

这个逻辑根据 $id 删除指定的贴子,并且使用 $this->Session->setFlash() 在用户重定向到 /posts 之后显示一条确认信息。如果用户试图用 GET 请求执行删除,我们就抛出一个异常。我们没有捕获的异常,将被 CakePHP 的异常句柄捕获并显示一个友好的错误页。有许多内置的 异常 ,能够用于指示应用程序需要生成的不同的 HTTP 错误。

因为我们仅仅执行了一些逻辑就重定向了,这个动作没有视图。但是仍然可能需要更新 index 视图,添加允许用户删除贴子的链接:

 1 <!-- File: /app/View/Posts/index.ctp -->
 2 
 3 <h1>Blog posts</h1>
 4 <p><?php echo $this->Html->link('Add Post', array('action' => 'add')); ?></p>
 5 <table>
 6     <tr>
 7         <th>Id</th>
 8         <th>Title</th>
 9         <th>Actions</th>
10         <th>Created</th>
11     </tr>
12 
13 <!-- Here's where we loop through our $posts array, printing out post info --> 14 
15 <?php foreach ($posts as $post): ?> 16     <tr> 17         <td><?php echo $post['Post']['id']; ?></td> 18         <td> 19 <?php echo $this->Html->link($post['Post']['title'], array('action' => 'view', $post['Post']['id'])); ?> 20         </td> 21         <td> 22 <?php echo $this->Form->postLink( 23                 'Delete', 24                 array('action' => 'delete', $post['Post']['id']), 25                 array('confirm' => 'Are you sure?')); 26             ?> 27 <?php echo $this->Html->link('Edit', array('action' => 'edit', $post['Post']['id'])); ?> 28         </td> 29         <td> 30 <?php echo $post['Post']['created']; ?> 31         </td> 32     </tr> 33 <?php endforeach; ?> 34 
35 </table>

使用 postLink() 生成用 JavaScript 调用删除贴子请求的链接。允许使用 GET 请求删除贴子是非常危险的,web 爬虫可能会意外地删除全部内容。

注解

这个视图的代码也在删除之前使用 FormHelper 的 JavaScript 对话框提醒用户。