CakePHP 实例教程: Categories Acts as Tree

Tree是CakePHP 1.2的核心Behaviors之一,可以用来轻易的实现无限极分类,并呈现树状列表。

图片来源:Tree traversa,WIKIPEDIA

基础实例
建立数据表

CREATE TABLE `categories` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`parent_id` int(11) default NULL,
`lft` int(11) default NULL,
`rght` int(11) default NULL,
`link_count` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;

字段说明:parent_id,lft,rght均为必要字段,parent_id是很常用的表层级关系的做法。但在这里,lft和rght才是重点。它们表示的是一个分类的左右边界。原理如下图所示

图片来源:Managing Hierarchical Data in MySQL,dev.mysql.com
建立Model
使用bake shell

$ cd app
$ cake bake

Category Model:

class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
}

Categories Controller:

class CategoriesController extends AppController {
var $name = 'Categories';

Add Action

function add() {
if (!empty($this->data)) {
$this->Category->create();
if ($this->Category->save($this->data)) {
$this->Session->setFlash(__('The Category has been saved', true));
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash(__('The Category could not be saved. Please, try again.', true));
}
}
$this->set('categories', $this->Category->generatetreelist(null, null, null, '-- '));
}

add.ctp

<?php echo $form->create('Category');?>

<?php __('Add Category');?>
<?php echo $form->input('name');?>
<?php echo $form->input('slug');?>
<?php echo $form->select('parent_id', $categories);?>

<?php echo $form->end('Submit');?>

更进一步
取得层级分类

class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
function getThreadCategories() {
$this->recursive=-1;
return $this->findAllThreaded(null,null,'lft asc');
}

取得下属条目
当给分类下添加条目,比如Links时,基本的做法是

class Category extends AppModel {
var $name = 'Category';
var $hasMany = array(
'Links' => array('className' => 'Link',
'foreignKey' => 'category_id',
)
);
}

这样就通过Link.category_id连接了Link和对应的Category。
包含下级分类所属条目
但很多时候我们需要一个分类下的条目时,是需要包含下级分类所属条目的,此时需要添加finderQuery,并去除foreignkey:

class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
var $hasMany = array(
'Links' => array('className' => 'Link',
'foreignKey' => '',
'finderQuery' => '
SELECT
`Links`.*
FROM
`links` AS `Links`
JOIN
categories Child ON Child.id=Links.category_id
JOIN
categories Category ON Category.id={$__cakeID__$}
WHERE
(Child.lft >= Category.lft)
AND
(Child.rght <= Category.rght)
'
)
);
}

刷新条目计数

class Link extends AppModel {
var $name = 'Link';
var $belongsTo = array(
'Category' => array('className' => 'Category',
'foreignKey' => 'category_id',
)
);
function updateCounterCache ($keys = array(), $created = false) {
parent::updateCounterCache($keys, $created);
 
$category = $this->field('category_id');
if (!$category) {
return;
}
$conditions = array('Category.id' => $category);
$fields = array('id', 'lft', 'rght');
$recursive = -1;
$limits = $this->Category->find('first', compact('conditions', 'fields', 'recursive'));
$path = $this->Category->getPath($category, $fields);
foreach ($path as $category) {
extract ($category);
$conditions = array(
'Category.lft >=' . $Category['lft'],
'Category.rght <=' . $Category['rght'],
);
$this->Category->updateAll(
array('link_count' => intval($this->find('count', array('conditions' => $conditions)))),
array($this->Category->escapeField() => $Category['id'])
);
}
}
}

这样每当我们新建一个Link时,其所属分类及上级分类的link_count将会被自动刷新。有缺憾的是,如果我们是把一个Link从旧分类调整到新分类,旧分类的link_count不会更新。