Не трудно догадаться из названия, что Tree Behavior (далее TB) позволит модели вести себя как дерево... кхм... :)
Стандартная задача для этих деревьев: есть много категорий для различных элементов с неограниченным уровнем вложенности. Нужно хранить их в базе и иметь возможность быстро получать вложенные категории, путь к текущей категории, количество детей и т.д.
Кстати, если не думать о Cake, то раньше я уже показывал, что надо освоить для трюков с деревьями.
Оригинал статьи от разработчиков CakePHP: Tree Behavior
Для использования Tree Behavior, в таблицу модели необходимо добавить 3 специальных поля:
- parent (по умолчанию это parent_id), используется для определения того, кто отец ребенка
- left (lft) - id левого узла
- right (rght) - правого
Программа минимум
Пример таблицы для категорий чего-либо:
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);
Стандартный код модели:
class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
}
?>
Для теста создается небольшой контроллер:
class CategoriesController extends AppController {
var $name = 'Categories';
function index() {
$this->data = $this->Category->generatetreelist(null, null, null, ' ');
debug ($this->data); die;
}
}
?>
По адресу /categories относительно URL нашего проекта будет такой вот список:
Добавление элементов
Стандартный способ добавления новой записи для модели выглядит след образом:
$data['Category']['parent_id'] = 3;
$data['Category']['name'] = 'Skating';
$this->Category->save($data);
При использовании TB не обязательно заполнять значение parent_id.
Если оно не указано, то TB добавит элемент в конец дерева:
$data = array();
$data['Category']['name'] = 'Новая категория';
$this->Category->save($data);
После этого добавления generatetreelist() вернет такой список:
- My Categories
- Fun
- Sport
- Surfing
- Extreme knitting
- Skating New
- Friends
- Work
- Новая категория
Изменение данных
Здесь все аналогично "обычным" моделям. Единтсвенное, что следует помнить:
нельзя изменять parent_id, если не хотите изменить структуру ваших данных.
$this->Category->id = 5;
$this->Category->save(array('name' =>'Extreme fishing'));
Обновленная версия данных имеет такой вид (слова «Обновлено» там не будет):
- My Categories
- Fun
- Sport
- Surfing
- Extreme fishing Обновлено
- Skating
- Friends
- Work
- Новая категория
Пример того, как можно передвинуть категорию на новое место:
$this->Category->id = 5;
$newParentId = $this->field('id', array('name' => 'Новая категория'));
$this->Category->save(array('parent_id' => $newParentId));
В строке 3 мы методом field() получили ID категории "Новая категория" по ее имени. Имеем новое дерево:
- My Categories
- Новая категория
- Extreme fishing Перемещено
Удаление данных
TB имеет несколько вариантов для удаления данных.
В данном учебном примере рассматривается самый простой:
удаление элемента и всех его детей обычным методом delete() вашей модели. Пример:
$this->Category->id = 10;
$this->Category->delete();
Удалилась категория "Reports" из категории "Work":
- My Categories
- Новая категория
Поиск
Большинство TB методов рассчитывает, что выбранные данные отсортированы по полю lft.
Если вызвать find() метод модели или один из методов класса TB,
то, в последствии, можно будет сильно удивиться полученным результатам.
Поиск детей
Метод children()
вернет всех детей категории, ID которой передается в первом параметре.
Второй необязательный параметр служит флагом для того, чтобы выбрать только детей первого уровня (если установлен в true).
Пример с исходными данным, которые получены после изучения метода delete() в этой статье:
$allChildren = $this->Category->children(1);
$this->Category->id = 1;
$allChildren = $this->Category->children();
$directChildren = $this->Category->children(1, true);
Такие вызовы дают обычные списки (массивы с одним уровнем вложенности).
Для рекурсивного массива используется find('threaded')
Подсчет детей
Аналогично методу children() работает childCount()
для подсчета количества элементов в категории:
$totalChildren = $this->Category->childCount(1);
$this->Category->id = 1;
$directChildren = $this->Category->childCount();
$numChildren = $this->Category->childCount(1, true);
Программа чуть больше, чем минимум
generatetreelist()
Уже известный метод, который показывает структуру наших данных.
getparentnode()
Возвращает родителя элемента.
getpath()
Возвращает путь к элементу по категориям.
moveDown() / moveUp()
Используется для перемещения нода вниз/вверх по дереву.
В качестве параметров принимает ID элемента, который нужно сдвинуть
и число позиций, на сколько нужно сдвинуть элемент.
Все дети передвигаемого элемента будут автоматически перемещены за родителем.
Пример одного из методов контроллера с именем Categories:
function movedown($title = null, $delta = null) {
$cat = $this->Category->findByTitle($title);
if (empty($cat)) {
$this->Session->setFlash('Не найдено категории с именем ' . $title);
$this->redirect(array('action' => 'index'), null, true);
}
$this->Category->id = $cat['Category']['id'];
if ($delta > 0) {
$this->Category->moveDown($this->Category->id, abs($delta));
} else {
$this->Session->setFlash('Укажите на сколько позиций необходимо сдвинуть категорию.');
}
$this->redirect(array('action' => 'show'), null, true);
}
Теперь, если зайти по адресу /categories/movedown/Cookies/1, то категория Cookies сдвинется на одну позицию вниз.
Если категории "Cookies" уже нет в базе, то получите ошибку :)