Как стать автором
Обновить

Переделываем приложение CD Collection

Kohana
Итак, в предыдущем топике (виден только подписчикам блога!) я ссылался на статью о Kohana, размещенную на сайте NetTuts+. Поскольку в описанном в ней приложении есть недостатки, предлагаю их найти и обезвредить.

1. Оптимизируем работу с шаблонами (VIEWS)



Вас не смущает необходимость в каждом шаблоне создавать страницу с нуля (включая доктайп, стили и т.д.)? Так ведь недолго и на статику перейти. Поэтому первая мысль — реализовать вложенные шаблоны.

Во всех шаблонах есть некая общая часть, которую мы вынесем в отдельный файл и будем подключать в каждом методе контроллера. Создадим в папке views файл index.php:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2. <html>
  3. <head>
  4. <?php
  5. echo html::stylesheet(array
  6. (
  7. 'assets/css/style'
  8. ),
  9. array
  10. (
  11. 'screen'
  12. ), FALSE);
  13. ?>
  14. <title>CD COLLECTION</title>
  15. </head>
  16. <body>
  17. <!-- BEGIN NAVIGATION MENU -->
  18. <ul id="navigation">
  19. <li><?=html::anchor('album', 'Albums')?></li>
  20. </ul>
  21. <!-- END NAVIGATION MENU -->
  22. <?=$content?>
  23. </body>
  24. </html>
* This source code was highlighted with Source Code Highlighter.


Я вынес в отдельный файл шапку страниц, а также от себя добавил меню навигации (пока из одного пункта, но позже мы его расширим). Обратите внимание на переменную $content, которая будет содержать результаты работы контроллера Album_Controller. Добавим следующие строчки в файл style.css (правила для меню навигации):

  1. #navigation {
  2. overflow: hidden;
  3. list-style-type: none;
  4. }
  5. #navigation li {
  6. float: left;
  7. margin-left: 0.5em;
  8. border: 1px solid black;
  9. }
  10. #navigation li a {
  11. font-size: 14px;
  12. display: block;
  13. padding: 2px 5px;
  14. text-decoration: none;
  15. color: #fff;
  16. background: #666;
  17. }
* This source code was highlighted with Source Code Highlighter.


Далее вырежем все лишнее из имеющихся шаблонов. Покажу конечный результат на примере файла list.php:

  1. <?php defined('SYSPATH') or die('No direct script access.');
  2. echo html::image('assets/images/add.png');
  3. echo html::anchor('album/show_create_editor', 'Add new album');
  4. ?>
  5. <table class="list" cellspacing="0">
  6. <tr>
  7. <td colspan="5" class="list_title">CD Collection</td>
  8. </tr>
  9. <tr>
  10. <td class="headers">Album name</td>
  11. <td class="headers">Author</td>
  12. <td colspan='3' class="headers">Genre</td>
  13. </tr>
  14. <?php
  15. foreach($albums_list as $item)
  16. {
  17. echo "<tr>";
  18. echo "<td class='item'>".$item->name."</td>";
  19. echo "<td class='item'>".$item->author."</td>";
  20. echo "<td class='item'>".$item->genre->name."</td>";
  21. echo "<td class='item'>".html::anchor('album/delete/'.$item->id,html::image('assets/images/delete.png'))."</td>";
  22. echo "<td class='item'>".html::anchor('album/show_update_editor/'.$item->id,html::image('assets/images/edit.png'))."</td>";
  23. echo "</tr>";
  24. }
  25. ?>
  26. </table>
* This source code was highlighted with Source Code Highlighter.


Как видите, теперь в шаблоне осталась только информация, непосредственно связанная с выводом списка альбомов. Однако недостаточно изменить только шаблоны, придется залезть и в контроллер Album_Controller (показываю изменения в методе show_albums_list()):

  1. // теперь наш контроллер является потомком Template_Controller!
  2. class Album_Controller extends Template_Controller
  3. {
  4. // указываем базовый шаблон
  5. public $template = 'index';
  6. ...
  7. private function show_albums_list()
  8. {
  9. $albums_list = $this->album_model->get_list();
  10. $this->template->content = View::factory('list')
  11. ->set('albums_list', $albums_list);
  12. }
  13. ...
* 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:

  1. /**
  2. * Name of the front controller for this application. Default: index.php
  3. *
  4. * This can be removed by using URL rewriting.
  5. */
  6. $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:

  1. public function read($id)
  2. {
  3. $this->db->where('id', $id);
  4. $query = $this->db->get($this->album_table);
  5. return $query->result_array();
  6. }
* This source code was highlighted with Source Code Highlighter.


Я вот не понимаю, зачем делать выборку массива записей из БД, если ожидается только один альбом? Переделываем метод:

  1. public function read($id)
  2. {
  3. return $this->db
  4. ->where('id', $id)
  5. ->get($this->album_table)
  6. ->current();
  7. }
* This source code was highlighted with Source Code Highlighter.


Поскольку нас интересует только одна запись, используем метод current() для получения первой строки. Если записей не найдено, вернется FALSE. Соответственно в show_update_editor() тоже будут внесены изменения (например, вместо сохранения полей альбом по отдельности можно просто сохранить всю запись) и получится что-то вроде этого:

  1. public function show_update_editor($id)
  2. {
  3. $album_data = $this->album_model->read($id);
  4. if (FALSE === $album_data)
  5. Event::run('system.404');
  6. $this->template->content = View::factory('update')
  7. ->set('album', $album_data)
  8. ->set('genres_list', $this->get_genres_list());
  9. }
* This source code was highlighted with Source Code Highlighter.


В начало метода добавлена проверка на существование редактируемого альбома, и если он не найдет, генерируется системное сообщение 'system.404', приводящее к соответствующей ошибке. Вся информация о найденном альбоме сохраняется в шаблон в виде переменной $album, так что внесем изменения и в файл views/update.php:

  1. <?php echo form::open('album/update'); ?>
  2. <table class='editor'>
  3. <tr>
  4. <td colspan='2' class='editor_title'>Update album</td>
  5. </tr>
  6. <?php
  7. echo "<tr>";
  8. echo "<td>".form::label('name', 'Name: ')."</td>";
  9. echo "<td>".form::input('name', $album->name)."</td>";
  10. echo "</tr>";
  11. echo "<tr>";
  12. echo "<td>".form::label('author', 'Author: ')."</td>";
  13. echo "<td>".form::input('author', $album->author)."</td>";
  14. echo "<tr/>";
  15. echo "<tr>";
  16. echo "<td>".form::label('genre', 'Genre: ')."</td>";
  17. echo "<td>".form::dropdown('genre_id',$genres_list, $album->genre_id)."</td>";
  18. echo "<tr/>";
  19. echo "<tr>";
  20. echo "<td colspan='2' align='left'>".form::submit('submit', 'Update album')."</td>";
  21. echo "</tr>";
  22. ?>
  23. </table>
  24. <?php
  25. echo form::hidden('album_id',$album->id);
  26. echo form::close();
  27. ?>
* This source code was highlighted with Source Code Highlighter.


ORM на сцену!



Все производимые изменения были по большей мере косметическими. Однако пришло время показать одно из наиболее популярных в Kohana орудий — библиотеку ORM. Зачем вручную писать различные методы выборки из БД, если есть готовая библиотека с многочисленными возможностями?

Давайте-ка приведем модели Album_Model и Genre_Model к ORM-виду:

  1. class Album_Model extends ORM
  2. {
  3. protected $belongs_to = array('genre');
  4. }
  5. class Genre_Model extends ORM {
  6. protected $has_many = array('albums');
  7. }
* This source code was highlighted with Source Code Highlighter.


Все! Больше (пока что) для наших нужд ничего не надо. Но самое интересное — как мы будем ORM использовать в контроллере. Посмотрим на примере отображения списка альбомов:

  1. private function show_albums_list()
  2. {
  3. $albums_list = ORM::factory('album')->with('genre')->find_all();
  4. $this->template->content = View::factory('list')
  5. ->set('albums_list', $albums_list);
  6. }
* 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():

  1. private function get_genres_list()
  2. {
  3. $db_genres_list = $this->genre_model->get_list();
  4. $genres_list = array();
  5. if(sizeof($db_genres_list) >= 1)
  6. {
  7. foreach($db_genres_list as $item)
  8. {
  9. $genres_list[$item->id] = $item->name;
  10. }
  11. }
  12. return $genres_list;
  13. }
* This source code was highlighted with Source Code Highlighter.


Взмахнем волшебной палочкой и метод превратится в одну строчку:

  1. private function get_genres_list()
  2. {
  3. return ORM::factory('genre')->find_all()->select_list('id', 'name');
  4. }
* This source code was highlighted with Source Code Highlighter.


Создание и редактирование ORM-объектов



Напоследок — самое вкусное. Методы create() и update() не выполняют никаких проверок, а просто пытаются подсунуть модели Album_Model введенные данные. А ведь в Kohana есть замечательная библиотека Validation, которую очень удобно использовать вместе с ORM!

Для начала добавим-таки несколько строчек в модель Album_Model:

  1. public function Validate(array & $array, $save = FALSE)
  2. {
  3. $array = Validation::factory($array)
  4. ->pre_filter('trim')
  5. ->add_rules('name', 'required')
  6. ->add_rules('author', 'required')
  7. ->add_rules('genre_id', 'required')
  8. ->add_callbacks('name', array($this, '_album_available'));
  9. return parent::validate($array, $save);
  10. }
  11. public function _album_available(Validation $array, $field) {
  12. $result = (bool) ! $this->db
  13. ->where(array('name' => $array['name'], 'author' => $array['author'], 'id !=' => $this->id))
  14. ->count_records($this->table_name);
  15. if ( !$result) $array->add_error($field, 'album_exists');
  16. return $result;
  17. }
* This source code was highlighted with Source Code Highlighter.


Это — метод validate(), отвечающий собственно за проверку введенных данных с помощью библиотеки Validation, и нестандартное правило (точнее даже callback), отвечающее за уникальность сочетания Альбом + Исполнитель. Использовать их очень просто, рассмотрим модифицированный метод create():

  1. public function create()
  2. {
  3. if ($this->input->post())
  4. {
  5. $data = array
  6. (
  7. 'name' => NULL,
  8. 'author' => NULL,
  9. 'genre_id' => NULL
  10. );
  11. $data = arr::overwrite($data, $this->input->post());
  12. $album = ORM::factory('album');
  13. if ($album->validate($data, TRUE)) {
  14. url::redirect('album');
  15. }
  16. else {
  17. Session::instance()->set('errors', $data->errors('album'));
  18. Session::instance()->set('data', $data->as_array());
  19. }
  20. }
  21. url::redirect('album/show_create_editor');
  22. }
* This source code was highlighted with Source Code Highlighter.


Мы последовательно проверили наличие данных в $_POST, загрузили интересующие нас поля в массив $data (для этого используется метод overwrite() хэлпера arr) и отправили его на проверку в метод validate(). Второй параметр, установленный в TRUE, означает, что в случае успешной проверки ORM-объект будет сохранен. Если же произошла ошибка валидации, сохраняем в сессии описание ошибок и текущие значения из формы (никто ведь не любит заново вбивать многочисленные поля форм из-за малюсенькой опечатки), и редиректим обратно в редактор. Соответственно надо внести изменения в метод show_create_editor() и его шаблон (create.php):

  1. public function show_create_editor()
  2. {
  3. $errors = Session::instance()->get_once('errors', array());
  4. $data = array('name'=>'', 'author'=>'', 'genre_id'=>'');
  5. $data = arr::overwrite($data, Session::instance()->get_once('data', array()));
  6. $genres_list = $this->get_genres_list();
  7. $this->template->content = View::factory('create')
  8. ->set('genres_list', $genres_list)
  9. ->set('errors', $errors)
  10. ->set('data', $data);
  11. }
* This source code was highlighted with Source Code Highlighter.


  1. <?php echo form::open('album/create'); ?>
  2. <?php foreach($errors as $error)
  3. echo "<div class='error'>".$error."</div>";?>
  4. <table class='editor'>
  5. <tr>
  6. <td colspan='2' class='editor_title'>Create new album</td>
  7. </tr>
  8. <?php
  9. echo "<tr>";
  10. echo "<td>".form::label('name', 'Name: ')."</td>";
  11. echo "<td>".form::input('name', $data['name'])."</td>";
  12. echo "</tr>";
  13. echo "<tr>";
  14. echo "<td>".form::label('author', 'Author: ')."</td>";
  15. echo "<td>".form::input('author', $data['author'])."</td>";
  16. echo "<tr/>";
  17. echo "<tr>";
  18. echo "<td>".form::label('genre', 'Genre: ')."</td>";
  19. echo "<td>".form::dropdown('genre_id',$genres_list, $data['genre_id'])."</td>";
  20. echo "<tr/>";
  21. echo "<tr>";
  22. echo "<td colspan='2' align='left'>".form::submit('submit', 'Create album')."</td>";
  23. echo "</tr>";
  24. ?>
  25. </table>
  26. <?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-файла, то ошибки будут переведены с помощью указанного файла. Давайте сделаем перевод возможных ошибок:

  1. // файл i18n/ru_RU/album.php
  2. <?php defined('SYSPATH') or die('No direct script access.');
  3. $lang = array
  4. (
  5. 'name' => array(
  6. 'required' => 'Название альбома не указано',
  7. 'album_exists' => 'Комбинация альбом + артист уже добавлена',
  8. ),
  9. 'author' => array
  10. (
  11. 'required' => 'Имя исполнителя не указано',
  12. ),
  13. 'genre_id' => array
  14. (
  15. 'required' => 'Не выбран жанр альбома',
  16. ),
  17. );
* This source code was highlighted with Source Code Highlighter.


  1. // файл i18n/en_US/album.php
  2. <?php defined('SYSPATH') or die('No direct script access.');
  3. $lang = array
  4. (
  5. 'name' => array(
  6. 'required' => 'Album name required',
  7. 'album_exists' => 'Album & artist combination already exists',
  8. ),
  9. 'author' => array
  10. (
  11. 'required' => 'Author name required',
  12. ),
  13. 'genre_id' => array
  14. (
  15. 'required' => 'Genre required',
  16. ),
  17. );
* This source code was highlighted with Source Code Highlighter.


Я сделал два файла — на русском и на английском, текстовые ресурсы сгруппированы по именам полей формы. Угадайте, какой файл будет использован по умолчанию в нашем приложении? Конечно, английский. Скопируйте из папки system/config файл locale.php и подправьте значения для русского языка:

  1. <?php defined('SYSPATH') OR die('No direct access allowed.');
  2. /**
  3. * @package Core
  4. *
  5. * Default language locale name(s).
  6. * First item must be a valid i18n directory name, subsequent items are alternative locales
  7. * for OS's that don't support the first (e.g. Windows). The first valid locale in the array will be used.
  8. * @see php.net/setlocale
  9. */
  10. $config['language'] = array('ru_RU', 'Russian_Russia');
  11. /**
  12. * Locale timezone. Defaults to use the server timezone.
  13. * @see php.net/timezones
  14. */
  15. $config['timezone'] = '';
* This source code was highlighted with Source Code Highlighter.


Итоги



Вроде бы все основные моменты я рассказал. Попробуйте внести изменения в проект NetTuts'а, если что — можете подсмотреть в получившийся у меня архив. Конечно, не хватает множества различных мелочей (сортировки, выборка по определенным артистам или жанрам), но в рамках данной статьи это нереально. Если есть желание самостоятельно поработать, можете также вынести артистов в отдельную таблицу, это потребует определенных усилий по изменению контроллеров и шаблонов.

Теги:kohanaкохана
Хабы: Kohana
Всего голосов 8: ↑6 и ↓2+4
Просмотры827

Похожие публикации

Лучшие публикации за сутки