Сен 08 2008
Использование TreeBehavior в CakePHP
Сегодня у этого блога первый маленький юбилей — 10-й пост. Скорость набрана не плохая, буду очень стараться не уронить планку.
Очень часто в своих проектах нам приходится реализовывать хранение и вывод иерархических данных — всевозможные древовидные структуры разделов и подразделов статей, категории товаров интернет-магазинов, папки с фотографиями и тому подобное. Многие организуют такие списки через простейшие id, name, parent_id, но у этого способа масса недостатков. Один из них — необходимость рекурсии, если неизвестен уровень вложенности искомого раздела. К счастью, в CakePHP есть встроенное средство для работы с иерархическими данными TreeBehavior, работающий по алгоритму MPTT (Multi Path Tree Traversal). Подробнее об этом алгоритме я напишу в следующий раз. Все примеры кода взяты из официальной документации на CakePHP.
Для того, чтобы использовать TreeBehavior, в таблице с иерархическими данными надо добавить три целочисленных поля:
- Родитель — в нем будет храниться id родительского объекта. По-умолчанию должно называться parent_id.
- Слева — хранит номер левой границы текущего объекта (читай подробнее про алгоритм MPTT). По-умолчанию должно называться lft.
- Справа — хранит номер правой границы текущего объекта. По-умолчанию — rght.
В соответствии с документацией по CakePHP, поле «родитель» используется для упрощения поиска прямых потомков выбранного объекта. В классической реализации алгоритма MPTT это поле отсутствует.
Создадим таблицу категорий и заполним ее тестовыми данными:
CREATE TABLE categories (
id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
parent_id INTEGER(10) DEFAULT NULL,
lft INTEGER(10) DEFAULT NULL,
rght INTEGER(10) DEFAULT NULL,
name VARCHAR(255) DEFAULT ”,
PRIMARY KEY (id)
);INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(1, ‘My Categories’, NULL, 1, 30);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(2, ‘Fun’, 1, 2, 15);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(3, ‘Sport’, 2, 3, 8);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(4, ‘Surfing’, 3, 4, 5);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(5, ‘Extreme knitting’, 3, 6, 7);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(6, ‘Friends’, 2, 9, 14);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(7, ‘Gerald’, 6, 10, 11);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(8, ‘Gwendolyn’, 6, 12, 13);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(9, ‘Work’, 1, 16, 29);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(10, ‘Reports’, 9, 17, 22);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(11, ‘Annual’, 10, 18, 19);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(12, ‘Status’, 10, 20, 21);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(13, ‘Trips’, 9, 23, 28);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(14, ‘National’, 13, 24, 25);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(15, ‘International’, 13, 26, 27);
Создадим модель:
<?php
// app/models/category.php
class Category extends AppModel {
var $name = ‘Category’;
var $actsAs = array(’Tree’);
}
?>
И простой контроллер для проверки что тестовое дерево заполнено правильно:
<?php
class CategoriesController extends AppController {
var $name = ‘Categories’;function index() {
$this->data = $this->Category->generatetreelist(null, null, null, ‘ ’);
debug ($this->data); die;
}
}
?>
При заходе на страничку /categories/ должен быть выдан список, соответствующий следующему:
- My Categories
- Fun
- Sport
- Surfing
- Extreme knitting
- Friends
- Gerald
- Gwendolyn
- Sport
- Work
- Reports
- Annual
- Status
- Trips
- National
- International
- Reports
- Fun
1. Добавляем новую категорию. Это делается точно также как в любой другой модели:
$data = array();
$data['Category']['parent_id'] = 3;
$data['Category']['name'] = ‘Skating’;
$this->Category->save($data);
Если нужно создать корневую категорию, то просто не указываем parent_id:
$data = array();
$data['Category']['name'] = ‘Other Category’;
$this->Category->save($data);
2. Изменяем категорию. Опять все как обычно:
$this->Category->id = 5; // id of Extreme knitting
$this->Category->save(array(’name’ =>’Extreme fishing’));
Если не изменять parent_id, то при сохранении просто заменятся поля из переданного массива. Если же наоборот указать parent_id — то вся соответствующая ветка дерева будет перенесена в новое место.
$this->Category->id = 5; // id of Extreme fishing
$newParentId = $this->field(’id’, array(’name’ => ‘Other Category’));
$this->Category->save(array(’parent_id’ => $newParentId));
3. Удаляем категорию и всех ее потомков:
$this->Category->id = 10;
$this->Category->delete();
4. Ищем и получаем данные о категориях. Во всех функциях этой группы, TreeBehavior ожидает данные, отсортированные по столбцу lft. Если вы используете сортировку по другим столбцам в своем поиске, будьте внимательны и обязательно протестируйте результаты.
Функция children() возвращает всех потомков выбранного элемента. Если второй (необязательный) параметр установить в true, то функция вернет только прямых потомков.
$allChildren = $this->Category->children(1); // одномерный массив из 11 элементов
// или
$this->Category->id = 1;
$allChildren = $this->Category->children(); // одномерный массив из 11 элементов
// Пробуем получить только прямых потомков
$directChildren = $this->Category->children(1, true); // одномерный массив из 2-х элементов
Для получения рекурсивного массива можно использовать функцию find(’threaded’).
Функция childCount() возвращает количество потомков у выбранного элемента. Точно также — второй параметр указывает считать только прямых потомков или всех.
$totalChildren = $this->Category->childCount(1); // вернет число 11
$numChildren = $this->Category->childCount(1, true); // вернет число 2
Функция generatetreelist() возвращает массив с элементами дерева в порядке вложенности. Вложенность может быть обозначена префиксами, как в примере в моем предыдущем посте на эту тему.
Дальше пока еще сам не потестировал, когда внедрим в несколько своих проектов — напишу продолжение.