Контент


ORM в Kohana v3.1

Вместе с изменениями ядра фреймворка в ветке 3.1.x происходят и изменения в коде модулей. Один из самых популярных (а также наиболее непонятных для новичков) — это модуль ORM, поэтому просто необходимо прокомментировать произошедшие в нем коррективы. Большинство из них, конечно же, связаны с революцией в валидации Kohana v3.1.

CRUD

Помимо метода save() теперь есть отдельные методы create() и update() для добавления новой или обновления старой записи соответственно. Метод save() просто запускает create() или update() в зависимости от $this->loaded().

Фильтры

Как мы помним, ранее валидация в ORM держалась на трех китах — фильтры, правила и коллбэки. После всех тех изменений в ветке 3.1 валидация сводится к использованию правил. Но фильтры остались, изменилась их роль в модели. Я уже писал, что разработчики Kohana считают правильным подгонять данные модели под нужный формат на этапе присвоения значения, а не в момент валидации. И вот результат — в ORM v3.1 появилась возможность применять различного рода преобразования перед сохранением значения в поле модели.

Вот как это примерно может выглядеть:

public function filters() 
{
   return array(
      TRUE        => array(
         array('trim'),
      ),
      'username'  => array(
         array('strtolower'),
      ),
      'login_count' => array(
        array('intval'),
      ),
   );
}

Теперь фильтры указываются не в свойстве $_filters, а в методе filters(), который должен возвращать массив. Ключами массива являются либо имена полей, либо TRUE (т.е. фильтр действует на все поля). Каждому ключу соответствует массив фильтров, фильтры тоже представляют собой массивы вида (фильтр, параметры). В приведенных выше примерах явно указаны только имена фильтров (стандартные функции php), они достаточно просты. Но роль фильтров может быть значительно расширена засчет все тех же контекстов, введенных в валидации Kohana v3.1. Давайте представим, что у нас текстовое поле text, которое не должно содержать более 100 символов:

public function filters() 
{
   return array(
      'text'   => array(
         array('text::limit_chars', array(':value', 100, ''))
      ),
   );      
}

Вуаля! При установке значения будет вызван метод text::limit_chars(), который обрежет значение до 100 символов. Строка ‘:value‘, конечно же, является контекстом, который будет заменен на фильтруемое значение. Третьим параметром я передаю пустую строку, т.к. по умолчанию добавится символ «многоточие».

В качестве контекстов можно использовать не только ‘:value‘, но также ‘:model‘ (текущая модель) и ‘:field‘ (имя поля, к которому относится фильтр).

Пример выше можно расширить еще больше. Представим, что у нас есть поля text и intro, причем intro является укороченной версией значения из text и мы не хотим заполнять его в явном виде. Создадим фильтр, который будет при заполнении поля text вычислять значение и для intro:

public function filters() 
{
   return array(
      'text'   => array(
         array(array($this, 'set_intro'))
      ),
   );      
}
 
public function set_intro($value)
{
   $this->intro = text::limit_chars($value, 100, '');
   return $value;
}

Не забудьте вернуть значение $value, это обязательное требование для фильтров!

Все просто — фильтр вызывает метод set_info() для текущего объекта модели, а тот заполняет вычисляемое поле intro. Получаем что-то вроде Jelly_Field_Expression из Jelly.

Метки (labels)

Тут изменение достаточно простое — как и в случае с фильтрами, вместо свойства $_labels надо создавать метод labels():

public function labels()
{
   return array(
      'text'   => 'article text',
   );
}

Валидация

Из валидации убраны фильтры, коллбэки и правила теперь по сути одно и то же. Достигается это за счет использования все тех же контекстов валидации, создавать сложные функции для проверки полей модели. В качестве контекстов доступны ‘:field‘, ‘:value‘, ‘:validation‘ (имя поля, его значение и сам объект Validation — эти контексты устанавливаются классом Validation) и ‘:model‘ (текущая модель ORM). В общем, возможности для валидации весьма широки, и грустить об отказе от коллбэков в их изначальном виде не приходится.

Объявляются правила в методе rules():

public function rules()
{
   return array(
      'username'   => array(
         array('not_empty'),
         array('min_length', array(':value', 4)),
         array(array($this, 'is_unique'), array(':value', ':validation')),
      ),
   );
}

Первое правило просто проверяет поле ‘username‘ на наличие значения. Обратите внимание, что стандартные правила класса Valid можно указывать просто по имени, а не как ‘Valid::not_empty‘ или array('Valid', 'not_empty'). Параметры в данное правило оставляем пустые, в этом случае автоматически будет передавать проверяемое значение (т.е. контекст ‘:value‘). Второе правило проверяет на минимальную длину, и тут уже мы передаем два параметра — значение и минимальное число символов. Третий параметр по сути является коллбэком, это вызов метода is_unique() с передачей в него значения и объекта Validation.

Объект Validation нужен для передачи в него текста ошибки, если вдруг коллбэк обнаружит расхождения. Выглядеть это будет примерно так:

public function is_unique($value, $validation)
{
   if ( ! условие на уникальность)
   {
      $validation->error('username', 'username is not unique');
   }
}

Чтобы метод is_unique() был более универсальным, в него можно дополнительно передавать контекст ‘:field‘ и писать ошибку с его использованием. Например, можно будет проверять не только username, но и email — механизм проверки в принципе ничем не отличается.

Проверка модели

Вызов валидации осуществляется все тем же методом check(). Но теперь метод возвращает не TRUE/FALSE, а бросает исключение ORM_Validation_Exception, если найдены ошибки (прямо как в Jelly). Соответственно теперь каждая проверка должна быть заключена в блок try...catch():

try {
   $user->save();
}
catch(ORM_Validation_Exception $e)
{
   // произошла ошибка валидации
}

Да-да, из приведенного мной участка кода следует, что метод check() автоматически вызывается при сохранении модели методами save(), create() и update().

Работа над ошибками валидации

Если раньше мы могли получить ошибки через встроенный объект Validate модели ORM (конструкцией вида $user->validate()->errors()), то теперь для этого надо использовать бросаемое исключение. Для этого в классе ORM_Validation_Exception существует метод errors():

try {
   $user->save();
}
catch(ORM_Validation_Exception $e)
{
   $errors = $e->errors('validation');
}

Принцип такой же — передаем в метод errors() путь к переводу в messages, а также булевский файл, переводить ли результат через i18n. Замечу, что можно использовать и $user->validation()->errors('validation') — этот метод тоже вернет тексты ошибок. Тогда в чем же разница? Об этом — следующий пункт различий.

Внешняя валидация

На форумах часто спрашивают, как проверить данные для нескольких моделей, либо данные для модели + капчу. Раньше эта задача полностью ложилась на плечи разработчика проекта, но сейчас ORM предлагает свое решение. В методы check(), save(), create() и update() можно передавать дополнительный объект класса Validation, который уже должен быть подготовлен к проверке (т.е. иметь загруженные входящие данные, правила и т.д.).

Перед проверкой самой модели метод дополнительно проверяется этот дополнительный объект Validation (если передан), и результат его проверки влияет на общий итог валидации модели. Ошибки будут добавлены в бросаемое исключение, поэтому и рекомендуется извлечение ошибок через объект ORM_Validation_Exception — он просто обладает информацией о внешних ошибках.

Таким образом, валидация будет провалена даже если сама модель заполнена корректно, но, к примеру, капча неверна.

В связи с возможным наличием «посторонних» ошибок, генерация ошибок несколько усложняется. Теперь массив ошибок (до перевода, его можно получить методом errors() без параметров) помимо ошибок самих полей модели содержит массив ошибок внешней валидации под индексом ‘_external‘. А сам механизм перевода ошибок выглядит так:

  • Вызываем метод errors($directory, $translate) исключения ORM_Validation_Exception, передавая туда в качестве первого параметра имя директории с переводом. Если не хотите брать перевод непосредственно из папки messages, то передайте пустую строку.
  • Внутри исключения происходит поиск файлов с переводом, учитывая переданный параметр $directory. Предположим, передали $directory = 'valid'. Для валидации полей модели Model_User будет ожидаться имя файла messages/valid/user.php (где ‘user‘ — имя модели, хранимое в свойстве $_object_name), если он не найден, то перевод берется из файла messages/validation.php.
  • Для ошибок внешней валидации будет вычислен путь messages/valid/user/_external.php. Опять же, если такой файл не найден (или в нем нет нужного перевода), идем к дефолтному messages/validation.php.

Решение достаточно интересное. С одной стороны, мы можем предусмотреть custom‘ные внешние ошибки для каждой модели, используя файлы _external.php, а с другой — всегда можно положиться на старый добрый messages/validation.php, в котором уже прописаны все стандартные тексты.

Прочие отличия от 3.0

  • Добавлено свойство $_db_group. Оно выполняет функции свойства $_db из ветки 3.0, т.е. содержит имя профиля Database для данной модели. Таким образом, при портировании своих моделей из 3.0 в 3.1 надо помнить об этом изменении, иначе модели будут использовать дефолтную конфигурацию.
  • Метод add($alias, $far_keys), который позволяет добавлять связь HABTM, в версии 3.1 поддерживает большое число форматов для параметра $far_keys — это может конкретная модель ORM (единственный вариант, доступный в 3.0), ее первичный ключ, или даже массив ключей (для добавления сразу нескольких связей). Зато исчезла возможность передать дополнительные значения в промежуточную таблицу, вероятно как раз из-за возможного добавления нескольких записей.
  • Метод remove($alias, $far_keys) работает по тому же принципу — можно передать один или несколько идентификаторов, а если параметр $far_keys пустой, то удалятся все имеющиеся записи для данной связи.
  • В метод values($values, $expected) добавлен второй параметр — список ключей загружаемого массива, которые надо учитывать. Бывает удобно, когда в качестве массива $values выступает $_POST или $_GET. Кстати, хотя в комментариях к этому методу написано, что он поддерживает загрузку данных для связи 1-к-1 (has_one и belongs_to), реализации данного функционала я не нашел. Шажок назад по сравнению с 3.0.
  • Класс ORM реализовывает интерфейс serializable, в соответствии с которым добавлены методы serialize() и unserialize(). Напомню, что в 3.0 использовались «магические» __sleep() и __wakeup().
  • Добавлены PhpDoc-комментарии, описывающие методы объектов Query Builder (например where() или from()) и «магические» методы модели, возвращающие соответствующие защищенные свойства (например, знаете ли вы, что $user->has_many() вернет значение свойства $_has_many?). Правда, в случае с магическими методами IDE правильно подсказывать не будет, т.к. методы почему-то описаны как свойства (т.е. с использованием @property вместо @method).

В заключение скажу, что изменения достаточно интересные, правда слегка однобокие (все ушло в валидацию). Лично я все жду, когда в ORM добавятся алиасы для полей, как в Jelly.

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в Kohana3.

Теги: , , , .


Комментарии (14)

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

  1. AeR пишет:

    Очень интересно, спасибо за статью !

  2. Satisfaction пишет:

    Отличная статья! Столько изменений, а главное нужных =)

  3. xbagir пишет:

    Хех. Вроде как разработчики стали поглядывать на yii, аналогии проглядываются по валидации :)

  4. Zares пишет:

    @xbagir
    Если что-либо из примитивного стало, наконец, более совершенным — нет повода полагать, что это есть результат заимствования того или иного решения или идеи со стороны…

    Скорее всего — это есть пример положительного результата, я бы так назвал, вкрапления результатов тех наработок, которые были ранее, в том числе и сторонних…

    Надеюсь, что в скором времени этот модуль станет полноценным штатным ORM, а вся та шелуха, образованием которой неизбежно сопровождается любой рост, просто осыплется, и перестанет, наконец, нас с вами волновать.

  5. xbagir пишет:

    Zares
    Ну как же. Кохана троечка много у зенда за основу взяла. И это не особо скрывалось. Не думаю, что плохо брать за идею какие-то зарекомендовашие себя решения. Тем более у уii есть что подсмотреть:)

  6. Zares пишет:

    Ну, знаете-ли… это становится похожим на протокол заседания адвокатской коллегии :)

    Речь-то шла всего лишь о новом ORM…
    Кстати, который мне с успехом удалось применить в новоиспеченном веб-приложении на KO3.1, впервые не прибегая к использованию сторонних решений, сэкономив при этом время разработки примерно на треть!

  7. xbagir пишет:

    Zares

    *Кстати, который мне с успехом удалось применить в новоиспеченном веб-приложении на KO3.1, *

    А можено поподробнее, почему ранее прибегали к сторонним решениям? Чего не хватало?

    p/s: Всех с праздником!!! )

  8. Zares пишет:

    @xbagir
    Адекватных кодеров.
    Теперь я все это делаю сам…

  9. Satisfaction пишет:

    А как теперь добавлять значения в промежуточную таблицу ?

  10. atom пишет:

    Для ошибок внешней валидации будет вычислен путь messages/valid/user/_external.php. Опять же, если такой файл не найден (или в нем нет нужного перевода), идем к дефолтному messages/validation.php.

    что то не работает такой вариант на версии 3.2. сделал в директории messges директорию auth и иуда положил user.php с переводом, тут же сделал user/_external.php и все равно не переводит. в чем проблема подскажите?

  11. musicalbee пишет:

    messages/valid/user/_external.php.
    сделал в директории messges директорию auth и иуда положил user.php с переводом, тут же сделал user/_external.php

    Может в папке ?

  12. biakaveron пишет:

    @Satisfaction
    Если в таблице должны быть другие данные, кроме внешних ключей, то напрашивается вывод — это не одна связь M-N, а две связи 1-N. Соответственно делаем промежуточную модель.

    @atom
    Если я правильно понимаю, в Вашем случае необходимо запрашивать ошибки через ->errors(‘auth’); Так делаете?

  13. atom пишет:

    @biakaveron

    да делаю именно так.

  14. Алексей пишет:

    class Model_Forum_Topic extends ORM {

    protected $_has_many = array(
    ‘posts’ => array(
    ‘model’ => ‘forum_post’,
    ‘foreign_key’ => ‘topic_id’,
    ),
    );

    }

    class Model_Forum_Post extends ORM {

    protected $_load_with = array(
    ‘topic’
    );
    protected $_belongs_to = array(
    ‘topic’ => array(
    ‘model’ => ‘forum_topic’,
    ‘foreign_key’ => ‘topic_id’,
    ),
    );
    }

    ORM::factory(‘forum_post’)
    ->with(‘topic’)
    ->where(‘topic:id’,'=’,1)
    ->find_all();
    Генерит запрос вида:
    SELECT `topic`.`id` AS `topic:id`, `topic`.`topic_text` AS `topic:topic_text`, `forum_post`.* FROM `forum_posts` AS `forum_post` LEFT JOIN `forum_topics` AS `topic` ON (`topic`.`id` = `forum_post`.`topic_id`) WHERE `topic:id` = 1

    Так вот именно с условием и не работает, хотя алиас поля именно такой, понимаю что единственный вариант делать:
    ORM::factory(‘forum_post’)
    ->with(‘topic’)
    ->where(‘topic.id’,'=’,1)
    ->find_all();
    но хотелось бы узнать — а если мыслить не таблицами и их алиасами, а алиасами полей, неужели так нельзя?
    Мускул говорит нет:
    mysql> SELECT …
    1054 — Unknown column ‘topic:id’ in ‘where clause’
    mysql>



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.