关于本教程
本教程讲解如何使用 scaffolding 和 Bake 立即开发 CakePHP 应用程序。您还将了解使用 CakePHP 的 ACL 的详细信息。您将了解 scaffolding 是什么,以及它提供的功能。然后将了解如何使用 Bake 为 scaffold 生成代码,并根据需要做出调整。最后,将了解 ACL:ACL 是什么,如何创建它们,以及如何在应用程序中使用它们。本教程是在第一部分 http://my.oschina.net/u/1272301/blog/164674中创建的在线产品应用程序 Tor 之上构建的。
(如你发现本文有什么错误的地方,请指正,谢谢。)
前提条件
本教程假设您熟悉 PHP 编程语言,基本掌握数据库设计且喜欢动手操作。您不必完全掌握 MVC 模式,因为本教程将会介绍它的基本原理。最重要的是,您必须愿意学习、愿意投入其中并十分渴望缩短开发时间。
系统需求
开始之前,需要具备一个工作环境。CakePHP 的最低服务器需求为:
还需要准备好一个数据库以供应用程序使用。本教程将提供在 MySQL 中创建任何必需的表的语法。
下载 CakePHP 的最简单方法是访问 CakeForge.org 并下载最新的稳定版本。本教程采用的是 V2.4.1。还可以直接使用来自 Subversion 的每日构建和拷贝。CakePHP Manual 中有更详细的信息(请参阅 参考资料)。
目前为止的 Tor
在第 1 部分的末尾的补充功能中http://my.oschina.net/u/1272301/blog/164674,您有机会通过构建 Tor 缺少的一些功能将自己的技巧融入工作中。登录/注销、索引、使用散列密码以及自动登录注册用户都列于待完成的任务列表中。您是怎样做的?
login 视图
login 视图可能类似于清单 1。
清单 1. login 视图
<?php if (isset($error)) { echo('Invalid Login.'); } ?> <p>Please log in.</p> <?php echo $this->form->create('User', array('action' => 'login')); ?> <?php echo $this->form->input('username'); echo $this->form->input('password'); ?> <?php echo $this->form->end('Login');?> <?php echo $this->html->link('Register', array('action' => 'register')); ?> |
index 视图可能类似于清单 2。
清单 2. index 视图
<p>Hello, <?php echo($user['first_name'] . ' ' . $user['last_name']); ?></p> <?php echo $this->html->link('knownusers', array('action' => 'knownusers')); ?> <?php echo $this->html->link('logout', array('action' => 'logout')); ?> |
这两个视图看上去应当十分简单。index 视图仅在会话中检查用户的用户名,如果尚未设置,则让用户登录。login 视图并不设置特定的错误消息,因此尝试猜测系统登录方法的人无法知道哪些信息是正确的。
控制器可能类似于清单 3。
清单 3. 控制器
<?php class UsersController extends AppController { var $name = 'Users'; var $helpers = array('Html','Form'); var $components = array('Session'); public function register() { if(!empty($this->request->data)) { $this->request->data['User']['password'] = md5($this->request->data ['User']['password']); if($this->User->save($this->request->data)) { //在当前页面或跳转页面显示 $this->Session->setFlash('Your registation information was accepted.'); $this->Session->write('user', $this->request->data['User']['username']); $this->redirect(array('controller' => 'Users', 'action' => 'index'), null, true); //也可以写成这样$this->redirect('/users/index'); } else{ $this->data['User']['password'] = ''; $this->Session->setFlash('Your registation information was failed.'); //直接跳转到另一页面显示 //$this->flash('There was a problem with your registration', '/users/register'); } } } public function knownusers() { //使用find函数查找数据并传递给knownusers,$knownusers可在knownusers的视图中使用 $this->set('knownusers', $this->User->find( 'all', array( 'fields' => array('id','username','first_name','last_name'), 'order' => 'id DESC' ) )); } function login() { if($this->request->data) { $results = $this->User->findByUsername($this->request->data['User']['username']); if($results['User']['password'] == md5($this->request->data['User']['password'])) { $this->Session->write('user', $this->request->data['User']['username']); $this->redirect(array('action' => 'index'), null, true); }else{ $this->set('error', true); } } } function logout() { $this->Session->delete('user'); if($username) { $results = $this->User->findByUsername($username); $this->set('user', $results['User']); }else{ $this->redirect(array('action' => 'login'), null, true); } } function index() { $username = $this->Session->read('user'); if($username) { $results = $this->User->findByUsername($username); $this->set('user', $results['User']); }else{ $this->redirect(array('action' => 'login'), null, true); } } } ?>
使用 md5() 对密码进行散列并比较其散列值,这意味着不必将明文密码存储到数据库中 — 只需在存储之前计算这些密码的散列值。logout 操作不需要使用视图;它只需清除输入到会话中的值。
如果您的解决方案不完全相同,也没关系。如果还没有自己的解决方案,请使用以上内容更新代码,以便准备完成本教程的其余部分。
Scaffolding
到目前为止,Tor 还没有实现太多功能。它允许人员注册、登录和查看还有哪些人已注册。现在用户需要的功能就是能够将产品输入目录,或查看来自其他用户的产品。立即开始实现此功能的最佳方法就是使用 Scaffolding。
Scaffolding 是 Ruby on Rails 中的一个概念(请参阅 参考资料)。它是一种快速构建一些结构以提供应用程序原型的出色方法,而无需编写大量一次性使用的代码。但是顾名思义,Scaffolding 是应当用于帮助构建应用程序的,而不是用于构建应用程序外围组件的。如果希望 Scaffolding 有不同的表现,就应该放弃它了。
设置产品表
Scaffolding 会检查数据库表,并创建通常与表结合使用的基本类型元素:列表、添加/删除/编辑按钮等等,这些功能通常称为 Create、Read、Update、Delete(CRUD)。首先,需要使用一些表来保存产品信息和经销商信息。
清单 4. 创建保存产品信息的表
CREATE TABLE 'products' ( 'id' INT( 10 ) NOT NULL AUTO_INCREMENT , 'title' VARCHAR( 255 ) NOT NULL , 'dealer_id' INT( 10 ) NOT NULL , 'description' blob NOT NULL , PRIMARY KEY ('id') ) TYPE = MYISAM ; CREATE TABLE 'dealers' ( 'id' INT( 10 ) NOT NULL AUTO_INCREMENT , 'title' VARCHAR( 255 ) NOT NULL , PRIMARY KEY ('id') ) TYPE = MYISAM ; |
此外,将一些数据插入 dealers 表中会对此次演示很有帮助。
INSERT INTO dealers (title) VALUES ('Tor Johnson School Of Drama'), ('Chriswell\'s Psychic Friends') |
关于 Scaffolding,有一个重要的注意事项:还记得要设置数据库使其外键遵循 singular_id 格式(如 user_id 或 winner_id)吗?在 CakePHP 中,scaffolding 假设以 _id 为结尾的所有字段都是表名加上后缀 _id 形成的外键 — 例如,Scaffolding 认为 dealer_id 是表 dealers 的外键。
设置 product 模型
产品功能需要一整套新的模型、视图和控制器。需要像第 1 部分中一样创建它们。在 app/models/product.php 中创建 product 模型。
清单 5. 创建 product 模型
<?php class Product extends AppModel { var $name = 'Product'; var $belongsTo = array ('Dealer' => array( 'className' => 'Dealer', 'conditions' => '', 'order' => '' , 'foreignKey'=>'dealer_id') ); } ?> |
请注意 $belongsTo 变量。这就是所谓的模型关联。
模型关联
模型关联(model association) 用于告诉模型以某种方式与另一个模型关联。在模型之间设置正确的关联将允许把实体及其关联模型作为整体来处理,而不是单独处理。在 CakePHP 中,有四种模型关联:
hasOnehasOne 关联告诉模型其中的每个实体都在另一个模型中有一个对应的实体。这种关联的示例是与用户实体对应的配置文件实体(假定一个用户仅允许有一个配置文件)。hasManyhasMany 关联告诉模型其中的每个实体都在另一个模型中有若干个对应的实体。这种关联的示例是在 category 模型中许多东西属于同一个类别(邮件、产品等等)。在 Tor 中,一个经销商实体有很多产品。belongsTo这种关联告诉模型其中的每个实体都指向另一个模型中的一个实体。这种关联与 hasOne 刚好相反,因此,一个示例是指向一个对应用户实体的配置文件实体。hasAndBelongsToMany这种关联表示一个实体在另一个模型中有多个对应实体,而且还指向另一个模型中的多个对应实体。这种关联的示例可能是菜谱。很多人可以喜欢一个菜谱,并且菜谱可以有多种成分。
本例中的 belongsTo 变量表示 products 表中的每个产品都 “属于” 一个特定的经销商。
创建 dealer 模型
正如关联所示,还需要一个 dealer 模型。稍后将在 Tor 中使用 dealer 模型来构建定义经销关系的功能。而 product 模型具有指向经销商的 belongsTo 关联,dealer 模型具有指向产品的 hasMany 关联。
清单 6. dealer 模型具有指向产品的 hasMany 关联
<?php class Dealer extends AppModel { var $name = 'Dealer'; var $hasMany = array ('Product' => array( 'className' => 'Product', 'conditions' => '', 'order' => '', 'foreignKey'=>'dealer_id') ); } ?> |
现在可以跳过添加数据检验的步骤,但随着应用程序的发展,您可能会希望添加各种检验。
创建 products 控制器
已经构建并关联了用于产品和经销商的模型。现在,Tor 知道如何关联数据。接下来,在app/controllers/ProductsController.php 中构建控制器 — 但是这一次,添加类变量 $scaffold。DealerController的内容与ProductsController的相同,请自行添加。
清单 7. 在控制器中添加类变量
<?php class ProductsController extends AppController { var $scaffold; } ?> |
保存控制器,然后访问 http://localhost/products(是的,不创建任何视图或 dealers 控制器)。应当会看到图 1。
它实际上十分简单。如图所示,现在有了一个用于操作 products 表的界面,允许添加、编辑、删除、列出产品、分段及细化您的产品。
尝试添加一个产品。系统应当会提示您输入产品的名称和描述,还要选择一个经销商。这个经销商列表是不是很眼熟?应当是这样;在创建了 dealers 表后,已将这些经销商插入其中。Scaffolding 在定义表关联时将其识别出来,并自动生成下拉式经销商列表。
现在返回去看一看为实现此功能而编写的代码量。是不是可以更轻松地实现此功能呢?
使用 Bake 代码生成器
没必要完全抛弃 Scaffolding 提供的全部内容。通过使用 Bake(CakePHP 代码生成器),可以生成一个包含 Scaffolding 功能和相关视图的控制器。对于 Tor 中与产品相关的部分,这将是一个节省大量时间的方法。
在 CakePHP V1.1 中,Bake 是一个直接调用的 PHP 脚本。在 CakePHP V2.4.1. 中,Bake 功能已经转移到了 Cake Console 中,在本教程后面您会逐渐熟悉 Cake Console。将路径 /lib/cake/console 添加到环境的 PATH 变量中,可以简化操作。设置这个变量之后,就可以直接调用 Cake Console 而不需要指定路径信息。这种做法并不是必需的,本教程假设不这样做。另外,应该从应用程序的 app 目录(在本示例中是 /app)运行 Cake Console,否则 Cake Console 会认为您打算对新的应用程序执行操作。
在继续执行操作之前,请制作现有 app 目录的拷贝。Bake 将覆盖 products 控制器,而且当操作包括关键字 “overwrite”(或者 “copy”、“delete”、“format” 或 “voodoo”)时,应当经常备份文件。如果遇到问题,请确保 php 位于环境的 PATH 变量中。
对 products 控制器使用 Bake
要想使用 Bake,应该使用 cd 命令切换到 /app 目录并启动 执行命令:.\console\cake bake。您应当会看到一个与图 2 相似的屏幕。(注意:本文使用windows环境下的cmd命令行,所以文件路径分隔符为‘\’,若在linux下操作请将‘\’改为‘/’)
图 2. Bake 菜单
对于 Tor 应用程序,已编写的模型应当不会有问题,所以让我们开始编写控制器。按 C 键选择控制器。Cake Console 将查看目前操作的应用程序,并列出可以运行 Bake 的控制器。
图 3. 控制器名称
在这个示例中,我们要对 products 控制器运行 Bake,它的控制器编号应该是 2。Bake 将询问您是否希望以交互方式构建控制器。目前,按 N 键让 Bake 自己做出所有决定,但是稍后您应当尝试以交互方式构建一个控制器,感受一下 Bake 的其他功能。接下来,Bake 会询问是否希望包含一些基本的类方法(见图 4)。按 Y(因为我们使用 Bake 的目的就是获得这些代码)。接下来,Bake 会询问是否希望创建用于管理路由的方法。按 N(目前不需要这些方法)。Bake 应当会通知您它创建了文件 \app\controllers\products_controller.php,并询问是否希望创建一些单元测试文件。目前可以跳过它们。完成操作之后,会回到 Bake 菜单。
大功告成!退出 Cake Console,打开 app/controllers/products_controller.php,看看 Bake 为您实现了什么功能。它应当类似于清单 8。
清单 8. 运行 Bake 后的 ProductsController.php
<?php App::uses('AppController', 'Controller'); /** * Products Controller * * @property Product $Product * @property PaginatorComponent $Paginator */ class ProductsController extends AppController { /** * Components * * @var array */ public $components = array('Session','Paginator'); /** * index method * * @return void */ public function index() { $this->Product->recursive = 0; $this->set('products', $this->Paginator->paginate()); } /** * view method * * @throws NotFoundException * @param string $id * @return void */ public function view($id = null) { if (!$this->Product->exists($id)) { throw new NotFoundException(__('Invalid product')); } $options = array('conditions' => array('Product.' . $this->Product->primaryKey => $id)); $this->set('product', $this->Product->find('first', $options)); } /** * add method * * @return void */ public function add() { if ($this->request->is('post')) { $this->Product->create(); if ($this->Product->save($this->request->data)) { $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); } /** * edit method * * @throws NotFoundException * @param string $id * @return void */ public function edit($id = null) { if (!$this->Product->exists($id)) { throw new NotFoundException(__('Invalid product')); } if ($this->request->is(array('post', 'put'))) { if ($this->Product->save($this->request->data)) { $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } else { $options = array('conditions' => array('Product.' . $this->Product->primaryKey => $id)); $this->request->data = $this->Product->find('first', $options); } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); } /** * delete method * * @throws NotFoundException * @param string $id * @return void */ public function delete($id = null) { $this->Product->id = $id; if (!$this->Product->exists()) { throw new NotFoundException(__('Invalid product')); } $this->request->onlyAllow('post', 'delete'); if ($this->Product->delete()) { $this->Session->setFlash(__('The product has been deleted.')); } else { $this->Session->setFlash(__('The product could not be deleted. Please, try again.')); } return $this->redirect(array('action' => 'index')); }}
使用 Cake Console 生成的这个控制器包含的基本功能与通过 Scaffolding 生成的控制器相同,但是现在可以根据需要做出调整。十分方便!准备好继续了吗?
对 products 视图使用 Bake
现在已经对 products 控制器运行了 Bake,Tor 还需要一些产品视图。Bake 也将替您执行这些操作。像以前一样,在/app 目录中执行 ..\console\cake bake。
最初的 Bake 菜单应当与对控制器运行 Bake 时一样。不过,这一次要生成一些视图。按 V 键选择视图。系统将列出可以运行 Bake 的视图列表。products 仍然应该位于列表中的第二位。Bake 将询问是否希望以交互方式构建视图。目前,按 N 键让 Bake 自己做出所有决定。Bake 还会询问是否希望生成用于管理路由的视图。目前也跳过它们。您以后可以试试交互式 Bake 和管理路由选项。
跳过这两个选项之后,Bake 应当会通知您它已经创建了视图。
退出 Cake Console 并打开 app/views/products/index.ctp 视图。它应当类似于清单 9。
清单 9. index 视图
<div class="products index"> <h2><?php echo __('Products'); ?></h2> <table cellpadding="0" cellspacing="0"> <tr> <th><?php echo $this->Paginator->sort('id'); ?></th> <th><?php echo $this->Paginator->sort('title'); ?></th> <th><?php echo $this->Paginator->sort('dealer_id'); ?></th> <th><?php echo $this->Paginator->sort('description'); ?></th> <th class="actions"><?php echo __('Actions'); ?></th> </tr> <?php foreach ($products as $product): ?> <tr> <td><?php echo h($product['Product']['id']); ?> </td> <td><?php echo h($product['Product']['title']); ?> </td> <td> <?php echo $this->Html->link($product['Dealer']['title'], array('controller' => 'dealers', 'action' => 'view', $product['Dealer']['id'])); ?> </td> <td><?php echo h($product['Product']['description']); ?> </td> <td class="actions"> <?php echo $this->Html->link(__('View'), array('action' => 'view', $product['Product']['id'])); ?> <?php echo $this->Html->link(__('Edit'), array('action' => 'edit', $product['Product']['id'])); ?> <?php echo $this->Form->postLink(__('Delete'), array('action' => 'delete', $product['Product']['id']), null, __('Are you sure you want to delete # %s?', $product['Product']['id'])); ?> </td> </tr> <?php endforeach; ?> </table> <p> <?php echo $this->Paginator->counter(array( 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') )); ?> </p> <div class="paging"> <?php echo $this->Paginator->prev('< ' . __('previous'), array(), null, array('class' => 'prev disabled')); echo $this->Paginator->numbers(array('separator' => '')); echo $this->Paginator->next(__('next') . ' >', array(), null, array('class' => 'next disabled')); ?> </div> </div> <div class="actions"> <h3><?php echo __('Actions'); ?></h3> <ul> <li><?php echo $this->Html->link(__('New Product'), array('action' => 'add')); ?></li> <li><?php echo $this->Html->link(__('List Dealers'), array('controller' => 'dealers', 'action' => 'index')); ?> </li> <li><?php echo $this->Html->link(__('New Dealer'), array('controller' => 'dealers', 'action' => 'add')); ?> </li> </ul> </div>
再看看其他视图。这节省了大量必要的代码编写工作。稍后,将调整这些视图以帮助改进 Tor。
测试
已经用 Bake 为产品功能生成了一个控制器和必要的视图。现在让应用程序运转起来。访问 http://localhost/products 并浏览应用程序的各个部分。添加产品,编辑一个产品,删除另一个产品,查看产品。它看上去应当与使用 Scaffolding 时的表现完全相同。
通过 Bake 生成更大更好的应用程序
这些并不是 Bake 能实现的所有功能。本教程末尾将有几个练习,让您可以尝试自己的更多想法。请记住,Bake 所生成的代码仅仅是一个起点,而不是开发工作的终结。但是如果正确使用,它能显著地节省时间。
访问控制列表
到目前为止,Tor 在访问方面还是十分开放的。例如,任何人都可以添加、编辑或删除产品等等。现在需要对访问进行限制。为了实现访问控制,我们将使用 CakePHP 的访问控制列表(Access Control List,ACL)功能。
什么是 ACL?
ACL 其实是一个权限列表。这就是 ACL 的全部内容。它不是执行用户身份验证的方法,也不是解决 PHP 安全问题的灵丹妙药。ACL 只是一个指定操作者能执行哪些操作的列表。
操作者 通常是用户,但也可以是控制器之类的组件。操作者 被称为访问请求对象(Access Request Object,ARO)。这里所说的操作 通常将意味着 “执行某些代码”。操作 被称为访问控制对象(access-control object,ACO)。
因此,ACL 是一个 ARO 及它们可以访问的 ACO 的列表。很简单,是不是?它应当是很简单的,但实际上它并不简单。
“指定操作者能执行哪些操作的列表” 这个定义确实很简单,但是实际过程涉及许多三个字母的缩写词(TLA),所以可能把人弄糊涂。但是示例会帮助您。
想象在某个夜总会中正在举办一个派对,宾客云集。派对分为几个部分 — 有 VIP 休息室、舞池和主吧台。当然,很多人等着进入夜总会。门口有身形魁梧的保镖检查来宾的 ID,他们查看名单,然后,或者拒绝来宾进入夜总会,或者让其参加仅对受邀者开放的派对部分。
那些来宾就是 ARO。他们请求访问派对的不同部分。VIP 休息室、舞池和主吧台全都是 ACO。ACL 是门口身形魁梧的保镖手中的名单。身形魁梧的保镖就是 CakePHP。
创建 ACL 表
在 CakePHP V1.1 中,ACL 管理的运行方式与 Bake 一样,要通过一个直接调用的 PHP 脚本。在 CakePHP V2.4.1 中,ACL 管理功能已经转移到了 Cake Console 中。可以使用 Cake Console 设置一个用来存储 ACL 信息的数据库表。在 /app 目录中,从命令行运行以下命令:.\console\cake acl initdb。Cake Console 告诉您它已经建立了三个数据库(见图 6):acos、aros 和 aros_acos。
这就是开始时需要执行的所有操作。现在开始定义 ARO 和 ACO。
定义 ARO
现在有了 ACL 数据库表,并有了一个允许用户自己注册的应用程序。如何为用户创建 ARO 呢?
将此组件添加到应用程序的注册部分会更有意义。这样,当新用户注册时,系统会为其自动创建对应的 ARO。这意味着必须为已创建的用户手工创建几个 ARO,但是 CakePHP 还是能让您轻松完成这个任务。
定义组
在 CakePHP(通常是在使用 ACL 时)中,可以为了分配或撤消权限而将用户指定给组。因为无需处理单个用户权限(这在应用程序有多个用户时会变成一项艰巨的任务),这可以极大地简化权限管理任务。
对于 Tor,将定义两个组。第一个组(称为 Users)将包含仅仅注册了帐户的所有人。第二个组(称为 Dealers)将用于在 Tor 内将额外权限授予特定用户。
将使用 Cake Console 创建这两个组,操作与创建 ACL 数据库十分类似。为了创建组,需要在 /app 目录中执行以下命令。
.\Console\cake acl create aro 0 Users .\Console\cake acl create aro 0 Dealers |
在执行每个命令之后,Cake Console 应当会显示一个消息,指出 ARO 已经创建。
New Aro 'Users' created. New Aro 'Dealers' created. |
传入的参数(例如,'root Users')是 parent 和 node。parent 参数是 ARO 应该属于的一个组。因为这些组处于顶层,所以传递 0。node 参数是用来引用这个组的字符串。
在注册部分中添加创建 ARO 的过程
在 Tor 的用户注册部分中添加创建 ARO 的过程并不难。只需包含正确的组件并添加几行代码。回忆一下,UsersController.php 中的注册函数应当类似于清单 10。
function register() { if (!empty($this->data)) { $this->request->data['User']['password'] = md5($this->request->data['User'] ['password']); if ($this->User->save($this->data)) { $this->Session->setFlash('Your registration information was accepted'); $this->Session->write('user', $this->data['User'] ['username']); $this->redirect(array('action' => 'index'), null, true); } else { $this->request->data['User']['password'] = ''; $this->Session->setFlash('There was a problem saving this information'); } } } |
为了开始使用 CakePHP 的 ACL 组件,需要包含该组件作为一个类变量。
<?php class UsersController extends AppController { var $components = array('Session','Acl'); ... |
$components 数组仅包含要使用的 CakePHP 组件的列表(按名称)。组件对于控制器的作用就如同 helper 对视图的作用。还有其他组件可用,例如将在后续教程中介绍的安全性组件。在本例中,只需要 ACL 组件。
现在您可以访问 ACL 组件提供的所有功能。创建 ARO 的方法是调用 ACL 的 ARO 对象上的 create(见清单 12)。这个方法接受的参数与从模型调用 create 方法时传递的参数相同,这两种方式本质上是相同的。在本例中,指定别名(用户名)、ACL 指向的模型(user)、记录的 foreign_key(新用户的 ID)和 parent_id(父节点的 ID,在这里是 Users ARO 组,是用下面的 findByAlias 行找到的)。要为用户创建 ARO,还需要知道保存用户后用户的 ID 是什么。当数据被保存后,可以通过 $this->User->id 来获取此信息。
register 函数现在可能类似于清单 12。
清单 12. register 函数
public function register() { if(!empty($this->request->data)) { //对密码进行md5加密(此处处理后,对密码验证失效) $this->request->data['User']['password'] = md5($this->request->data['User']['password']); if($this->User->save($this->request->data)) { //在当前页面或跳转页面显示 $this->Session->setFlash('Your registation information was accepted.'); $this->Session->write('user', $this->request->data['User']['username']); $parent = $this->Acl->Aro->findByAlias('Users'); $this->Acl->Aro->Create(array( 'alias' => $this->data['User']['username'], 'model' => 'User', 'foreign_key' => $this->User->id, 'parent_id' => $parent['Aro']['id']) ); $this->Acl->Aro->save(); $this->redirect(array('controller' => 'Users', 'action' => 'index'), null, true); //也可以写成这样$this->redirect('/users/index'); } else{ $this->data['User']['password'] = ''; $this->Session->setFlash('Your registation information was failed.'); //直接跳转到另一页面显示 //$this->flash('There was a problem with your registration', '/users/register'); } }
您会注意到,在数据成功保存之前,不会创建 ARO。
试运行
这就是设置 ARO 并运行所需的全部操作。为了进行检验,请返回 webroot/app 目录并使用 Cake Console 查看 ARO 树:../cake/console/cake acl view aro。输出应当类似于图 7。
图 7. ACL shell view aro 对空列表的输出
现在访问 http://localhost/users/register,并注册一个新用户。完成后,重新运行 ../cake/console/cake acl view aro 命令。输出应当类似于图 8。
图 8. ACL shell view aro 对一个用户的输出
从现在开始,只要有人注册新帐户,系统就会自动地为他创建一个 ARO。这个 ARO 将属于组 Users。
从现在开始,只要有人注册新帐户,系统就会自动地为他创建一个 ARO。这个 ARO 将属于组 Users。
为现有用户创建 ARO
新的 Tor 用户已经能够创建自己的 ARO,现在需要为现有用户创建 ARO。按照与创建组几乎完全相同的方法,用 Cake Console 执行此操作。首先,使用刚创建的用户访问 http://localhost/users/knownusers 以获取已创建的用户的列表。
图 9. 现有用户列表
然后,对于每个用户,需要像创建组一样执行 create aro 命令。对于 parent,指定 'Users'。对于 node,指定用户名。例如,要为图 9 中的 dentarthurdent 创建 ARO,应该执行以下命令(同样从/app 目录执行):..\console\cake acl create aro Users dentarthurdent。
确保对 knownusers 列表中的每个用户运行这些命令,但是在用户注册过程中为测试 ARO 创建功能而创建的用户除外。确保为每个用户指定正确的用户 ID 和用户名。完成后,.\console\cake acl 的结果应当类似于图 10。
通过在命令行中运行 .\console\cake acl help,可以了解 Cake Console 能够执行的其他 ACL 功能。
定义 ACO
现在 Tor 已经定义了自己的 ARO,应当识别并定义 ACO 了。在本例中,将定义 ACO 来表示产品,并将 ACO 组织为组,就像为 ARO 所做的那样。
在 products 控制器中添加 ACO 定义
将在 products 控制器的 add 函数中添加最初的 ACO 定义,类似于在用户注册中处理 ARO 定义所做的操作。add 函数很快就会完全具有 Bake 所提供的功能。它应当类似于清单 13。
清单 13. add 函数
public function add() { if ($this->request->is('post')) { $this->Product->create(); if ($this->Product->save($this->request->data)) { $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); }
CakePHP 同样使得为 ACO 添加定义十分简单。首先,在控制器中添加 $components 类变量,正如您对 users 控制器所做的操作。
清单 14. 在控制器中添加 $components 类变量
<?php class ProductsController extends AppController { var $components = array('Paginator','Acl'); ... |
创建 ACO 的过程几乎与创建 ARO 的过程完全一样。调用 ACL 的 ACO 对象上的 create 方法。这一次,别名仅仅使用产品的标题是不够的,因为产品标题可能不是惟一的。可以使用产品 ID 和产品标题的组合作为别名。模型是 Product,foreign_key 是新产品的 ID,parent_id 是插入此产品的经销商的 ID(目前还没有设置这个 ID,稍后完成)。将这些整合到 add 函数中,它应当类似于清单 15。
清单 15. 新的 add 函数
public function add() { if ($this->request->is('post')) { $this->Product->create(); if ($this->Product->save($this->request->data)) {
//添加ACO定义 $dealer = $this->Product->Dealer->read(null, $this->data['Product']['dealer_id']); $parent = $this->Acl->Aco->findByAlias($dealer['Dealer']['title']); $this->Acl->Aco->create(array( 'alias' => $this->Product->id.'-'.$this->request->data ['Product']['title'], 'model' => 'Product', 'foreign_key' => $this->Product->id, 'parent_id' => $parent['Aco']['id']) ); $this->Acl->Aco->save(); $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); }
以上就是在 Tor 中为创建的产品自动创建 ACO 所需的全部操作。在继续执行操作之前,应当为现有产品和组创建 ACO。
为经销商添加 ACO 定义
可以使用 Cake Console 定义 ACO,方法与为现有用户定义 ARO 的方法几乎一样。访问 CakePHP 在 http://localhost/products 上生成的产品列表会有帮助。
再次通过命令行在/app 目录中运行几个 create 命令。首先,创建组来表示在创建 dealers 表时创建的经销商。但是这一次,指定要创建 ACO。
.\console\cake acl create aco root "Tor Johnson School Of Drama" .\console\cake acl create aco root "Chriswell's Psychic Friends" |
可以运行 .\console\cake acl view aco 来检验组是否符合预期。
图 11. 对于没有产品的经销商的 ACO 输出接下来,从 products 表中删除现有产品。访问产品索引(http://localhost/products/index)并单击每个产品旁边的 Delete,就能够完成此操作。
由于到目前为止只创建了几个产品,因此删除并重新创建它们是添加 ACO 最简单的方法。请先不要测试新产品的 add 函数。现在已经为现有经销商创建了 ACO 并已删除了现有产品,下面要设置一些权限。
指定权限
现在 Tor 有多个表示用户的 ARO,并可以创建表示产品的 ACO(按经销商分组)。现在需要通过定义一些权限来整合它们。
权限的工作方式
我们将明确定义哪些人有权对产品执行操作。通过明确允许 ARO(本例中为用户)具有对 ACO(本例中为产品)和一个操作的权限来完成这项任务。操作可以是读取(意味着用户可以查看数据库信息)、创建(用户可以将信息插入数据库中)、更新(用户可以修改信息)、删除(用户可以从数据库中删除信息)或者 *(意味着用户可以执行所有操作)。每个操作都必须单独授权;允许删除并不意味着允许创建或查看。
默认情况下,在检查权限时,如果没有定义权限,则 CakePHP 假定权限为 DENY。
回页首
定义策略
定义权限策略不仅仅是编写和执行代码。需要考虑 ACL 实际上要完成哪些功能。如果没有清楚地了解需要阻止哪些人执行哪些操作,就会发现经常需要重新定义权限。
Tor 有用户和产品。为了实现本教程的目的,将允许创建产品的用户具有完全权限,可以编辑和删除产品。任何用户都能够查看产品,除非明确拒绝访问。
回页首
在产品 add 函数中添加权限定义
Tor 需要知道在创建产品时如何指定权限。这可以通过在控制器中添加两行来完成。一行用于为用户添加查看权限,另一行用于为创建产品的用户添加完全权限。授予权限的语句如下所示:$this->Acl->allow(ARO, ACO, TYPE);。
如果不指定一种 TYPE(create、read、update 或 delete),则 CakePHP 将假定您要授予完全权限。products 控制器中新的 add 函数应当类似于清单 16:
清单 16. products 控制器中新的 add 函数
public function add() { if ($this->request->is('post')) { $this->Product->create(); if ($this->Product->save($this->request->data)) { //添加ACO定义 $dealer = $this->Product->Dealer->read(null, $this->data['Product']['dealer_id']); $parent = $this->Acl->Aco->findByAlias($dealer['Dealer']['title']); $this->Acl->Aco->create(array( 'alias' => $this->Product->id.'-'.$this->request->data['Product']['title'], 'model' => 'Product', 'foreign_key' => $this->Product->id, 'parent_id' => $parent['Aco']['id']) ); $this->Acl->Aco->save(); //添加权限定义 $this->Acl->allow('Users', $this->Product->id .'-'. $this->data['Product']['title'], 'read'); $this->Acl->allow($this->Session->read('user'), $this->Product->id .'-'. $this->data['Product']['title'],'*'); $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); }
OK — 现在可以添加一些产品。作为某一用户登录并添加几个产品,只要看看整个过程有没有问题即可。可以使用 Cake Console 查看在添加新产品时创建的 ACO。现在差不多完成了。已经定义了 ARO、ACO 并指定了权限。现在,在执行与产品相关的各种操作时,Tor 都需要检查权限。
让 ACL 发挥作用
已经准备好了所有组件,现在可以让 ACL 发挥作用了。完成后,系统将允许所有用户在 Tor 中查看产品,但只有创建产品的用户才能够编辑或删除该产品。
将在 products 控制器的每个操作中添加几行。这几行将检查用户是否具有访问权,然后根据权限允许或拒绝操作。
仅允许用户查看产品
首先处理 view 操作。添加一行以检查对产品的访问权,如果不允许操作,则显示一条消息。
public function view($id = null) { if (!$this->Product->exists($id)) { throw new NotFoundException(__('Invalid product')); $this->redirect(array('action' => 'index')); } $product = $this->Product->read(null, $id); //检查对产品的访问权 if($this->Acl->check($this->Session->read('user'), $id .'-'. $product['Product']['title'], 'read')){ $this->set('product', $product); }else{ $this->Session->setFlash('Only registered users may view this product.'); $this->redirect(array('action' => 'index')); } $options = array('conditions' => array('Product.' . $this->Product->primaryKey => $id)); $this->set('product', $this->Product->find('first', $options)); }
保存文件,确保退出 Tor 并访问位于 http://localhost/products 的产品列表。当单击任何产品时,应当会被重定向到 User Registration 页面,见图 12。
现在,使用任意帐户登录并重试。这一次应当能够查看产品,结果见图 13。
图 13. 查看产品
已经解决了第一部分权限。现在需要告诉 Tor 拒绝除产品创建者以外的其他人对产品进行编辑和删除。
仅允许产品创建者编辑或删除产品
为 products 控制器中的 edit 和 delete 操作设置权限的过程是一样的。
清单 18. edit 操作
public function edit($id = null) { $product = $this->Product->read(null, $id); if($this->Acl->check($this->Session->read('user'), $id.'-'.$product['Product']['title'], 'update')){ if (!$this->Product->exists($id)) { throw new NotFoundException(__('Invalid product')); } if ($this->request->is(array('post', 'put'))) { if ($this->Product->save($this->request->data)) { $this->Session->setFlash(__('The product has been saved.')); return $this->redirect(array('action' => 'index')); } else { $this->Session->setFlash(__('The product could not be saved. Please, try again.')); } } else { $options = array('conditions' => array('Product.' . $this->Product->primaryKey => $id)); $this->request->data = $this->Product->find('first', $options); } $dealers = $this->Product->Dealer->find('list'); $this->set(compact('dealers')); } else{ $this->Session->setFlash('You cannot edit this product.'); $this->redirect(array('action' => 'index'),null,true); } }
对于 delete 操作,应当添加几行,用来删除产品的 ACO。delete 操作类似于清单 19。
清单 19. delete 操作
public function delete($id = null) { $product = $this->Product->read(null, $id); if($this->Acl->check($this->Session->read('user'), $id.'-'.$product['Product']['title'], 'delete')){ if (!$this->Product->exists()) { throw new NotFoundException(__('Invalid product')); } $this->request->onlyAllow('post', 'delete'); if ($this->Product->delete()) { //删除ACO $aco = $this->Acl->Aco->findByAlias($id .'-'.$product['Product']['title']); $this->Acl->Aco->delete($aco['Aco']['id']); $this->Session->setFlash(__('The product has been deleted.')); } }else { $this->Session->setFlash(__('The product could not be deleted. Please, try again.')); } return $this->redirect(array('action' => 'index')); }
保存 products 控制器并尝试使用它。首先在 http://localhost/users/logout 上注销,然后返回到位于 http://localhost/products/ 的产品列表并尝试编辑或删除一个产品。系统将显示一条消息并将您重定向回产品列表。
图 14. 编辑或删除失败
现在以用户 wrestler 的身份登录并尝试编辑一个产品,然后将其删除。您应当不会遇到任何问题。(编辑修改产品名称后将不能再编辑,此与aco表未更新有关,请自行研究)
图 15. 编辑或删除成功
补充功能
在 CakePHP 中,可以使用 Scaffolding 和 Bake 轻松快捷地构建应用程序的各个部分。通过使用 ACL,可以对应用程序的许多方面实施控制。还有很多可以做的事情。下面是一些可以尝试的练习。
经销商
正如您可能在 Bake 构建的 products 视图中所注意到的,index 视图中有指向经销商的链接。与对产品执行的操作一样,使用 Cake Console 为经销商构建控制器和视图。不要构建模型,因为已经定义了一个模型并与产品关联了起来。
修改经销商的 add 操作来检验经销商名称是否是惟一的。
ACL
products 控制器的 add 操作中有一个 bug。它不检查哪些人可以创建产品。此功能应当仅对用户可用。修正这个 bug。
构建了经销商后,可以使用学到的 ACL 技巧防止不属于经销商组的任何人使用任何经销商功能。
完成后,使用 ACL 允许任何用户创建经销商。您会发现为产品创建的 ACO 转入了表示经销商的 ACO 组。如何设置 ACL 以便允许任何经销商成员更改产品,但只有产品创建者才能删除产品?
视图
在 products index 视图中,仅对用户能够编辑或删除的产品显示 Edit 和 Delete 按钮。
结束语
Scaffolding 是一种快速建立应用程序的出色方法,而 Bake 是迅速建立某种结构的方法。通过使用 CakePHP 的 ACL,可以在应用程序内实现许多细粒度的控制。本教程仅介绍了 CakePHP 帮助简化工作并加快开发速度的几种方法。
第 3 部分 将讲解如何使用 Sanitize(一种便利的 CakePHP 类),它通过清除用户提交的数据帮助确保应用程序的安全性。