Итак, в предыдущем топике (виден только подписчикам блога!) я ссылался на статью о Kohana, размещенную на сайте NetTuts+. Поскольку в описанном в ней приложении есть недостатки, предлагаю их найти и обезвредить.
Вас не смущает необходимость в каждом шаблоне создавать страницу с нуля (включая доктайп, стили и т.д.)? Так ведь недолго и на статику перейти. Поэтому первая мысль — реализовать вложенные шаблоны.
Во всех шаблонах есть некая общая часть, которую мы вынесем в отдельный файл и будем подключать в каждом методе контроллера. Создадим в папке views файл index.php:
Я вынес в отдельный файл шапку страниц, а также от себя добавил меню навигации (пока из одного пункта, но позже мы его расширим). Обратите внимание на переменную $content, которая будет содержать результаты работы контроллера Album_Controller. Добавим следующие строчки в файл style.css (правила для меню навигации):
Далее вырежем все лишнее из имеющихся шаблонов. Покажу конечный результат на примере файла list.php:
Как видите, теперь в шаблоне осталась только информация, непосредственно связанная с выводом списка альбомов. Однако недостаточно изменить только шаблоны, придется залезть и в контроллер Album_Controller (показываю изменения в методе show_albums_list()):
Во-первых, мы сделали наследование от Template_Controller. Т.е. теперь в каждом контроллере появится свойство $this->template, которое будет содержать базовый шаблон (пусть вас не смущает строковое значение, в контроллере переменная $template станет объектом класса View). Кроме того, по умолчанию этот базовый шаблон будет автоматически отображен, т.е. вызов $this->template->render(TRUE) в конце каждого метода уже не нужен.
Во-вторых, в каждый шаблон можно вкладывать сколько угодно других шаблонов (да и вообще других объектов). Так, в приведенном выше примере мы установили в свойстве $this->template переменную 'content', содержащую шаблон list.php, а него в свою очередь передали переменную 'albums_list'. Таким образом, нам уже не надо хранить переменные типа $list_view (удаляем их из контроллера).
В принципе, ничего не поменялось, но теперь, если нам потребует изменить список css-файлов или пункты в меню навигации, мы будем править только один файл (index.php).
Конечно, localhost/kohana/index.php/album как-то не смотрится. Поскольку все прогрессивное человечество давно использует .htaccess, а на оф. сайте есть информация о его настройке, внесем небольшую коррективу в файл config/config.php:
Теперь имя фронт-контроллера (index.php) в URL можно опускать. Дополнительно настроим дефолтный роутинг на наш контроллер Album_Controller:
Теперь localhost/kohana будет вести туда же, куда localhost/kohana/index.php/album.
Рассмотрим метод Album_Model->read($id), который должен возвращать альбом с идентификатором $id:
Я вот не понимаю, зачем делать выборку массива записей из БД, если ожидается только один альбом? Переделываем метод:
Поскольку нас интересует только одна запись, используем метод current() для получения первой строки. Если записей не найдено, вернется FALSE. Соответственно в show_update_editor() тоже будут внесены изменения (например, вместо сохранения полей альбом по отдельности можно просто сохранить всю запись) и получится что-то вроде этого:
В начало метода добавлена проверка на существование редактируемого альбома, и если он не найдет, генерируется системное сообщение 'system.404', приводящее к соответствующей ошибке. Вся информация о найденном альбоме сохраняется в шаблон в виде переменной $album, так что внесем изменения и в файл views/update.php:
Все производимые изменения были по большей мере косметическими. Однако пришло время показать одно из наиболее популярных в Kohana орудий — библиотеку ORM. Зачем вручную писать различные методы выборки из БД, если есть готовая библиотека с многочисленными возможностями?
Давайте-ка приведем модели Album_Model и Genre_Model к ORM-виду:
Все! Больше (пока что) для наших нужд ничего не надо. Но самое интересное — как мы будем ORM использовать в контроллере. Посмотрим на примере отображения списка альбомов:
Запись ORM::factory('album')->with('genre')->find_all() можно перевести как «выбрать все альбомы вместе с информацией о жанре», т.е. автоматически будет сгенерирован join между таблицами albums и genres. В результате будет возвращен объект ORM_Iterator, но мы просто будем работать с ним как с массивом. ;)
Как вы помните, для получения списка жанров в контроллере Album_Controller был создан метод get_genres_list():
Взмахнем волшебной палочкой и метод превратится в одну строчку:
Напоследок — самое вкусное. Методы create() и update() не выполняют никаких проверок, а просто пытаются подсунуть модели Album_Model введенные данные. А ведь в Kohana есть замечательная библиотека Validation, которую очень удобно использовать вместе с ORM!
Для начала добавим-таки несколько строчек в модель Album_Model:
Это — метод validate(), отвечающий собственно за проверку введенных данных с помощью библиотеки Validation, и нестандартное правило (точнее даже callback), отвечающее за уникальность сочетания Альбом + Исполнитель. Использовать их очень просто, рассмотрим модифицированный метод create():
Мы последовательно проверили наличие данных в $_POST, загрузили интересующие нас поля в массив $data (для этого используется метод overwrite() хэлпера arr) и отправили его на проверку в метод validate(). Второй параметр, установленный в TRUE, означает, что в случае успешной проверки ORM-объект будет сохранен. Если же произошла ошибка валидации, сохраняем в сессии описание ошибок и текущие значения из формы (никто ведь не любит заново вбивать многочисленные поля форм из-за малюсенькой опечатки), и редиректим обратно в редактор. Соответственно надо внести изменения в метод show_create_editor() и его шаблон (create.php):
Большая часть изменений в методе — попытка получить данные из сессии и подстановка их в шаблон для показа пользователю. Ошибки выводятся в самом начале формы, но никто не мешает выводить их перед соответствующими полями ввода. Аналогичные изменения необходимо внести в методы update(), show_update_redactor() и шаблон update.php.
Обратили внимание на метод $data->errors('album') в строке №17 метода create()? Немного странно для массива, правда? Дело в том, что после того, как массив $data был передан в метод validate(), он превращается в объект Validation. А уже в нем есть метод errors(), который возвращает ошибки валидации. Если передать в него имя i18n-файла, то ошибки будут переведены с помощью указанного файла. Давайте сделаем перевод возможных ошибок:
Я сделал два файла — на русском и на английском, текстовые ресурсы сгруппированы по именам полей формы. Угадайте, какой файл будет использован по умолчанию в нашем приложении? Конечно, английский. Скопируйте из папки system/config файл locale.php и подправьте значения для русского языка:
Вроде бы все основные моменты я рассказал. Попробуйте внести изменения в проект NetTuts'а, если что — можете подсмотреть в получившийся у меня архив. Конечно, не хватает множества различных мелочей (сортировки, выборка по определенным артистам или жанрам), но в рамках данной статьи это нереально. Если есть желание самостоятельно поработать, можете также вынести артистов в отдельную таблицу, это потребует определенных усилий по изменению контроллеров и шаблонов.
1. Оптимизируем работу с шаблонами (VIEWS)
Вас не смущает необходимость в каждом шаблоне создавать страницу с нуля (включая доктайп, стили и т.д.)? Так ведь недолго и на статику перейти. Поэтому первая мысль — реализовать вложенные шаблоны.
Во всех шаблонах есть некая общая часть, которую мы вынесем в отдельный файл и будем подключать в каждом методе контроллера. Создадим в папке views файл index.php:
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html>
- <head>
- <?php
- echo html::stylesheet(array
- (
- 'assets/css/style'
- ),
- array
- (
- 'screen'
- ), FALSE);
- ?>
- <title>CD COLLECTION</title>
- </head>
- <body>
- <!-- BEGIN NAVIGATION MENU -->
- <ul id="navigation">
- <li><?=html::anchor('album', 'Albums')?></li>
- </ul>
- <!-- END NAVIGATION MENU -->
- <?=$content?>
- </body>
- </html>
* This source code was highlighted with Source Code Highlighter.
Я вынес в отдельный файл шапку страниц, а также от себя добавил меню навигации (пока из одного пункта, но позже мы его расширим). Обратите внимание на переменную $content, которая будет содержать результаты работы контроллера Album_Controller. Добавим следующие строчки в файл style.css (правила для меню навигации):
- #navigation {
- overflow: hidden;
- list-style-type: none;
- }
-
- #navigation li {
- float: left;
- margin-left: 0.5em;
- border: 1px solid black;
- }
-
- #navigation li a {
- font-size: 14px;
- display: block;
- padding: 2px 5px;
- text-decoration: none;
- color: #fff;
- background: #666;
- }
* This source code was highlighted with Source Code Highlighter.
Далее вырежем все лишнее из имеющихся шаблонов. Покажу конечный результат на примере файла list.php:
- <?php defined('SYSPATH') or die('No direct script access.');
- echo html::image('assets/images/add.png');
- echo html::anchor('album/show_create_editor', 'Add new album');
- ?>
- <table class="list" cellspacing="0">
- <tr>
- <td colspan="5" class="list_title">CD Collection</td>
- </tr>
- <tr>
- <td class="headers">Album name</td>
- <td class="headers">Author</td>
- <td colspan='3' class="headers">Genre</td>
-
- </tr>
- <?php
- foreach($albums_list as $item)
- {
- echo "<tr>";
- echo "<td class='item'>".$item->name."</td>";
- echo "<td class='item'>".$item->author."</td>";
- echo "<td class='item'>".$item->genre->name."</td>";
- echo "<td class='item'>".html::anchor('album/delete/'.$item->id,html::image('assets/images/delete.png'))."</td>";
- echo "<td class='item'>".html::anchor('album/show_update_editor/'.$item->id,html::image('assets/images/edit.png'))."</td>";
- echo "</tr>";
- }
- ?>
- </table>
-
* This source code was highlighted with Source Code Highlighter.
Как видите, теперь в шаблоне осталась только информация, непосредственно связанная с выводом списка альбомов. Однако недостаточно изменить только шаблоны, придется залезть и в контроллер Album_Controller (показываю изменения в методе show_albums_list()):
- // теперь наш контроллер является потомком Template_Controller!
- class Album_Controller extends Template_Controller
- {
- // указываем базовый шаблон
- public $template = 'index';
-
- ...
- private function show_albums_list()
- {
- $albums_list = $this->album_model->get_list();
- $this->template->content = View::factory('list')
- ->set('albums_list', $albums_list);
- }
- ...
* This source code was highlighted with Source Code Highlighter.
Во-первых, мы сделали наследование от Template_Controller. Т.е. теперь в каждом контроллере появится свойство $this->template, которое будет содержать базовый шаблон (пусть вас не смущает строковое значение, в контроллере переменная $template станет объектом класса View). Кроме того, по умолчанию этот базовый шаблон будет автоматически отображен, т.е. вызов $this->template->render(TRUE) в конце каждого метода уже не нужен.
Во-вторых, в каждый шаблон можно вкладывать сколько угодно других шаблонов (да и вообще других объектов). Так, в приведенном выше примере мы установили в свойстве $this->template переменную 'content', содержащую шаблон list.php, а него в свою очередь передали переменную 'albums_list'. Таким образом, нам уже не надо хранить переменные типа $list_view (удаляем их из контроллера).
Для установки значения переменных в шаблоне можно использовать несколько способов. Метод set() удобен в случае последовательных манипуляций над объектом, в этом случае получается эдакая цепочка вызовов. Присвоение же значений напрямую ($this->template->content = ...) делает то же самое, но читается приятнее (ИМХО).
В принципе, ничего не поменялось, но теперь, если нам потребует изменить список css-файлов или пункты в меню навигации, мы будем править только один файл (index.php).
А вас не смущают URL приложения?
Конечно, localhost/kohana/index.php/album как-то не смотрится. Поскольку все прогрессивное человечество давно использует .htaccess, а на оф. сайте есть информация о его настройке, внесем небольшую коррективу в файл config/config.php:
- /**
- * Name of the front controller for this application. Default: index.php
- *
- * This can be removed by using URL rewriting.
- */
- $config['index_file'] = '';
* This source code was highlighted with Source Code Highlighter.
Теперь имя фронт-контроллера (index.php) в URL можно опускать. Дополнительно настроим дефолтный роутинг на наш контроллер Album_Controller:
// config/routes.php $config['_default'] = 'album';
Теперь localhost/kohana будет вести туда же, куда localhost/kohana/index.php/album.
Использование моделей автором
Рассмотрим метод Album_Model->read($id), который должен возвращать альбом с идентификатором $id:
- public function read($id)
- {
- $this->db->where('id', $id);
- $query = $this->db->get($this->album_table);
- return $query->result_array();
- }
* This source code was highlighted with Source Code Highlighter.
Я вот не понимаю, зачем делать выборку массива записей из БД, если ожидается только один альбом? Переделываем метод:
- public function read($id)
- {
- return $this->db
- ->where('id', $id)
- ->get($this->album_table)
- ->current();
- }
* This source code was highlighted with Source Code Highlighter.
Поскольку нас интересует только одна запись, используем метод current() для получения первой строки. Если записей не найдено, вернется FALSE. Соответственно в show_update_editor() тоже будут внесены изменения (например, вместо сохранения полей альбом по отдельности можно просто сохранить всю запись) и получится что-то вроде этого:
- public function show_update_editor($id)
- {
- $album_data = $this->album_model->read($id);
- if (FALSE === $album_data)
- Event::run('system.404');
- $this->template->content = View::factory('update')
- ->set('album', $album_data)
- ->set('genres_list', $this->get_genres_list());
- }
* This source code was highlighted with Source Code Highlighter.
В начало метода добавлена проверка на существование редактируемого альбома, и если он не найдет, генерируется системное сообщение 'system.404', приводящее к соответствующей ошибке. Вся информация о найденном альбоме сохраняется в шаблон в виде переменной $album, так что внесем изменения и в файл views/update.php:
- <?php echo form::open('album/update'); ?>
- <table class='editor'>
- <tr>
- <td colspan='2' class='editor_title'>Update album</td>
- </tr>
- <?php
- echo "<tr>";
- echo "<td>".form::label('name', 'Name: ')."</td>";
- echo "<td>".form::input('name', $album->name)."</td>";
- echo "</tr>";
-
- echo "<tr>";
- echo "<td>".form::label('author', 'Author: ')."</td>";
- echo "<td>".form::input('author', $album->author)."</td>";
- echo "<tr/>";
-
- echo "<tr>";
- echo "<td>".form::label('genre', 'Genre: ')."</td>";
- echo "<td>".form::dropdown('genre_id',$genres_list, $album->genre_id)."</td>";
- echo "<tr/>";
-
- echo "<tr>";
- echo "<td colspan='2' align='left'>".form::submit('submit', 'Update album')."</td>";
- echo "</tr>";
-
- ?>
- </table>
- <?php
- echo form::hidden('album_id',$album->id);
- echo form::close();
- ?>
* This source code was highlighted with Source Code Highlighter.
ORM на сцену!
Все производимые изменения были по большей мере косметическими. Однако пришло время показать одно из наиболее популярных в Kohana орудий — библиотеку ORM. Зачем вручную писать различные методы выборки из БД, если есть готовая библиотека с многочисленными возможностями?
Давайте-ка приведем модели Album_Model и Genre_Model к ORM-виду:
- class Album_Model extends ORM
- {
- protected $belongs_to = array('genre');
- }
-
- class Genre_Model extends ORM {
-
- protected $has_many = array('albums');
- }
* This source code was highlighted with Source Code Highlighter.
Все! Больше (пока что) для наших нужд ничего не надо. Но самое интересное — как мы будем ORM использовать в контроллере. Посмотрим на примере отображения списка альбомов:
- private function show_albums_list()
- {
- $albums_list = ORM::factory('album')->with('genre')->find_all();
- $this->template->content = View::factory('list')
- ->set('albums_list', $albums_list);
- }
* This source code was highlighted with Source Code Highlighter.
Запись ORM::factory('album')->with('genre')->find_all() можно перевести как «выбрать все альбомы вместе с информацией о жанре», т.е. автоматически будет сгенерирован join между таблицами albums и genres. В результате будет возвращен объект ORM_Iterator, но мы просто будем работать с ним как с массивом. ;)
На самом деле, если мы закомментируем вызов метода with(), все по прежнему будет работать! В ORM реализована «ленивая загрузка» (lazy loading), так что при попытке доступа к свойства $album->genre будет произведен запрос к БД. Просто лучше сделать один join, чем в цикле делать дополнительные выборки жанров. Если не хочется каждый раз вместе с альбомами вручную выбирать жанры, добавьте в Album_Model следующую строчку:
protected $load_with = array('genre');
После этого жанры будут подгружаться вместе с альбомами автоматически.
Формирование выпадающего списка жанров
Как вы помните, для получения списка жанров в контроллере Album_Controller был создан метод get_genres_list():
- private function get_genres_list()
- {
- $db_genres_list = $this->genre_model->get_list();
- $genres_list = array();
-
- if(sizeof($db_genres_list) >= 1)
- {
- foreach($db_genres_list as $item)
- {
- $genres_list[$item->id] = $item->name;
- }
- }
- return $genres_list;
- }
* This source code was highlighted with Source Code Highlighter.
Взмахнем волшебной палочкой и метод превратится в одну строчку:
- private function get_genres_list()
- {
- return ORM::factory('genre')->find_all()->select_list('id', 'name');
- }
* This source code was highlighted with Source Code Highlighter.
Создание и редактирование ORM-объектов
Напоследок — самое вкусное. Методы create() и update() не выполняют никаких проверок, а просто пытаются подсунуть модели Album_Model введенные данные. А ведь в Kohana есть замечательная библиотека Validation, которую очень удобно использовать вместе с ORM!
Для начала добавим-таки несколько строчек в модель Album_Model:
- public function Validate(array & $array, $save = FALSE)
- {
- $array = Validation::factory($array)
- ->pre_filter('trim')
- ->add_rules('name', 'required')
- ->add_rules('author', 'required')
- ->add_rules('genre_id', 'required')
- ->add_callbacks('name', array($this, '_album_available'));
- return parent::validate($array, $save);
- }
-
- public function _album_available(Validation $array, $field) {
- $result = (bool) ! $this->db
- ->where(array('name' => $array['name'], 'author' => $array['author'], 'id !=' => $this->id))
- ->count_records($this->table_name);
- if ( !$result) $array->add_error($field, 'album_exists');
- return $result;
- }
* This source code was highlighted with Source Code Highlighter.
Это — метод validate(), отвечающий собственно за проверку введенных данных с помощью библиотеки Validation, и нестандартное правило (точнее даже callback), отвечающее за уникальность сочетания Альбом + Исполнитель. Использовать их очень просто, рассмотрим модифицированный метод create():
- public function create()
- {
- if ($this->input->post())
- {
- $data = array
- (
- 'name' => NULL,
- 'author' => NULL,
- 'genre_id' => NULL
- );
- $data = arr::overwrite($data, $this->input->post());
- $album = ORM::factory('album');
- if ($album->validate($data, TRUE)) {
- url::redirect('album');
- }
- else {
- Session::instance()->set('errors', $data->errors('album'));
- Session::instance()->set('data', $data->as_array());
- }
- }
- url::redirect('album/show_create_editor');
- }
* This source code was highlighted with Source Code Highlighter.
Мы последовательно проверили наличие данных в $_POST, загрузили интересующие нас поля в массив $data (для этого используется метод overwrite() хэлпера arr) и отправили его на проверку в метод validate(). Второй параметр, установленный в TRUE, означает, что в случае успешной проверки ORM-объект будет сохранен. Если же произошла ошибка валидации, сохраняем в сессии описание ошибок и текущие значения из формы (никто ведь не любит заново вбивать многочисленные поля форм из-за малюсенькой опечатки), и редиректим обратно в редактор. Соответственно надо внести изменения в метод show_create_editor() и его шаблон (create.php):
- public function show_create_editor()
- {
- $errors = Session::instance()->get_once('errors', array());
- $data = array('name'=>'', 'author'=>'', 'genre_id'=>'');
- $data = arr::overwrite($data, Session::instance()->get_once('data', array()));
- $genres_list = $this->get_genres_list();
- $this->template->content = View::factory('create')
- ->set('genres_list', $genres_list)
- ->set('errors', $errors)
- ->set('data', $data);
- }
* This source code was highlighted with Source Code Highlighter.
- <?php echo form::open('album/create'); ?>
- <?php foreach($errors as $error)
- echo "<div class='error'>".$error."</div>";?>
- <table class='editor'>
- <tr>
- <td colspan='2' class='editor_title'>Create new album</td>
- </tr>
- <?php
- echo "<tr>";
- echo "<td>".form::label('name', 'Name: ')."</td>";
- echo "<td>".form::input('name', $data['name'])."</td>";
- echo "</tr>";
-
- echo "<tr>";
- echo "<td>".form::label('author', 'Author: ')."</td>";
- echo "<td>".form::input('author', $data['author'])."</td>";
- echo "<tr/>";
-
- echo "<tr>";
- echo "<td>".form::label('genre', 'Genre: ')."</td>";
- echo "<td>".form::dropdown('genre_id',$genres_list, $data['genre_id'])."</td>";
- echo "<tr/>";
-
- echo "<tr>";
- echo "<td colspan='2' align='left'>".form::submit('submit', 'Create album')."</td>";
- echo "</tr>";
- ?>
- </table>
- <?php echo form::close(); ?>
* This source code was highlighted with Source Code Highlighter.
Большая часть изменений в методе — попытка получить данные из сессии и подстановка их в шаблон для показа пользователю. Ошибки выводятся в самом начале формы, но никто не мешает выводить их перед соответствующими полями ввода. Аналогичные изменения необходимо внести в методы update(), show_update_redactor() и шаблон update.php.
Вывод ошибок валидации
Обратили внимание на метод $data->errors('album') в строке №17 метода create()? Немного странно для массива, правда? Дело в том, что после того, как массив $data был передан в метод validate(), он превращается в объект Validation. А уже в нем есть метод errors(), который возвращает ошибки валидации. Если передать в него имя i18n-файла, то ошибки будут переведены с помощью указанного файла. Давайте сделаем перевод возможных ошибок:
- // файл i18n/ru_RU/album.php
- <?php defined('SYSPATH') or die('No direct script access.');
-
- $lang = array
- (
- 'name' => array(
- 'required' => 'Название альбома не указано',
- 'album_exists' => 'Комбинация альбом + артист уже добавлена',
- ),
-
- 'author' => array
- (
- 'required' => 'Имя исполнителя не указано',
- ),
-
- 'genre_id' => array
- (
- 'required' => 'Не выбран жанр альбома',
- ),
- );
* This source code was highlighted with Source Code Highlighter.
- // файл i18n/en_US/album.php
- <?php defined('SYSPATH') or die('No direct script access.');
-
- $lang = array
- (
- 'name' => array(
- 'required' => 'Album name required',
- 'album_exists' => 'Album & artist combination already exists',
- ),
-
- 'author' => array
- (
- 'required' => 'Author name required',
- ),
-
- 'genre_id' => array
- (
- 'required' => 'Genre required',
- ),
- );
* This source code was highlighted with Source Code Highlighter.
Я сделал два файла — на русском и на английском, текстовые ресурсы сгруппированы по именам полей формы. Угадайте, какой файл будет использован по умолчанию в нашем приложении? Конечно, английский. Скопируйте из папки system/config файл locale.php и подправьте значения для русского языка:
- <?php defined('SYSPATH') OR die('No direct access allowed.');
- /**
- * @package Core
- *
- * Default language locale name(s).
- * First item must be a valid i18n directory name, subsequent items are alternative locales
- * for OS's that don't support the first (e.g. Windows). The first valid locale in the array will be used.
- * @see php.net/setlocale
- */
- $config['language'] = array('ru_RU', 'Russian_Russia');
-
- /**
- * Locale timezone. Defaults to use the server timezone.
- * @see php.net/timezones
- */
- $config['timezone'] = '';
-
* This source code was highlighted with Source Code Highlighter.
Итоги
Вроде бы все основные моменты я рассказал. Попробуйте внести изменения в проект NetTuts'а, если что — можете подсмотреть в получившийся у меня архив. Конечно, не хватает множества различных мелочей (сортировки, выборка по определенным артистам или жанрам), но в рамках данной статьи это нереально. Если есть желание самостоятельно поработать, можете также вынести артистов в отдельную таблицу, это потребует определенных усилий по изменению контроллеров и шаблонов.