Вот и добрался я до того, что бы представить сознательной общественности еще одну свою наработку — модуль Kohana Nested Sets. Который, как вы наверное уже догадались, позволяет работать с деревьями в Kohana.
И так приступим … Я собственно говоря не буду ходить вокруг да около и приступлю сразу к его использованию, для этого рассмотрим использование модуля Kohana Nested Sets на простом и обыденном (как эта жизнь) примере который встречается весьма часто, а именно в процессе знакомства с модулем я вместе с вами создам простое приложение на Kohana 3.2 которое будет позволять управлять структурой категорий чего-либо.
1 — Установка и настройка Kohana
Сразу оговорюсь на данном шаге я для вас не открою новую вселенную поэтому я его пропущу …, если вы все таки не знаете как установить и настроить Kohana, то вы можете узнать это тут я же просто воспользуюсь настроенным фреймворком из моей предыдущей статьи о Kohana.
2 — Установка модуля Kohana Nested Sets
Для начала нужно скопировать файлы модуля в директорию модулей Kohana для этого вы можете скачать файлы модуля Kohana Nested Sets с github или воспользоваться git для его установки:
git clone https://github.com/snovichkov/kohana-nested-sets.git
Далее нам нужно зарегистрировать модуль в Kohana для этого в файле application/bootstrap.php настройте параметры вызова метода Kohana::modules примерно так:
Kohana::modules(array( // 'auth' => MODPATH.'auth', // Basic authentication // 'cache' => MODPATH.'cache', // Caching with multiple backends // 'codebench' => MODPATH.'codebench', // Benchmarking tool 'database' => MODPATH.'database', // Database access // 'image' => MODPATH.'image', // Image manipulation 'orm' => MODPATH.'orm', // Object Relationship Mapping // 'unittest' => MODPATH.'unittest', // Unit testing // 'userguide' => MODPATH.'userguide', // User guide and API documentation 'nested-sets' => MODPATH.'kohana-nested-sets', // Kohana Nested Sets Module ));
3 — Создание таблицы в БД
Предположим что наши категории будут иметь 2 параметра для редактирования это имя и описание категории, на основании чего создадим для хранения наших категорий таблицу в БД. SQL скрипт создания таблицы представлен ниже:
CREATE TABLE IF NOT EXISTS `categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, `lft` int(11) NOT NULL, `rgt` int(11) NOT NULL, `level` int(4) NOT NULL, `name` varchar(200) NOT NULL, `description` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
4 — Создание модели
Так как для работы с БД мы будем использовать Kohana ORM, то для начала работы нам нужно описать модель категории которая будет наследовать класс ORM_Nested_Sets, то что получилось у меня представлено ниже:
<?php defined('SYSPATH') or die('No direct script access.'); /** * Category entity class * * @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru> * @copyright Copyrights (c) 2012 Novichkov Sergey * * @property integer $id * @property integer $lft * @property integer $rgt * @property integer $level * @property string $name * @property string $description */ class Model_Category extends ORM_Nested_Sets{ /** * Use or not scope for multi root's tree * * @var bool */ protected $use_scope = FALSE; /** * Table columns * * Field name => Label * * @var array */ protected $_table_columns = array( 'id' => 'id', 'lft' => 'lft', 'rgt' => 'rgt', 'level' => 'level', 'name' => 'name', 'description' => 'description', ); } // End Model_Category
5 — Создание маршрутов
Для того что бы приступить к созданию контроллера нам нужно подготовить маршрут (файл application/bootstrap.php) который будет использовать наш контроллер, я за основу взял базовый маршрут изменив параметры по умолчанию для главной страницы приложения:
Route::set('default', '(<controller>(/<action>(/<id>)))') ->defaults(array( 'controller' => 'category', 'action' => 'view', ));
6 — Создание контроллера
Что бы не раздувать статью я определил следующие действия которые будет реализовывать наш контроллер:
- Вывод дерева списком
- Добавление / Изменение узла дерева
- Удаление узла из дерева
Вот то, что у меня получилось:
<?php defined('SYSPATH') or die('No direct script access.'); /** * Category controller class * * @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru> * @copyright Copyrights (c) 2012 Novichkov Sergey */ class Controller_Category extends Controller_Template { /** * @var View */ public $template = 'category/view'; /** * View all tree action */ public function action_view() { /** @var Model_Category $root **/ // check root category $root = ORM::factory('Category', array('lft' => 1, )); // if root not exist, create new root if (! $root->loaded()) { // create new $root->name = 'Root'; $root->description = 'Root node of categories tree'; $root->save(); // refresh root $root->reload(); } // set template variables $this->template->set(array( // all root node child's 'root' => $root, 'categories' => $root->get_descendants(), 'message' => Session::instance()->get_once('message'), )); } /** * Create node action */ public function action_new() { $this->action_modify(); } /** * Edit node action */ public function action_edit() { $this->action_modify(); } /** * Create or edit view action */ private function action_modify() { // change template $this->template = View::factory('category/modify'); /** @var Model_Category $root **/ // check root category $root = ORM::factory('Category', array('lft' => 1, )); // check root if ( ! $root->loaded()) { throw new HTTP_Exception_502('Root node of categories tree not founded'); } /** @var Model_Category $node **/ // node $node = ORM::factory('Category', $this->request->param('id')); // set template variables $this->template->set(array( // all root node child's 'root' => $root, 'node' => $node, 'categories' => $root->get_descendants(), 'message' => Session::instance()->get_once('message'), )); } /** * Create, Update tree node action */ public function action_save() { /** @var Model_Category $root **/ // root node $root = ORM::factory('Category', Arr::get($this->request->post(), 'parent')); // check root if ( ! $root->loaded()) { throw new HTTP_Exception_502('Root node of categories tree not founded'); } /** @var Model_Category $node **/ // create new node object $node = ORM::factory('Category', Arr::get($this->request->post(), 'id')); // bind data $node->values($this->request->post(), array('name', 'description')); // insert node as last child of root try { if (! $node->loaded()) { // insert $node->insert_as_last_child_of($root); } else { // save $node->save(); // change parent if needed if (! $root->is_equal_to($node->get_parent())) { $node->move_as_last_child_of($root); } } } catch (Exception $e) { throw $e; // process error check } // setup success message Session::instance()->set('message', 'Operation was successfully completed'); // redirect to modify page $this->request->redirect(Route::url('default', array('controller' => 'category', 'action' => 'edit', 'id' => $node->id))); } /** * Delete node action */ public function action_delete() { /** @var Model_Category $node **/ // node to delete $node = ORM::factory('Category', $this->request->param('id')); // check node if (! $node->loaded()) { // setup error message Session::instance()->set('message', 'Operation was failed'); // redirect to view tree page $this->request->redirect('/'); } // remove node $node->delete(); // setup success message Session::instance()->set('message', 'Operation was successfully completed'); // redirect to view tree page $this->request->redirect('/'); } } // End Controller Category
P.S: Сразу хотел бы оговориться, что при создании контроллера я не удилил достаточно внимания проверке ошибок, а так же валидации т.к. это не является целью данной статьи и я намеренно упустил все это, что бы не заграмождать код.
7 — Создание шаблонов
Как уже видно из кода контроллера я использовал два шаблона в проекте.
category/view.php — вывод дерева в виде таблицы и отображение ссылок для основных операций над деревом
<?php defined('SYSPATH') or die('No direct script access.'); /** * @var Model_Category $root * @var Boolean|Database_Result $categories * @var string $message * * @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru> * @copyright Copyrights (c) 2012 Novichkov Sergey */ ?><!DOCTYPE html> <html> <head> <title>Categories</title> <style type="text/css"> *{ margin: 0; padding: 0; } html, body{ width: 100%; height: 100%; } a:hover{ text-decoration: none; } #wrap{ margin: 0 auto; width: 960px; } #wrap h2{ margin: 20px 0; } #wrap h2 a{ float: right; } #wrap .message{ padding: 5px; border: 3px solid #00f; color: #00f; margin-bottom: 20px; } #wrap table{ width: 100%; border-collapse: collapse; } #wrap table th, td{ border: 1px solid #000; padding: 5px; vertical-align: top; } #wrap table th{ background: #000; color: #fff; } #wrap .actions{ white-space: nowrap; width: 1%; } </style> </head> <body> <div id="wrap"> <h2>Categories <a href="<?php echo Route::url('default', array('controller' => 'category', 'action' => 'new')) ?>" title="New">New</a></h2> <?php if ($message) : ?> <div class="message"><?php echo HTML::chars($message) ?></div> <?php endif; ?> <table> <tr> <th>Name</th> <th>Description</th> <th class="actions">Actions</th> </tr> <?php if ($categories AND $categories->count() > 0) : ?> <?php foreach($categories as $category) : /** @var Model_Category $category **/ ?> <tr> <td style="padding-left: <?php echo 5 + ($category->level - 1) * 10 ?>px"><?php echo HTML::chars($category->name) ?></td> <td><?php echo HTML::chars($category->description) ?></td> <td class="actions"> <a title="Edit" href="<?php echo Route::url('default', array('controller' => 'category', 'action' => 'edit', 'id' => $category->id)) ?>">Edit</a> | <a title="Delete" href="<?php echo Route::url('default', array('controller' => 'category', 'action' => 'delete', 'id' => $category->id)) ?>">Delete</a> </td> </tr> <?php endforeach; ?> <?php else : ?> <tr><td colspan="3">Tree of categories is empty</td></tr> <?php endif; ?> </table> </div> </body> </html>
category/modify.php — отображение формы для редактирования или создания узла
<?php defined('SYSPATH') or die('No direct script access.'); /** * @var Model_Category $node * @var Model_Category $root * @var Boolean|Database_Result $categories * @var string $message * * @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru> * @copyright Copyrights (c) 2012 Novichkov Sergey */ ?><!DOCTYPE html> <html> <head> <title>Categories</title> <style type="text/css"> *{ margin: 0; padding: 0; } html, body{ width: 100%; height: 100%; } a:hover{ text-decoration: none; } #wrap{ margin: 0 auto; width: 960px; } #wrap h2{ margin: 20px 0; } #wrap .message{ padding: 5px; border: 3px solid #00f; color: #00f; margin-bottom: 20px; } #wrap .row{ margin-bottom: 5px; } #wrap .row label{ display: block; margin-bottom: 5px; } #wrap .row input, #wrap .row textarea, #wrap .row select{ width: 100%; } #wrap .row textarea{ height: 100px; resize: vertical; } #wrap .controls{ text-align: right } </style> </head> <body> <div id="wrap"> <h2><?php echo $node->loaded() ? 'Edit' : 'New' ?> Category</h2> <?php if ($message) : ?> <div class="message"><?php echo HTML::chars($message) ?></div> <?php endif; ?> <form method="post" action="<?php echo Route::url('default', array('controller' => 'category', 'action' => 'save')) ?>"> <div class="row"> <?php $parent = $node->get_parent() ?> <label for="category_parent">Parent</label> <select name="parent" id="category_parent"> <option value="<?php echo $root->id ?>">Root</option> <?php if ($categories AND $categories->count() > 0) : ?> <?php foreach ($categories as $category) : ?> <option<?php if ($category->id == $parent->id) : ?> selected="selected"<?php endif ?> value="<?php echo $category->id ?>"><?php echo str_repeat(' ', $category->level * 3), HTML::chars($category->name) ?></option> <?php endforeach; ?> <?php endif; ?> </select> </div> <div class="row"> <label for="category_name">Name</label> <input type="text" name="name" value="<?php echo HTML::chars($node->name) ?>" id="category_name"> </div> <div class="row"> <label for="category_description">Description</label> <textarea rows="10" cols="10" name="description" id="category_description"><?php echo HTML::chars($node->description) ?></textarea> </div> <div class="controls"> <a href="/" title="Back to list">Back to list</a> <input type="submit" value="<?php echo $node->loaded() ? 'Save' : 'Create' ?>"> <input type="hidden" name="id" value="<?php echo HTML::chars($node->id) ?>"> </div> </form> </div> </body> </html>
Ну вот и все мы на простом примере рассмотрели как использовать модуль Kohana Nested Sets, используйте его на здоровье :), а так же все свои предложения или пожелания оставляйте в комментариях к этому посту или на странице проекта на github.
I am afraid I’ve found bug. Your module does not work with transactions!
$db = Database::instance();
$db>begin();
try {
// I assumed $id was initited early in code and that $category_parent exists;
$category_parent = $category = ORM::factory(‘category’,$id);
$category = ORM::factory(‘category’)
//do something with category
$category->save()
$db->commit()
} catch (Exception $e) {
$db->rollback()
}
Even if $category->save() throws an Exception changes made to database are not rolled back.
You get this error because my module also uses a transaction. If you need more information please see methods: Kohana_ORM_Nested_Sets::save and Kohana_ORM_Nested_Sets::make_root.
Well, that’s reasonable… Thanks for nice module. Good job! :)
Хороший модуль. Только по стилю кода вы в кохановское соглашение не попали. А за перенос из Doctrine — отдельное мерси!!!
Рад, что вам пригодился модуль. Касательно соглашений Kohana я всегда готов к критике именно по этому код выложен на github, и исправить все недостатки вы можете сами и сделать pull request.