
Вот и добрался я до того, что бы представить сознательной общественности еще одну свою наработку — модуль 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.