17 августа 2008 г.

Используем TreeBehavior в CakePHP 1.2

Это вовсе не полное руководство, а пара замечаний к практическому использованию.

TreeBehavior – весьма полезный компонент CakePHP, реализующий паттерн Nested Sets для указанной модели. Если что-то непонятно, то вот ссылки:

  1. Про паттерны древовидных структур
  2. Что такое Behavior в CakePHP 1.2
  3. TreeBehavior

TreeBehavior позволяет хранить несколько корневых элементов, у которых родительский id равен NULL. Не поддавайтесь искушению – для себя сделайте отдельный корневой элемент. Большинство функций, например Model::children не работают с идентификатором записи равной NULL. Поэтому, если возникнет необходимость получить всех потомков корня, придется сначала выбрать самостоятельно все элементы с parent_id=NULL, а потом для каждого из них вызвать Model::children().

То есть, если создается, скажем, структурированный каталог сайтов, не надо поддаваться искушению добавить основные разделы, как корневые – создайте сначала “совсем-совсем” корневой элемент с именем, скажем, ROOT, а к нему уже добавлять реальные разделы, которые будут использоваться. Должна получиться такая структура:

ROOT (id=1, parent_id=0)
   |
   +-- Сайты о программировании (id=2, parent_id=1)
   |
   +-- Блоги (id=3, parent_id=1)
   |
   +-- CakePHP (id=4, parent_id=1)
       |
       +----- Форумы о Cake
       |
       +----- Компоненты Cake

Зато, при таком подходе можно использовать одну модель для хранения разных деревьев.

И еще. Я либо плохо искал, либо это действительно так. В TreeBehavior я не нашел метода, чтобы получить массив потомков узла в виде дерева. Метод children возвращает “плоский” массив. Пришлось написать небольшой метод для моей модели, который вызывает children, а потом каждому элементу массива добваляет элемент ‘children’ который содежит массив потомков, у которых тоже добвляет элемент ‘children’ и т.д.

Если главные элементы у модели, все-таки все с parent_id=NULL, функция тоже будет работать. Но лучше все-таки использовать схему, описанную мной выше.


function childrenAsTree($options=null) {

  // Если TreeBehavior неактивен, то можно и не
  // напрягаться
  if ($this->Behaviors->enabled('Tree')) {

    if (is_array($options)) {
      if (array_key_exists('id', $options)) {
        $_id = $options['id'];
      } else {
        $_id = null;
      }
    } else {
      $_id = $options;
    }

    // все потомки NULL --
    // это все записи
    if (is_null($_id)) {
      $res = $this->find('all', array('recursive'=>-1));
    } else {
      $res = $this->children($_id);
    }

    $retArr = $this->_toTree($res, $_id);
    return $retArr;
  }

}
 
private function &_toTree(&$arr, $parentId ) {
  $parentFieldName = $this->Behaviors->Tree->settings[$this->alias]['parent'];
  
  $retArr = array();
  
  if (!empty($arr)) {
  
  // Set::extract fix :-(
    if (is_null($parentId)) {
      foreach($arr as $item) {
        if (empty($item[$this->alias][$parentFieldName])) {
          $retArr[] = $item;
        }
      }
    } else {
      $retArr = Set::extract($arr,
        "/{$this->alias}[$parentFieldName=$parentId]");
    }

    for ($i=0; $i<count($retArr); $i++) {
      $retArr[$i]['children'] = $this->_toTree(&$arr,
        $retArr[$i][$this->alias][$this->primaryKey]);
    }
  }
  
  return $retArr;
}