232 читателя, 13 постов
Администрация
Модераторы
Блог о PHP-фреймворке.

# в этой таблице мы будем хранить пользователей
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) unsigned NOT NULL auto_increment,
`username` varchar(32) NOT NULL default '',
`password` char(50) NOT NULL default '',
`email` varchar(127) NOT NULL default '',
`join` int(10) unsigned NOT NULL default '0',
`last_login` int(10) unsigned NOT NULL default '0',
`logins` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_username` (`username`),
UNIQUE KEY `uniq_email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;
INSERT INTO `users` (`id`, `username`, `password`, `email`, `join`, `last_login`, `logins`) VALUES (1, 'admin', '098f6bcd4621d373cade4e832627b4f6', 'example@example.com', 1215075372, 0, 0);
# тут у нас будут описания прав пользователей
CREATE TABLE IF NOT EXISTS `roles` (
`id` int(4) unsigned NOT NULL auto_increment,
`name` varchar(32) NOT NULL,
`description` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;
INSERT INTO `roles` (`id`, `name`, `description`) VALUES (1, 'login', 'Зарегистрированный');
INSERT INTO `roles` (`id`, `name`, `description`) VALUES (2, 'admin', 'Админ');
# эта таблица будет содержать привязку прав к пользователю
CREATE TABLE IF NOT EXISTS `roles_users` (
`user_id` int(10) unsigned NOT NULL,
`role_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `fk_role_id` (`role_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `roles_users` (`user_id`, `role_id`) VALUES (1, 2);
# a эта таблица будет использоваться модулем Auth для хранения залогиненых пользователей
DROP TABLE IF EXISTS `d_user_tokens`;
CREATE TABLE IF NOT EXISTS `user_tokens` (
`id` int(11) unsigned NOT NULL auto_increment,
`user_id` int(11) unsigned NOT NULL,
`user_agent` varchar(40) NOT NULL,
`token` varchar(32) NOT NULL,
`created` int(10) unsigned NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_token` (`token`),
KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(10) unsigned NOT NULL auto_increment,
`parent_id` int(10) unsigned NOT NULL default '0',
`name` varchar(150) character set utf8 NOT NULL,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=14;
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (1, 0, 'Недвижимость');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (2, 1, 'Квартиры');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (3, 1, 'Комнаты');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (4, 1, 'Дома, дачи');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (5, 0, 'Авто');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (6, 5, 'Легковые автомобили');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (7, 5, 'Запчасти, аксессуары');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (8, 5, 'Мотоциклы, мопеды');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (9, 0, 'Работа');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (10, 9, 'Админ персонал');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (11, 9, 'ИТ, интернет');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (12, 9, 'Работа на дому');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (13, 9, 'Другие сферы');
CREATE TABLE IF NOT EXISTS `items` (
`id` int(10) unsigned NOT NULL auto_increment,
`title` varchar(250) character set utf8 NOT NULL,
`category_id` int(10) unsigned NOT NULL default '0',
`content` text NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`datepub` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `title` (`title`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2;
INSERT INTO `items` (`id`, `title`, `category_id`, `content`, `user_id`, `datepub`) VALUES (1, 'Тестовое объявление', 2, 'Это просто объявление. Таких тут вскоре будет много', 1, 1229251545);class Category_Model extends ORM_Tree{
protected $children = 'categories';
protected $has_many = array('items');
}
class Item_Model extends ORM{
protected $has_one = array('user');
protected $belongs_to = array('category');
}Думаю, стоит прояснить, что названия моделей должны быть в единичном числе, т.е. если используется таблица items, то название модели должно быть Item_Model. Также дела обстоят и с объявлениями переменных модели, если предполагается множество объектов ($children, $has_many), то аргумент должен быть во множественном числе, если же объект один ($has_one, $belong_to), то и число — единичное.
Что такое ORM?
Думаю, что если Вы дочитали до сюда, то вопросов по MVC у Вас возникнуть не должно, а вот в отношении ORM они вполне могут появиться. Википедия нам говорит, что ORM — это технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». Что это значит? Это значит, что вызвав метод factory класса ORM с параметром, к примеру, 'table', мы получим объект, проекцию таблицы `tables` (о множественных и единичных числах я говорил немного выше), и место того, чтобы делать SQL запросы к базе через расширение, или какой-либо абстрактный класс, мы можем обращаться к методам и свойствам вызваного класса. Т.е. например, выборка записей с id равным 5 из базы произойдет не привычным $db->query('SELECT * FROM `table` WHERE (`id` = 5) '), а обращением к ORM проекции — ORM::factory('table')->where('id', 5)->find_all(). Но в отличии от простого абстрактного доступа, ORM предоставляет массу вкусностей, которые сложно понять, не попробовав. Так, если речь идет именно об id, то запись можно получить ещё проще — ORM::factory('table', 5). На примере наших вышеобозначенных моделей, мы можем получить имя категории в которой лежит объявление с id = 5 вызовом ORM::factory('item', 5)->category->name, а не писать кучу SQL запросов.
class Category_Controller extends Template_Controller {
public $template = 'index';
public function index() { // главная страница
$_result = '';
$_cats = array();
$categories = ORM::factory('category')->where('parent_id', 0)->find_all();
foreach($categories as $l) {
$_tmp = new View('category');
$_tmp->id = $l->id;
$_tmp->name = $l->name;
$_tmp->children = $l->children;
$_cats[] = $_tmp;
}
$_result = new View('category');
$_result->cats = $_cats;
$this->template->content = $_result;
}
public function view($params) { // прсмотр категории
$_result = '';
$categories = ORM::factory('category', $params)->children;
foreach($categories as $l) {
foreach ($l->items as $n) {
$_tmp = new View('item');
$_tmp->content = $n;
$_result.= $_tmp;
}
}
$this->template->content = $_result;
}
public function viewsub($params) { // просмотр под-категории
$_result = '';
$categories = ORM::factory('category', $params);
foreach($categories->items as $l) {
$_tmp = new View('item');
$_tmp->content = $l;
$_result.= $_tmp;
}
$this->template->content = $_result;
}
}class Items_Controller extends Template_Controller {
public $template = 'index';
public function index() { // редирект на главную, если просто вызван контроллер
url::redirect('/index');
}
public function view($arg) { // отображение статьи с id = $arg
$_item = ORM::factory('item', $arg);
$this->template->content = new View('item');
$this->template->content->content = $_item;
}
public function edit($arg) { // правка статьи c id = $arg
$_tmp = ORM::factory('item', $arg);
if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&
(Auth::instance()->get_user()->id == $_tmp->user_id))) { // продолжать только если пользователь автор или админ
$category = array();
// наполняем список категорий
foreach(ORM::factory('category')->where('parent_id', 0)->find_all() as $l) {
foreach($l->children as $n) {
$category[$l->name][$n->id] = $n->name;
}
}
// создадим класс для работы с формами
$form = new Forge(url::current());
$form->set_attr('method', 'post');
$form->input('title') // создаем тег <input id = 'title' />
->label('Заголовок') // делаем для него <label for='title'>Заголовок</label>
->rules('required|length[3,40]') // правила валидации - обязательное, от 3 до 40 символов
->value($_tmp->title); // и присваиваем ему значение из $_tmp->title
$form->textarea('addtext')
->label('Текст')
->rules('required')
->value($_tmp->content);
$form->dropdown('category')
->label('Категория')
->options($category)
->selected($_tmp->category_id);
$form->submit('Править');
// вот такая вот простая валидация формы
if ($form->validate()) {
// создаем и заполняем объект ORM
$new = ORM::factory('item', $arg);
$new->title = $form->inputs['title']->value;
$new->content = $form->inputs['addtext']->value;
$new->category_id = $form->inputs['category']->value;
// а после - сохраняем
$new->save();
url::redirect('/');
}
// отправка формы виду
$this->template->content = $form->render();
} else {
$this->template->content = "Вы не зарегистрированы";
}
}
public function add() { // добавление статьи
if(Auth::instance()->logged_in()) { // только для зарегистрированых пользователей
$category = array();
foreach(ORM::factory('category')->where('parent_id', 0)->find_all() as $l) {
foreach($l->children as $n) {
$category[$l->name][$n->id] = $n->name;
}
}
$form = new Forge(url::current());
$form->set_attr('method', 'post');
$form->input('title')
->label('Заголовок')
->rules('required|length[3,40]');
$form->textarea('addtext')
->label('Текст')
->rules('required');
$form->dropdown('category')
->label('Категория')
->options($category)
->selected(0);
$form->submit('Добавить');
if ($form->validate()) {
$new = ORM::factory('item');
$new->title = $form->inputs['title']->value;
$new->content = $form->inputs['addtext']->value;
$new->category_id = $form->inputs['category']->value;
// вот так инстанция класса Auth дает доступ к пользовательским данным
$new->user_id = Auth::instance()->get_user()->id;
$new->datepub = time();
$new->save();
url::redirect('/');
}
$this->template->content = $form->render();
} else {
$this->template->content = "Вы не зарегистрированы";
}
}
public function delete($arg) { // удаление статьи
$new = ORM::factory('item', $arg);
if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&
(Auth::instance()->get_user()->id == $new->user_id))) { // только автор или админ
$new->delete();
url::redirect('/');
} else {
$this->template->content = "Вы не зарегистрированы";
}
}
}class User_Controller extends Template_Controller {
public $template = 'index';
public function login() { // вход
if (Auth::instance()->logged_in()) { // если не зарегистрированы, то на главную
url::redirect('/');
} else { // создание формы
$form = new Forge;
$form->set_attr('method', 'post');
$form->input('username')
->label('Логин')
->rules('required|length[4,32]');
$form->password('password')
->label('Пароль')
->rules('required|length[4,40]');
$form->submit('Войти');
if ($form->validate()) {
$user = ORM::factory('user', $form->username->value);
// если вход успешен, то на главную
if (Auth::instance()->login($user, $form->password->value)) {
url::redirect('/');
} else {
// если нет, то вывести ошибку
$form->password->add_error('login_failed', 'Неверное имя пользователя, или пароль.');
}
}
}
$this->template->content = $form->render();
}
public function logout() { // выход
if(Auth::instance()->logged_in()) {
Auth::instance()->logout(TRUE);
}
url::redirect('/');
}
public function register() { // регистрация
if(Auth::instance()->logged_in()) {
url::redirect('/');
} else {
$form = new Forge(url::current(), 'Регистрация');
$form->set_attr('method', 'post');
$form->input('username')
->label('Логин')
// допускается только пароль из латиницы, с цифрами знаками '_' и '-' от 4 до 32 символов
->rules('required|length[4,32]|valid_alpha_dash');
$form->password('password')->label('Пароль');
$form->password('password2')
->label('Опять пароль')
->rules('required|length[6,40]|valid_alpha_dash')
// это поле должно совпадать с полем 'password'
->matches($form->password);
$form->input('email')
->label('E-Mail')
->rules('required|valid_email');
$form->submit('Регистрация');
if ($form->validate()) {
$user = ORM::factory('user', $form->username->value);
// проверяем, нет ли уже такого пользователя
if (!$user->username_exists($form->username->value)) {
$user->username = $form->username->value;
$user->password = $form->password->value;
$user->email = $form->email->value;
// добавляем пользователя и ставим для него права
if($user->save() && $user->add(ORM::factory('role', 'login'))) {
Auth::instance()->login($user, $form->password->value);
url::redirect('/');
}
}
}
}
$this->template->content = $form->render();
}
}<?php foreach($cats as $content): ?>
<div class="category">
<h3><a href="/category/<?php echo $content->id ?>"><?php echo $content->name ?></a>/h3>
<ul>
<?php foreach($content->children as $l): ?>
<li><a href="/subcategory/<?php echo $l->id ?>"><?php echo $l->name ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Kohana</title>
</head>
<body>
<div id="enter" style="float:right">
<?php if(Auth::instance()->logged_in()) { ?>
Привет, <b><?php echo Auth::instance()->get_user()->username ?></b> • <a href="/logout">Выход</a>
<?php } else { ?>
<a href="/login">Войти</a> • <a href="/register">Регистрация</a>
<?php } ?>
</div>
<div id='links'>
<a href="/">Главная</a>
<a href="/add">Добавить</a>
</div>
<div id='content'>
<?php echo $content ?>
</div>
</body>
</html><div class="item">
<?php if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&
Auth::instance()->get_user()->id == $content->user_id)) { ?>
<span style="float:right">
<a href="/items/edit/<?php echo $content->id ?>">Править</a>
<a href="/items/delete/<?php echo $content->id ?>">Удалить</a>
</span>
<?php } ?>
<h1><?php echo html::anchor('/' . $content->id, html::specialchars($content->title)) ?></h1>
<div class="other">Опубликовано <?php echo date("j-M-Y ", $content->datepub) ?>
пользователем <b><?php echo html::specialchars($content->>user->username) ?></b>
</div>
<div class="news">
<?php
echo text::auto_p($content->content);
?>
</div>
</div>$config['default']['connection'] = array(
'type' => 'mysql',
'user' => 'пользователь',
'pass' => 'пароль',
'host' => 'сервер',
'database' => 'имя базы',
);$config['modules'] = array
(
MODPATH.'auth',
MODPATH.'forge',
); $config['_default'] = 'category';
$config['category/([0-9]+)'] = 'category/view/$1';
$config['subcategory/([0-9]+)'] = 'category/viewsub/$1';
$config['add'] = 'items/add';
$config['items/([0-9]+)'] = 'items/view/$1';
$config['edit/([0-9]+)'] = 'items/edit/$1';
$config['delete/([0-9]+)'] = 'items/delete/$1';
$config['login'] = 'user/login';
$config['logout'] = 'user/logout';
$config['register'] = 'user/register';
$config['([0-9]+)'] = 'items/view/$1';Forge, как я уже говорил выше, является модулем для работы с формами. Не так давно (начиная с 2.2; текущая 2.3) он был исключен из стандартной поставки Kohana, и теперь доступен тут — http://kohanamodules.googlecode.com/svn/tags/2.2/forge/
Также хочу обратить внимание, что у меня возникли проблемы при валидации select в Forge. На багтрэкере Koahna нашел два тикета с этой проблемой, оба помечены, как решенные. Может у меня руки кривые, может у кого-то ещё, но я немного поправил код /modules/forge/libraries/Form_Dropdown.php и ровно на строчке 71 вставил вот такой вот код:
foreach($this->data['options'] as $l) {
if(is_array($l) && (array_key_exists($this->value, $l))) {
return $this->is_valid = TRUE;
}
}
На уникальную панацею не претендует, но для нашей двойной вложенности сойдет.
/application/config/config.php
/database.php
/routes.php
/controllers/category.php
/items.php
/user.php
/models/category.php
/item.php
/views/category.php
/index.php
/item.php
/modules/auth/
/forge/
/forge/libraries/Form_Dropdown.php (*)
комментарии (81)
Нужно продвигать сей чудесный фреймфорк. ато у нас он почему-то еще относительно не популярен.
А и спасибо за статью. Я бы сам тоже написал что-нить. Но все времени и сил не хватает.
Для справки: «Кохана» на украинском означает «Любимая», а вот «Коханка» — «Любовница»
Так, на всякий случай ;)
$categories = ORM::factory('category')->where('parent_id', 0)->find_all();foreach($categories as $l) {
...
foreach($l->children as $n) {
$_tmp->count+= ORM::factory('item')->where('category_id', $n->id)->count_all();
}
...
}
Каждое обращение к ORM это один запрос к базе?
Если да, то наверное было бы логичным сделать это всё одним запросом чем многими в цикле.
Хотя, при действительно большем кол-ве подкатегорий (у меня в исходном проекте их 105) запросов к базе будет не меньше, так-что думаю вставить в items колонку с главной категорией, которой принадлежит подкатегория. Если у кого-то есть лучшие решения — с удовольствием выслушаю =)
SELECT count(*) as count, category_id from items group by category_id
Происдел всю ночь, так и не придя к какому-либо выводу, видимо придется всё-таки добавить поле в исходный проект, если «с утра» ничего в голову не придет, а с примером — буду думать…
В принципе, сделал реализацию, сократив запросы до 32х, хотя тоже считаю что это слишком много, да и выполнено в таком грязном виде, что лучше думать, что её вообще нет.
Темболее есть все причины думать, что можно ограничится 12 запросами, при довольно изящной реализации, но пока не знаю точно как, будем работать =)
Тут ведь не в функционале суть, а в его реализации.
SELETC BLA...BLA FROM BLA WHERE CategoryId IN ('1','2','3')
С каждым днем всё больше убеждаюсь в неоправданности ORM и ActiveRecords.
Плюсик вам в карму, надеюсь что будете ещё писать о кохане :)
Пример показывает, что вы смогли разобраться в ней, но не все как вы.
По поводу сложности — к сожалению я с другими фреймворкими не сталкивался, а Kohana выбрал поддавшись лишь минутной слабости и, хочу заметить, ниразу не пожалел. А статьей как-раз и хочу помочь, как Вы сказали, не таким, как я =)
1. Названия таблиц без префиксов. Из в принципе нельзя использовать или тут только для примера?
2. А чего при создании таблиц где-то указано DEFAULT CHARSET=utf8; а где-то нет? Ну понятно, для примера, но всеже если этот пример выполнить в баде где по дефолту Win-1251 установлено, что тогда будет? Не съедут ли данные?
3. ORM_Tree работает с деревьями на основе parent_id? Только с ними? Просто не самый лучший способ хранить деревья.
Да, и во вьювах на мой взгляд конструкции
более понятны, нежели
но это мое личное мнение.
С DEFAULT CHARSET — виноват, сейчас исправлюсь.
По поводу ORM_Tree — нет, в описании модели класса можно объявить свойство protected $parent_key, которое будет содержать имя колонки с id родителя.
А шаблоны — не раз уже затрагивалась эта тема, но мне так удобнее, это просто пример =)
Вы, кажется, не поняли вопроса. Я спрашивал не про имя колонки, а про алгоритм хранения дерева. Тут используется алгоритм на основе хранения id родителя (parent_id), что вообще-то часто достаточно ресурсоемко. Кроме этого существуют другие, менее ресурсоемкие алгоритмы (Depth-Level, Nested Set). Вопрос — поддерживае ли их ORM_Tree?
Хотя, существует переписаный вариант библиотеки от CI MPTT для Kohana, но я с ним дела, к сожалению, не имел.
Единственный плюс вижу в том, что бы в одной базе хранить данные нескольких проектов, но по моему уже 90% хостеров даже по самым дешевым тарифам дают 3-5 бд.
А минусом является то, что надо не забыть его подставлять в запросы.
Вы действительно настолько уверенны в PHP и konaha, что готовы переложить на него полностью всю заботу о синхронизации данных в БД? Не страшно?
Старый друг — лучше новых двух… Меня, как программиста в универе учили лени, т.е. изначально изучать старое, и годами проверенное, чем искать (и не дай Бог) изобретать что-либо новое…
А если серьезно, то я не знаю, как ещё передать всего три свойства одного класса другому. Если подскажете, буду весьма признателен.
Дело в том, что я ещё не шибко хорошо пишу, а т.к. про этот фреймворк мало кто знает, и ещё меньше про него пишут, пришлось руководствоваться принципом «кто, если не я», Вы уж простите.
Надеюсь в далнейшем инициативу перехавтят авторы с гораздо большим опытом и способностями к его изложению.
дело в том что сейчас я пишу первый, но относительно сложный проект на CI и возникают всякие вопросы, например — где проводить валидацию данных, в модели или контроллере, как можно бы сделать view-обертку для других view и т.п. хотелось бы глянуть как это все принято правильно делать.
Насчет view-обертки — в CI нет встроенных средств а-ля Layout в Zend Framework. Лично я использую библиотеку «Template»: официальная страничка и ветка на форуме CI.
валидация данных должна проводится в модели тчк
И первое впечатление такое – для доски объявлений слишком много кода. Имхо надо больше гибкости. И меньше кода. Но если большая часть этого кода будет реиспользоваться в других частях приложения (не копипаст а именно использоваться), тогда я возьму слово назад =).
Я хочу понять зависимость кол-ва запросов от объема данных…
Сие предложение ввело меня в ступор, дальнейшие объяснения понять не смог.
Модуль Auth уже имеет в своем арсенале необхдимые для него модели и требует несколько таблиц от базы для хранения своих данных. Далее я предлагаю создать модели, необходимые для хранения структуры категорий и объявлений.
Русская языка… Может так будет понятнее:
«У Маши в корзине лежит пять яблок, для того чтобы кушать, купила еще — зашла в магазин и заплатила.»
Вопросы:
Пять яблок для того чтобы кушать?
или
Для того чтобы кушать, купила еще?
Ы?
В случае наличия у Вас более подходящих вариантов для её изложения, я с радостью заменю исходный на Ваш, при целесообразности сиих манипуляций.
Не могу найти место в коде, где отсекаются запросы на добавление объявления с указанием category_id скажем 1.
Может быть только в реализации Админки сайта.
ORM в своей работе, обычно, ОЧЕНЬ активно посылает SQL-запросы, и даже для простых задач число запросов может исчисляться десятками… Для меня это много…
Может я старомоден ) но если при генерации страницы мой Движек делает более 7-10 запросов, то я начинаю нервничать и искать где что можно оптимизировать или закешировать…
в случае использования ORM такая гибкость исчезает, к сожалению…
Я ещё не сильно разобрался, где и что лучше вызывать, но на основе комментариев для себя уже выделил несколько важных моментов, которые в дальнейшем планирую использовать и учитывать.
Зачем в каждом классе-наследнике Template_Controller переопределяется поле template? Если это делается для определение дефолтового значения переменной в классе-наследнике — то по канонам ооп это должно делаться в конструкторе класса. Собственно, я поэтому вместо Template_Controller написал свой базовый абстрактный класс, который при рендеринге переменной проверяет инициализацию этой по сути абстрактной переменной.