Продовжую опис зручних поведінок, які я знайшов в пекарні. Сьогодні ми поговоримо з вами про контроль ревізій. Контроль ревізій для моделей дуже схожий на контроль ревізій який ви, можливо, бачили в CVS або Wiki. Саме для останньої я хотів написати схожу поведінку (навіть хотів назвати Wikkable), але тепер я збираюсь додавати її до усіх своїх проектів, для того, щоб користувачі не псували собі записи.
Автор
Відразу зазначу, що цю поведінку написав пан alkemann. Знайти останню версію можна на Google Code, а для тих, хто полюбляє найостанніші версії, прошу в SVN.
Про поведінку
Можливості:
- Легко встановити
- Автоматично зберігає версії підчас збереження моделі
- Може ігнорувати зміни в окремих полях моделі
- Можна обмежувати кількість збережених версій
- Можливість відкатитися до будь-якої версії
- Можливість відкатитися до будь-якої дати
- Можливість порівняти різні версії
- Передивлятися одну чи усі версії моделі
Інсталяція
- Покласти нову версію RevisionBehavior в директорію app/models/behaviors
- Додати поведінку до AppModel (чи до якоїсь окремої моделі)
- Для кожної моделі, для якої потрібна поведінка створити тіньову таблицю
- Поведінка не буде працювати для моделей, що не мають тіньової таблиці
- Якщо ви додаєте поведінку для існуючого проекту,виконайте метод initializeRevisions() для кожної моделі.
Створення тіньових таблиць
Треба створювати тіньові таблиці після створення звичайних. Вони повинні мати назву [ім?я таблиці]_rev. Якщо вам таке закінчення не подобається, можете його змінити в змінній $revision_prefix в середені повединки. Також, поведінка очікує, що таблиці знаходяться в середені одної й той же конфігурації бази данних, але можного це змінити для кожної моделі, через опцію useDbConfig.
Треба створити таку ж саму таблиці, за наступини винятками:
- Поле ‘id’ не повинно бути первинним ключем (primary key) та\або автоінкрементним полем.
- Додайте поле ‘version_id’ (int, primary key, autoincrement) та ‘version_created’ (datetime).
- Не додавайте поля, які ви не збираєтесь зберігати в тіньової таблиці.
Конфігурація
Коли додаєте ‘Revision’ в масиві actsAs до моделі, можна зазначити наступні опції:
- limit : кількість версій, які треба зберигати. Мінімум 2 (по замовчуванню 1).
- ignore : массив, де ви можете вказати, які поля ігнорувати
- auto : boolean. Коли його значення дорівнює false, то версії не зберігатимуться кожного разу після збереження моделі.
- useDbConfig : string/null Ім?я конфіругації бази данних для зберігання. Null — для використання конфігурації моделі.
Приклад
Я вважаю, що краще наведу тут гарний приклад, з детальним описом, а API вже можна буде знайти на сайті і почитати. Тому, ось він, приклад з реального проекту Культор, про який я вам колись розповім.
Таблиця з постами для блогів:
CREATE TABLE `users_posts` ( `id` int(11) NOT NULL auto_increment, `item_id` int(11) NOT NULL, `title` varchar(255) default NULL, `body` longtext, `date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, `user_id` varchar(36) default NULL, PRIMARY KEY (`id`), KEY `user_id` (`user_id`), ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; |
А ось тіньова таблиця:
CREATE TABLE `users_posts_revs` ( `version_id` int(11) NOT NULL auto_increment, `version_created` datetime default NULL, `id` int(11) NOT NULL, `item_id` int(11) NOT NULL, `title` varchar(255) character set cp1251 NOT NULL, `body` longtext character set cp1251 NOT NULL, `date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, `user_id` varchar(36) default NULL, PRIMARY KEY (`version_id`) ) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; |
Як ви бачите, вона повторює таблицю users_posts, але має два додаткових поля version_id та version_created. Як нескладно здогадатися, в version_created зберігається дата, коли версію було створено.
Тепер додаєм поведінку в модель:
var $actsAs = array('Revision' => array('limit'=>20)); |
Обмежуємо кількість збережених версій до 20.
Тепер контролер:
function view($id) { $this->UsersPost->id = $id; $usersPost = $this->UsersPost->read(); $undo_rev = $this->UsersPost->previous(); $this->set('undo_rev',$undo_rev); $history = $this->UsersPost->revisions(); $users = $this->UsersPost->User->find('list'); $this->set(compact('usersPost','undo_rev','history','users')); } |
Метод View. Окрім звичайного читання, ми отримуємо останню ревізію (для кнопки Undo), і усі інші версії щоб ($this->UsersPost->revisions();) користувач зміг побачити хто і що змінював.
І виводимо в вид (view.ctp):
nice($undo_rev['UsersPost']['version_created']).' '; echo $html->link('Відмінити', array('action'=>'undo',$usersPost['UsersPost']['id'])); echo ' <h4>Історія ревізій</h4> <ul>'; $nr_of_revs = sizeof($history); foreach ($history as $k => $rev) { echo ' <li>'.($nr_of_revs-$k).' '.$rev['UsersPost']['version_created'].' '. $html->link('Зробити поточною', array('action'=>'make_current',$usersPost['UsersPost']['id'],$rev['UsersPost']['version_id'])); } ?></li> </ul> |
Як можна побачити, тут ми виводимо останню ревізію, а після неї – список усіх можливих версій.
Також, використовуємо два додаткові методи – undo та make_current. Ось їх код:
function undo($id) { $this->UsersPost->id = $id; $this->UsersPost->undo(); $this->redirect(array('action'=>'view',$id)); } function make_current($id, $version_id) { $this->UsersPost->id = $id; $this->UsersPost->revertTo($version_id); $this->redirect(array('action'=>'view',$id)); } |
Нічого страшного тут немає. Використовуємо методи поведінки undo(), та revertTo(номер версії).
Ось і все. Покажу вам ще одну цікаву річ. Завантажимо окрему ревізію підчас рідагування запису.
Додамо в вид:
echo ' <h4>Історія Ревізій</h4> <ul>'; $nr_of_revs = sizeof($history); foreach ($history as $k => $rev) { echo ' <li>'.($nr_of_revs-$k).' '.$rev['UsersPost']['version_created'].' '. $html->link('Завантажити', array('action'=>'edit',$rev['UsersPost']['id'],$rev['UsersPost']['version_id'])); } // [..] ?></li> </ul> |
І в контроллер:
function edit($id = null, $version_id = null) { $this->UsersPost->id = $id; if (!$id && empty($this->data)) { $this->Session->setFlash(__('Invalid UsersPost', true)); $this->redirect(array('action'=>'index')); } if (!empty($this->data)) { if ($this->UsersPost->save($this->data)) { $this->Session->setFlash(__('The UsersPost has been saved', true)); $this->redirect(array('action'=>'index')); } else { $this->Session->setFlash(__('The UsersPost could not be saved. Please, try again.', true)); } } if (empty($this->data)) { if (is_numeric($version_id)) { $this->data = $this->UsersPost->shadow('first',array('conditions' => array('version_id' => $version_id))); } else { $this->data = $this->UsersPost->read(); } } $users = $this->UsersPost->User->find('list'); $this->set(compact('users')); $history = $this->UsersPost->revisions(); $this->set(compact('users','history')); } |
Звичайна процедура редагування запису, але доданий ще один параметр – нумер ревізії. Тепер ми можемо завантажити ту, чи іншу ревізію,
відредагувати і зберегти в якості поточної версії.
$this->data = $this->UsersPost->shadow(’first’,array(’conditions’ => array(’version_id’ => $version_id)));
Тут ми завантажуємо стару ревізію з тіньової таблиці в стандартні данні форми.
Чистити тіньову таблицю не треба, бо вона чиститься автоматично.
Методи
Окрім того, можуть бути зручними наступні методи:
- diff($from_version_id = null, $to_version_id = null, $options = array()) – отримати різницю між версіями. В результаті матимемо наступне:
array( 'Post' => array( 'version_id' => array( 0 => 192, 1 => 67, 2 => 45), 'version_created => array( 0 => '2008-12-03 12:03:00', 1 => '2008-12-02 11:02:00', 2 => '2008-12-01 10:01:00', 'id' => 4, 'title' => array( 0 => 'New title', 1 => 'Edited title', 2 => 'Original title' ), 'content' => 'Lorem ipsum' ); |
- newest, oldest, previous – отримати новішу, старішу чи попередню версію.
- undelete() – відновлює видалений пост якщо він зберігся в таблиці.
$this->Post->id = 7; $this->Post->undelete(); |
- undo() – відкатитися до попередньої версії.
$this->Post->id = 2; $this->Post->undo();<strong> </strong> |
- shadow($type = ‘first’, $options = array()) – завантажити якусь версію.
Типи: ‘first’,'all’,'count
Опції: Опції передаються в find для тіньової таблиці.
- revertTo – відкатитися до версії.
- revertToDate($datetime, $cascade = false) – відкатитися до дати.
Якщо $cascade = true, то поведінка намагатиметься відкатити й пов?язані моделі.
Ось і все. Більше про методи можна прочитати в пекарні. Бажаю вам успіхів ;)

