WEB Рукоделие

6 ноября 2007 г.

Модели в CakePHP

Беда какая-то с реализацией моделей в Cake. Не то, чтобы совсем уж плохо, или не то, чтобы их совсем нет, как в Zend Framework, но реализация пока хромает.

Особенно с рекурсией туго. Т.е. если 1 уровень рекурсии, то все хорошо, но если нужно 2 уровня, то уже плохо.

Например, есть у нас модель "Лекарство", связанная с моделями "Производитель" и "Цена", а "Цена" связана с моделью "Валюта". Связи примерно такие:

  • Лекарство belongsTo Производитель
  • Производитель hasMany Лекарство
  • Лекарство hasOne Цена
  • Цена belongsTo Валюта

Обратная связь от Производителя — чтоб можно было получить все продукты, ему принадлежащие.

Однако при попытке получить данные о лекарстве получается полная фигня — либо, при recursive=1 не получаем Валюту, либо при recursive=2 получаем на втором уровне все продукты от Производитель.

Для этого есть несколько решений. как для версии 1.1, так и 1.2.

Чаще всего предлагается сделать unbindModel для ненужных связей, в данном случае $this->Лекарство->Производитель->unbindModel(array(hasMany=>array('Лекарство'))). У этого способа есть неприятная особенность, при добавлении новых связей к другим моделям, придется снова залезать в код контроллера и убирать "лишние" связи.

Обратный способ — создавать связи "на лету". Т.е. перед использованием вызывать bindModel, а в описании модели эти связи не хранить. Этот способ лучше, но более трудоемкий и тоже неправильный. Изначально модель должна все-таки "знать" о своих связях :-)

Для версии 1.1, в свое время предлагалось расширить функционал базового класса Model (AppModel) методом expects, в котором можно было бы указывать только те модели, которые нужны для запроса. Код этого метода можно найти либо в Bakery, либо в гугловской группе. Expects, наверняка должен работать и в 1.2, но я не пробовал. Потому, что появился еще один метод.

В 1.2 народ начал активно использовать различные Model behaviors, и ThinkingPHP предложил свой behavior для нашей проблемы. По-моему это на сегодняшний день наилучший вариант. Использование Containable behaivior сильно упрощает жизнь и на сегодняшний день это самый верный Cake way.

Теперь в AppModel надо добавить строчку:


class AppModel extends Model {
 var $actsAs = array('Containable');
}

После этого, перед вызовом метода findAll или любого другого, достаточно указать какие модели или даже отдельные столбцы моделей нужно использовать. Containable behavior сам вызовет нужные методы unbind для тех моделей, которые не пригодились.

Вот например вот так можно перечислить модели, связь с которыми нужна для модели User:


$this->User->contain(
 'UserType',
 'Group.GroupPermission',
 'Post.Category',
 'Post.RelatedPost',
 'Post.Comment.User');

Или даже указать отдельные столбцы (заодно обратите внимание на различные варианты возможного синтаксиса):


$this->User->contain(
 array('Group' =>
  array('fields' => array('id'))));

$this->User->contain(
 array('Group.fields' => array('id'))); 

$this->User->contain(array( 'Group' => array('id'))); $this->User->contain('Group.fields.id');

Все это отлично, если бы не одно "но". Эта замечательная штука не работает с pagination. Дело в том, что pagination выполняет 2 запроса — первый COUNT(*) для подсчета общего числа строк, второй — собственно выборка. А unbindModel, который вызывается из contain работает только на 1 запрос, если ем явно не указать обратного, передав значение false в качестве второго параметра. Решение этой проблемы предложено в комментариях к посту с behaviour. В код behaviour надо добавить свойство, прям в сам класс:


var $reset=true;

также добавить метод для установления этого свойства:


function setContainReset($reset) {
 $this->reset = $reset;

}

а строчку с вызовом unbindModel:


$assocModel->unbindModel($unbind);

поменять на:


$assocModel->unbindModel($unbind, $this->reset);

Voila, теперь перед вызовом $this-Model->contain(...) достаточно вызвать $this->Model->setContainReset(false) чтобы запретить восстанавливать связи после вызова COUNT в pagination.

Остается, однако, еще проблема большого числа запросов при $recursive > 1. Я ее коряво, но решил.