суббота, 5 февраля 2011 г.

Kohana 3: Работа с моделью

Мы уже более-менее разобрались с двумя компонентами архитектуры MVC, остался третий M - Model (Модель).

Модель - это PHP класс предназначенный для работы с информацией предоставленной или запрошенной контроллером. Например у вас есть гостевая книга, контроллер передаёт запрос в модель на получение последних десяти записей, модель возвращает эти записи контроллеру, который может передать эти данные в отображение. Контроллер так же может посылать новые записи в модель, обновлять или удалять существующие.

Проще говоря, модель занимается обработкой и управлением данными.


Для начала мы должны определиться с где и какие данные у нас. Это XML-лента, CSV, JSON, База Данных или что-то ещё? Думаю не стоит усложнять и в этом примере мы будем работать с нашим другом базой данных MySQL. Следующим шагом настроим соединение с базой данных MySQL.

Давайте откроем файл инициализации 'application/bootstrap.php' найдём следующую строку:

// 'database'   => MODPATH.'database',   // Database access

раскомментируем её:

'database'   => MODPATH.'database',   // Database access

Теперь сохраним его. Мы указали файлу инициализации фреймворка загрузить модуль базы данных, но нам необходимо его настроить. Скопируем 'database.php' из 'modules/database/config/' в 'application/config/'. Откроем файл 'application/config/database.php' и отредактируем его в соответствии с нашими настройками. Мои выглядят следующим образом:

<?php defined('SYSPATH') or die('No direct access allowed.');

return array
(
      'default' => array
      (
            'type'       => 'mysql',
            'connection' => array(
                  /**
                   * Следующие параметры доступны для MySQL:
                   *
                   * string   hostname     имя хоста, или сокет
                   * string   database     имя базы данных
                   * string   username     имя пользователя
                   * string   password     пароль пользователя
                   * boolean  persistent   использовать постоянное соединение?
                   *
                   * Порты и сокеты могут быть добавлены к имени хоста
                   */
                  'hostname'   => 'localhost',
                  'database'   => 'kohana_test',
                  'username'   => 'kohana_user',
                  'password'   => FALSE,
                  'persistent' => FALSE,
            ),
            'table_prefix' => '',
            'charset'      => 'utf8',
            'caching'      => FALSE,
            'profiling'    => TRUE,
      ),
      'alternate' => array(
            'type'       => 'pdo',
            'connection' => array(
                  /**
                   * Следуюшие параметры доступны для PDO:
                   *
                   * string   dsn         имя источника данных
                   * string   username    имя пользователя
                   * string   password    пароль пользователя
                   * boolean  persistent  Использовать постоянное соединение?
                   */
                  'dsn'        => 'mysql:host=localhost;dbname=kohana_test',
                  'username'   => 'kohana_user',
                  'password'   => FALSE,
                  'persistent' => FALSE,
            ),
            /**
             * Следующие дополнительные параметры доступны для PDO:
             *
             * string   identifier  set the escaping identifier
             */
            'table_prefix' => '',
            'charset'      => 'utf8',
            'caching'      => FALSE,
            'profiling'    => TRUE,
      ),
);

Сохраним его. Имя базы: kohana_test, имя пользователя: kohana_user, без пароля. Вы можете использовать свои значения или создать аналогичную базу и пользователя у себя.

Если у нас используется одна база для нескольких приложений, то можно задать префикс таблиц для модуля базы данных. Например, если хотим что бы имя таблиц нашего приложения начиналось с 'ko3_', укажем параметр 'table_prefix' => 'ko3_'. Соответственно у таблиц создаваемых в ручную нужно добавлять этот префикс самостоятельно. При работе с таблицами через модуль базы данных префикс указывать не нужно, он будет добавляться автоматически.

Вот SQL запрос создания таблицы:

CREATE TABLE `posts` (
  `id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) DEFAULT NULL,
  `post` TEXT,
  PRIMARY KEY  (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC;

Запустите его в вашем любимом MySQL клиенте. Возможно вы заметили, что указана кодировка utf8 в обеих конфигурациях и параметрах создания таблицы. Это позволит нам работа с i18n (интернационализацией).

Создадим новый каталог 'model' в 'application/classes/'. В каталоге 'application/classes/model/' создадим файл 'post.php' со следующим содержимым:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Post extends Kohana_Model
{
      /**
       * Get the last 10 posts
       * @return ARRAY
       */
      public function get_last_posts($limit, $offset = 0)
      {
            // SQL: SELECT * FROM 'posts' ORDER BY 'id' DESC LIMIT 0, 10
            return DB::select()                 // SELECT - DB:SELECT
                  ->from('posts')               // Из таблицы 'post'
                  ->order_by('id','DESC')       // Сортируем по 'id' в обртном порядке
                  ->limit($limit)               // Количество записей с результатом
                  ->offset($offset)             // пропустив $offset (по умолчанию 0) записей
                  ->execute()                   // Выполняем
                  ->as_array();                 // Результат ввиде массива
      }
}
?>

Разберём код метода 'get_last_posts()': мы используем Query Builder фреймворка Kohana3 для быстрого и легко читаемого построения запроса.

select() - запрашиваем все поля, from('post') - таблицы 'post', limit($limit) - количество строк с результатом запроса, offset($offset) пропустив $offset (по умолчанию 0) строк результата, execute() - выполнить запрос, as_array() - данные вернуть в виде массива с результатами запроса. Вот и всё, ничего сложного.

В SQL наш запрос можно записать так:

SELECT * FROM 'posts' ORDER BY 'id' DESC LIMIT 0, 10;

Для каждого типа запроса к базе используется свой класс для SELECT - DB::select, для INSERT - DB::insert, для UPDATE - DB::update и для DELETE - DB::delete.

Для наглядности, несколько примеров использования разных типов запросов и, соответственно, классов:

DB::insert

INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd')
$query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd'));

DB::update

UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john'
$query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john');

DB::delete

DELETE FROM `users` WHERE `username` IN ('john', 'jane')
$query = DB::delete('users')->where('username', 'IN', array('john', 'jane'));

Теперь у нас есть модель и методом, я уверен, вы хотите его попробовать в деле. Откроем в редакторе 'first.php' из каталога 'application/classes/controller' и добавим в него метод 'action_posts()' со следующим содержимым:

public function action_posts()
{
      $posts = Model::factory('post');
      $first = array();

      $this->template->title            = 'Kohana 3.0 Model Test';
      $this->template->meta_keywords    = 'PHP, Kohana, KO3, Framework, Model';
      $this->template->meta_description = 'A test of the KO3 framework Model';
      $this->template->styles  = array();
      $this->template->scripts = array();

      // Получаем 10 последних записей
      $first['posts'] = $posts->get_last_posts(10);
      $this->template->content = View::factory('pages/posts', $first);
}

В основном этот метод занимается тем, что вызывает метод модели 'get_last_posts()' и получает массив данных, которые мы передаём в отображение. Кстати, о отображениях... Создадим новый файл с именем 'posts.php' в каталоге 'application/views/pages/' и следующим содержимым:

<?php
foreach($posts as $post):
echo '<div'.HTML::attributes(array('class'=>'post')).">\n";
echo "\t<h2>".$post['title']."</h2>\n";
echo "\t<div>".$post['post']."</div>\n";
echo '</div>'."\n";
endforeach;
?>

В этом отображении в цикле перебирается массив '$post' с данными переданными контроллером, из массива выводятся записи 'titte' и 'post'. Но так как в нашей таблице нет записей, то соответственно ничего выводится не будет. Сейчас мы это исправим, выполним SQL запрос который добавит записи в таблицу:

INSERT  INTO `posts`(`id`,`title`,`post`) VALUES (1,'Тестовое сообщение','Здесь у нас немного текста.');
INSERT  INTO `posts`(`id`,`title`,`post`) VALUES (2,'Ещё одно сообщение','Ещё немного текста');

Теперь откроем в браузере http://kohana.local/first/posts и должны увидеть две записи на экране.

Добавим возможность добавлять новые записи в базу данных. Откроем модель записей 'application/classes/model/post.php' и создадим новый метод:

/**
 * Создание записей в таблице
 * @param string $title  Текст заголовка
 * @param string $post   Текст сообщения
 */
public function add_post($title, $post)
{ 
 // INSERT INTO 'posts' SET 'title' = $title, 'post' = $post
 DB::insert('posts',array('title','post')) // Добавляем записи 'title' и 'post' в таблицу 'posts'
  ->values(array($title, $post))        // 'title' = $title, 'post' = $post
  ->execute();
}

Вставка происходит тоже довольно просто. Сохраним модель. Вернёмся к отображению 'application/views/pages/posts.php' и изменим содержимое:

<?php
if (!empty($msg)):
echo '<div '.HTML::attributes(array('class'=>'alert '.$msg_type)).'>'.$msg."</div>\n";
endif;

foreach($posts as $post):
echo '<div'.HTML::attributes(array('class'=>'post')).">\n";
echo "\t<h2>".$post['title']."</h2>\n";
echo "\t<div>".$post['post']."</div>\n";
echo '</div>'."\n";
endforeach;

echo Form::open(url::base().'first/posts/',array('method' => 'post'))."\n";
echo '<div>';
echo "\t".Form::label('title', 'Заголовок')."\n";
echo "\t".Form::input('title')."\n";
echo "</div>\n";
echo '<div>';
echo "\t".Form::label('post', 'Сообщение')."\n";
echo "\t".Form::textarea('post' ,NULL ,array('rows' => 5, 'cols' => 20))."\n";
echo "</div>\n";
echo Form::submit('submit', 'Отправить')."\n";
echo Form::close()."\n";
?>

Как вы должны заметить, в этом отображении мы используем хэлперы. С хэлпером 'HTML' мы уже немного знакомы, сейчас мы использовали метод 'attributes' который перебирает массив переданных ему параметров и формирует из них атрибуты html тэгов. Второй хэлпер 'Form' генерирует элементы формы.

Form::open($action = NULL, array $attributes = NULL) - генерирует тэг открытия формы,

Form::label($input, $text = NULL, array $attributes = NULL) - генерирует тэг подписи,

Form::input($name, $value = NULL, array $attributes = NULL) -генерирует тэг input,

Form::textarea($name, $body = '', array $attributes = NULL, $double_encode = TRUE) - генерирует тэг textarea

Form::submit($name, $value, array $attributes = NULL) - генерирует тэг submit,

Form::close() - генерирует тэг закрытия формы.

Вернёмся к контроллеру 'application/classes/controller/first.php' и добавим новый метод '_add_post()':

/**
 * Метод посредник для добавления записи в таблицу
 * @param string $title         Текст заголовка
 * @param string $post_content  Текст сообщения
 * @access private
 */
private function _add_post($title, $post_content)
{
   // Загружаем модель
   $post = Model::factory('post');

   // Проверям обязательные поля
   if(empty($title))
   {
      return(array('error' => 'Пожалуйста введите заголовок.'));
   }
   elseif(empty($post_content))
   {
      return(array('error' => 'Пожалуйста введите сообщение.'));
   }

   // Записываем в базу данных
   $post->add_post($title, $post_content);
   return TRUE;
}

Код приведённый выше является посредником между 'action_posts()' и моделью, которая сохраняет записи. Вернёмся к 'action_posts' и добавим немного кода:

public function action_posts()
{
   // Загружаем модель
   $posts = Model::factory('post');

   $first = array();
   
   $this->template->title            = 'Kohana 3.0 Model Test';
   $this->template->meta_keywords    = 'PHP, Kohana, KO3, Framework, Model';
   $this->template->meta_description = 'A test of the KO3 framework Model';
   
   $this->template->styles  = array();
   $this->template->scripts = array();
   
   $firs['msg']      = '';
   $firs['msg_type'] = '';
   
   // Обрабатываем POST
   if($_POST)
   {
      $ret = $this->_add_post(
         Security::xss_clean(Arr::get($_POST, 'title', '')), // Очищаем элемент $_POST['title']
         Security::xss_clean(Arr::get($_POST, 'post', ''))); // Очищаем элемент $_POST['post']

      if(isset($ret['error']))
      {
         $first['msg']      = $ret['error']; // Текст ошибки
         $first['msg_type'] = 'error';       // Класс сообщения в отображении
      }
      else
      {
         $first['msg']      = 'Сохранено.';
         $first['msg_type'] = 'success';
      }
   }           

   // Получаем 10 последних записей
   $first['posts'] = $posts->get_last_posts(10);

   $this->template->content = View::factory('pages/posts', $first);
}

Сохраним контроллер и обновим страницу браузера. Теперь мы можем видеть довольно уродливую форму внизу страницы. Заполните поля и нажмите кнопку "Отправить". Ваше сообщение должно появится вверху страницы под надписью "Сохранено", если вы заполнили не оба поля, то увидите сообщение об ошибке.

Что бы наша страница не была такой страшной добавим файл CSS-стиля:

#container {
     width: 640px;
}

h2 {
     font-size: 1.3em;
     font-weight: bold;
}

.alert {
     font-size: 1.3em;
     font-weight: bold;
     margin: 10px;
     padding: 5px;
}

.error {
     background: #fcc;
}

.success {
     background: #cfc;
}

div.post {
     padding: 5px 5px;
     margin: 10px;
     color: #222;
     background: #eee;
     border: 1px solid #aaa;
     display: block;
}

label {
     width: 40px;
     display: block;
     margin: 2px 0;
}

form > div {
     margin: 5px;
}

Сохраним его в 'assets/css/' под именем 'post.css'.

В контроллере 'application/classes/controller/first.php' в методе 'action_post()' изменис строку :

$this->template->styles  = array();

на

$this->template->styles  = array('assets/css/post.css' => 'screen');

Как вы заметили в нашей модели отсутствует механизм изменения записей, можете рассматривать это как домашнее задание.

P.S. За основу взяты материалы с Inside DealTaker и Unofficial Kohana 3.0 Wiki

P.P.S. Я изменил оригинальный исходный код материалов этой статьи, что бы показать больше возможностей фреймворка Kohana 3 и по возможности придерживаться стандарта написания кода фреймворка Conventions and Coding Style. Так же было изменено имя контроллера шаблона Controller_Default_Template и он был вынесен в подкаталог 'default'.

Код к данному руководству можно найти на Google Code.

svn checkout http://dev-mark.googlecode.com/svn/trunk/dev-mark/Kohana3-tutorial Kohana3-tutorial

Другие части руководства:

Похожие по тематике посты:

2 комментария:

Unknown комментирует...

Может немного не по теме, но для новичков может пригодится. Security::xss_clean() в kohana 3.1 уже отсутствует. Сам только начал изучать kohana, поэтому еще не разобрался, чем можно заменить.

alex комментирует...

Как раз по теме, я начал переводить когда актуальной была Kohana 3.0, а в 3.1 много изменилось.
В 3.0 в Security::xss_clean() использовался вот этот код http://svn.bitflux.ch/repos/public/popoon/trunk/classes/externalinput.php

Можно расширить хелпер Security добавив в класс новый метод xss_clean().

Спасибо, что напомнили по xss_clean()!