Pull to refresh

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

Reading time16 min
Views1K
Итак, в предыдущем топике (виден только подписчикам блога!) я ссылался на статью о 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.  
  6. #navigation li {
  7.   float: left;
  8.   margin-left: 0.5em;
  9.   border: 1px solid black;
  10. }
  11.  
  12. #navigation li a {
  13.   font-size: 14px;
  14.   display: block;
  15.   padding: 2px 5px;
  16.   text-decoration: none;
  17.   color: #fff;
  18.   background: #666;
  19. }
* 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.  
  14.     </tr>
  15.     <?php
  16.       foreach($albums_list as $item)
  17.       {
  18.         echo "<tr>";
  19.         echo "<td class='item'>".$item->name."</td>";
  20.         echo "<td class='item'>".$item->author."</td>";
  21.         echo "<td class='item'>".$item->genre->name."</td>";
  22.         echo "<td class='item'>".html::anchor('album/delete/'.$item->id,html::image('assets/images/delete.png'))."</td>";
  23.         echo "<td class='item'>".html::anchor('album/show_update_editor/'.$item->id,html::image('assets/images/edit.png'))."</td>";
  24.         echo "</tr>";
  25.       }
  26.     ?>
  27.     </table>
  28.   
* 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. ...
  8.   private function show_albums_list()
  9.   {
  10.     $albums_list = $this->album_model->get_list();
  11.     $this->template->content = View::factory('list')
  12.                   ->set('albums_list', $albums_list);
  13.   }
  14. ...
* 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.  
  12.   echo "<tr>";
  13.   echo "<td>".form::label('author', 'Author: ')."</td>";
  14.   echo "<td>".form::input('author', $album->author)."</td>";
  15.   echo "<tr/>";
  16.  
  17.   echo "<tr>";
  18.   echo "<td>".form::label('genre', 'Genre: ')."</td>";
  19.   echo "<td>".form::dropdown('genre_id',$genres_list, $album->genre_id)."</td>";
  20.   echo "<tr/>";
  21.  
  22.   echo "<tr>";
  23.   echo "<td colspan='2' align='left'>".form::submit('submit', 'Update album')."</td>";
  24.   echo "</tr>";
  25.  
  26. ?>
  27. </table>
  28. <?php
  29.   echo form::hidden('album_id',$album->id);
  30.   echo form::close();
  31. ?>
* 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.  
  6. class Genre_Model extends ORM {
  7.   
  8.   protected $has_many = array('albums');
  9. }
* 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.  
  6.     if(sizeof($db_genres_list) >= 1)
  7.     {
  8.       foreach($db_genres_list as $item)
  9.       {
  10.         $genres_list[$item->id] = $item->name;
  11.       }
  12.     }
  13.     return $genres_list;
  14.    }
* 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.  
  12.   public function _album_available(Validation $array, $field) {
  13.     $result = (bool) ! $this->db
  14.       ->where(array('name' => $array['name'], 'author' => $array['author'], 'id !=' => $this->id))
  15.       ->count_records($this->table_name);
  16.     if ( !$result) $array->add_error($field, 'album_exists');
  17.     return $result;
  18.   }
* 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.  
  14.   echo "<tr>";
  15.   echo "<td>".form::label('author', 'Author: ')."</td>";
  16.   echo "<td>".form::input('author', $data['author'])."</td>";
  17.   echo "<tr/>";
  18.  
  19.   echo "<tr>";
  20.   echo "<td>".form::label('genre', 'Genre: ')."</td>";
  21.   echo "<td>".form::dropdown('genre_id',$genres_list, $data['genre_id'])."</td>";
  22.   echo "<tr/>";
  23.  
  24.   echo "<tr>";
  25.   echo "<td colspan='2' align='left'>".form::submit('submit', 'Create album')."</td>";
  26.   echo "</tr>";
  27. ?>
  28. </table>
  29. <?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.  
  4. $lang = array
  5. (
  6.     'name' => array(
  7.       'required' => 'Название альбома не указано',
  8.       'album_exists' => 'Комбинация альбом + артист уже добавлена',
  9.     ),
  10.  
  11.     'author' => array
  12.     (
  13.       'required' => 'Имя исполнителя не указано',
  14.     ),
  15.  
  16.     'genre_id' => array
  17.     (
  18.       'required' => 'Не выбран жанр альбома',
  19.     ),
  20. );
* 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.  
  4. $lang = array
  5. (
  6.     'name' => array(
  7.       'required' => 'Album name required',
  8.       'album_exists' => 'Album & artist combination already exists',
  9.     ),
  10.  
  11.     'author' => array
  12.     (
  13.       'required' => 'Author name required',
  14.     ),
  15.  
  16.     'genre_id' => array
  17.     (
  18.       'required' => 'Genre required',
  19.     ),
  20. );
* 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. /**
  13. * Locale timezone. Defaults to use the server timezone.
  14. * @see php.net/timezones
  15. */
  16. $config['timezone'] = '';
  17.  
* This source code was highlighted with Source Code Highlighter.


Итоги



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

Tags:
Hubs:
Total votes 8: ↑6 and ↓2+4
Comments12

Articles