Библиотека ORM всегда вызывала множество вопросов как у новичков, так и у более опытных пользователей Kohana. Удобная в использовании, но сложная в изучении, она проскакивает наверное в каждом третьем-четвертом топике официального форума. А знаете ли Вы, что есть и другие библиотеки? Для 2.3.4 есть Auto Modeler от zombor (Jeremy Bush) и его переработанная версия — Simple Modeler от thejw23. А поскольку в последнее время мы больше обращаем внимание на Ko3, рассмотрим альтернативу ORM, созданную самим создателем фреймворка — Sprig от Shadowhand (Woody Gilk).
Как мы знаем, основная «фишка» ORM — прозрачная работа с таблицами БД. Т.е. мы можем не описывать имеющиеся в таблице поля, а обращаться к ним как к свойствам модели. Это становится возможным благодаря автоматическому запросу (SHOW COLUMNS, если говорить о MySQL) к таблице при создании модели, загружающему информацию о структуре таблицы. Впрочем, это не освобождает от необходимости объявлять связи с другими таблицами.
Возможно Вы не знаете — когда Ваша таблица примет окончательные очертания, можно прописать мета-данные о формате полей в статическом свойстве
$_column_cache
(это массив, используем ключ$this->_object_name
), чтобы ORM не запрашивал их каждый раз. Проще всего «подсмотреть» эти данные (используяvar_dump()
илиKohana::dump()
) и скопировать в$_column_cache
.
А что же Sprig? Тут немного другая философия — каждое поле таблицы представлено отдельным объектом, потомком абстрактного класса Sprig_Field. Казалось бы, что тут выдумывать, поле оно и есть поле, делаем его строковым и не волнуемся ни о чем. Ан нет! Использование отдельных классов имеет ряд преимуществ:
- Многие типы полей имеют сложившиеся форматы вывода на экран (к примеру, дату обычно не выводят в формате UNIX timestamp). С помощью Sprig мы можем настроить форматирование любого используемого типа данных.
- Аналогично и с генерацией элементов форм. Никто не будет показывать поле логического типа (т.е. TRUE/FALSE) в виде текстового — тут нужен checkbox. Sprig позволяет описать параметры генерируемого поля, в том числе и перечень допустимых значений (для формирования выпадающего списка).
- Есть типовые ограничения (уникальность, NOT NULL и т.д.), которые приходится прописывать для каждого поля в свойствах валидации (
$_filters
,$_rules
и$_callbacks
). А в Sprig поля имеют дополнительные свойства, описывающие различные аспекты хранения их в БД (и они автоматом используются при валидации). Об этом я напишу чуть позже.
Из чего строится модель
Каждая модель должна быть потомком класса Sprig (кто бы сомневался) и реализовывать метод _init()
, в котором собственно и описываются все поля таблицы. Например, вот что происходит в модели Sprig_Model_User (включена в дистрибутив Sprig):
protected function _init() { $this->_fields += array( 'id' => new Sprig_Field_Auto, 'username' => new Sprig_Field_Char(array( 'empty' => FALSE, 'unique' => TRUE, 'rules' => array( 'regex' => array('/^[\pL_.-]+$/ui') ), )), 'password' => new Sprig_Field_Password(array( 'empty' => FALSE, )), 'password_confirm' => new Sprig_Field_Password(array( 'empty' => TRUE, 'in_db' => FALSE, 'rules' => array( 'matches' => array('password'), ), )), 'last_login' => new Sprig_Field_Timestamp(array( 'empty' => TRUE, 'editable' => FALSE, )), ); } |
Поля хранятся в свойстве $_fields
. Каждый экземпляр класса Sprig_Field может быть создан с передачей дополнительных параметров, так что на стандартные для выбранного типа настройки можно сверху «накатить» свои. Так, если автоинкрементное поле `id` создается с настройками по умолчанию (что уж там еще настраивать), то для поля `username` добавлено правило валидации (ключ ‘rules‘) и свойства поля из БД — NOT NULL (ключ ‘empty‘) и UNIQUE (ключ ‘unique‘). Давайте рассмотрим полный список свойств класса Sprig_Field.
- $empty — поддерживает ли поле отсутствие значения. NOT NULL по сути. По умолчанию FALSE.
- $primary — является ли первичным ключем (PRIMARY KEY). По умолчанию FALSE.
- $unique — флаг уникальности (UNIQUE KEY). По умолчанию FALSE.
- $null — все пустые значения (например, пустые строки) автоматически будут преобразованы в NULL. По умолчанию FALSE.
- $editable — разрешено ли изменение даного поля пользователем. Может использоваться для автоинкрементых полей и автоматически генерируемых меток времени. По умолчанию TRUE.
- $default — значение по умолчанию.
- $choices — массив допустимых значений (что-то вроде эмуляции ENUM).
- $column — имя поля в БД. По умолчанию сгенерируется на основе имени объекта. Вы можете указать более удобный псевдоним для поля в методе
_init()
, а реальное имя указать в данном свойстве. Обычно это используется для внешних ключей. - $description — описание поля.
- $in_db — существует ли поле в БД (такие поля в ORM помещались в
$ignored_columns
). По умолчанию TRUE. - $label — метка поля. Используется для формирования элемента label формы, а также при валидации. По умолчанию сгенерируется автоматически, используя метод
Inflector::humanize()
, который заменяет тире и знаки подчеркивания на пробелы. - $filters — массив применяемых фильтров валидации.
- $rules — массив правил для поля.
- $callbacks — массив внешних функций (коллбэков).
Немного в стороне стоят свойства
$min_length
и$max_length
, которые могут быть добавлены в текстовые поля для ограничения длины.
Часто используемые комбинации данных свойств представлены в виде готовых классов-потомков Sprig_Field.
- Sprig_Field_Auto — автоинкремент. Первичный ключ, нередактируемый, пустые значения преобразовываются в NULL.
- Sprig_Field_Boolean — логическое поле. Может хранить NULL, значение по умолчанию — FALSE. При валидации автоматически применяется фильтр
filter_var(FILTER_VALIDATE_BOOLEAN)
. - Sprig_Field_Char — символьное поле. Можно указать минимальную/максимальную длину.
- Sprig_Field_Country — поле для кода стран в двухсимвольном (за небольшими исключениями) международном формате. Массив код => расшифровка хранится в свойстве
$codes
, которое будет скопировано в$choices
по умолчанию. Если необходимо предоставить возможность ввода других кодов, установите свойство$prompt
в TRUE. - Sprig_Field_Email — поле для электронной почты, добавлено правило
validate::email()
. - Sprig_Field_Enum — поле, имеющее ограниченное число значений. Если Вы забудете их установить в свойстве
$choices
, получите Exception. - Sprig_Field_Float — число с плавающей запятой. Можно настроить количество точек после запятой (свойство
$places
). - Sprig_Field_Image — поле для хранения пути к изображению. При создании необходимо явно указать параметр ‘path‘ — путь к директории с картинками. Также можно передать длину и ширину (‘width‘ и ‘height‘).
- Sprig_Field_Integer — целочисленное значение. Можно задать ‘min_value‘ и ‘max_value‘.
- Sprig_Field_Password — поле для хранени паролей. Можно указать метод хэширования в свойстве ‘hash_with‘ (по умолчанию ‘sha1‘).
- Sprig_Field_Text — поле для многострочных текстовых полей.
- Sprig_Field_Timestamp — поле для хранения даты/времени. Поддерживаются свойства ‘auto_now_create‘ и ‘auto_now_update‘ (автоматическая подстановка значения
NOW()
при создании и редактировании соответственно, по умолчанию FALSE) и ‘format‘ (формат вывода данных на экран через функциюdate()
, по умолчанию ‘Y-m-d G:i:s A‘). - Sprig_Field_ForeignKey — абстрактный класс, базовый для внешних ключей. NOT_NULL, но в то же время по умолчанию он не является существующим полем в БД (т.е.
$in_db == FALSE
). Дело в том, что не для всех типов связей внешний ключ располагается в текущей таблице (ниже рассмотрены доступные классы по типам связей). - Sprig_Field_HasOne — класс для описания связи has_one (один-к-одному). Поле игнорируемое (
$in_db == FALSE
) и нередактируемое. Этот класс следует применять для «старшей» таблицы в связи, т.к. в той, которая экспортирует свой первичный ключ в связанную таблицу. - Sprig_Field_BelongsTo — класс для belongs_to (обратная сторона связи has_one или has_many). В данном случае внешний ключ находится в таблице, так что $in_db == TRUE.
- Sprig_Field_HasMany — класс для has_many. Недоступен для редактирования.
- Sprig_Field_ManyToMany — поле HABTM. Может быть отредактировано, имеется дополнительное свойство $through (как Вы наверное догадываетесь, оно предназначено для имени промежуточной таблицы и по умолчанию формируется из названий связанных таблиц по тому же принципу, что и в ORM).
Кроме собственно полей, есть свойства для хранения мета-данных модели. По большей части они знакомы нам благодаря ORM:
- $_primary_key — имя поля для первичного ключа. Теоретически может быть массивом (в случае составного первичного ключа), но вроде как это еще не полностью поддерживаемая возможность.
- $_table — имя используемой таблицы.
- $_title_key — аналог
$_primary_val
из ORM. По умолчанию ‘name’. - $_sorting — настройка сортировки по умолчанию. Синтаксис
имя поля => направление
, напримерarray('created' => 'DESC')
.
Создание экземпляра модели и работа с ним
В отличие от ORM в Sprig конструктор недоступен для прямого вызова, создавать модели необходимо через статический метод factory($name, array $values = NULL)
. Это позволяет не создавать модель каждый раз заново, а использовать уже имеющуюся копию данного класса (эта копия представляет собой проинициализированную модель без каких-либо данных, т.е. происходит экономия на времени работы конструктора, в частности на методе _init()
). При этом копии между собой не связаны, так что изменения в одной не повлекут корректировку другой.
Второй параметр $values
позволяет передать значения для загрузки в модель. Значения, отсутствующие в списке полей таблицы, будут отброшены. Например, то, что в ORM записывалось как ORM::factory('blog', 1)
, в Sprig будет выглядеть как Sprig::factory('blog', array('id' => 1)
. Обратите внимание, что идентификатор будет загружен в модель, но запроса к БД не будет. Налицо все та же «ленивая загрузка». Для явного обращения к БД надо использовать метод load().
Можно присваивать значения полям модели вручную, обращаясь к ним как в ORM, например $blog->id = 1
. Также имеется метод values(array $values)
для изменения сразу нескольких полей.
$blog->id вернет значение поля. Если необходим сам объект (в данном случае это Sprig_Field_Auto), используйте методы field($name) или fields().
CRUD
Метод load()
я уже упомянул выше. Однако у него есть параметры, которые могут быть полезными для выборки данных из БД. Так, первым параметром идет объект класса Database_Query_Builder_Select, т.е. объект запроса на выборку, создаваемый обычно методом DB::select()
. С его помощью можно указать дополнительные условия выборки. Второй параметр — $limit
, позволяет ограничить количество отбираемых записей (по умолчанию 1).
Операции создания/редактирования и удаления в Sprig представлены отдельными методами (как мы помним, в ORM создание и редактирование были объединены в рамках метода save()
).
- Метод
create()
создает новую запись в таблице, используя текущие значения. Если есть поле (или поля) типа Sprig_Field_Timestamp со свойством$auto_now_create == TRUE
, то для него значением текущая метка времени. Далее автоматически будет вызвана валидация данных, так что вызовы методаcreate()
, а также (забегаю вперед) и методаupdate()
, должны заворачиваться вtry ... catch()
, но об этом ниже, в секции «Валидация». Метод возвращает текущий объект ($this
). - Метод
update()
работает только с измененными данными (свойство $_changed). Кроме того, поля Sprig_Field_Timestamp со свойством$auto_now_update == TRUE
будут заполнены текущей меткой времени. В целом все аналогично созданию записи — валидация измененных значений с дальнейшим сохранением в базе. - Метод
delete()
предназначен для удаления записей, и может быть использован в различных случаях. Если запись была загружена из БД, она будет удалена. Если же объект только что создан и не сохранен в БД, введенные в него значения будут использованы в качестве условия для удаления всех подходящих записей в БД. Если модель пустая, ничего не будет удалено (интересно, как тогда удалить все записи, т.к. методаdelete_all()
нет).
Настройка формата значений при выводе на экран
Приятная «вкусность». Для каждого поля можно менять форматирование значения, для чего используется метод verbose($value)
. По умолчанию он выводит значение как строку, но есть и отличные от дефолтных методы, например для меток времени и картинок:
// Sprig_Field_Timestamp, использует свойство $format для хранения формата даты public function verbose($value) { if (is_integer($value)) { return date($this->format, $value); } else { return ''; } } // Sprig_Field_Image, выводит полный путь к картинке, используя свойство $path public function verbose($value) { return $this->path.$value; } |
Есть и другие классы с собственными обработчиками (для чисел с плавающей запятой будет произведено форматирование через number_format()
, для логических выведение строковых ‘TRUE‘ или ‘FALSE‘, а для Hasmany — вывод значений через запятую).
Работа с формами
Выбор элементов форм в зависимости от типа поля — одно из преимуществ Sprig. Каждый объект класса Sprig_Field должен реализовывать метод input($name, $value, array $attr = NULL)
. По умолчанию будет выведен либо текстовый элемент input, либо выпадающий список select (если задано свойство $choices
):
public function input($name, $value, array $attr = NULL) { // Make the value verbose $value = $this->verbose($value); if (is_array($this->choices)) { return Form::select($name, $this->choices, $value, $attr); } else { return Form::input($name, $value, $attr); } } |
Массив $attr
позволяет передать дополнительные атрибуты элемента форм (например, ‘class‘ или ‘tabindex‘). Метод возвращает код сгенерированного поля формы. Как видно, метод input()
активно использует verbose()
, так что не забывайте его менять для создаваемых Вами классов нестандартных полей.
Для того, чтобы лучше понять, как (и зачем) реализовать собственный метод для формирования контролов, приведу методы для картинок и логических полей:
// Sprig_Field_Image // Вывод поле для загрузки изображения. Если есть значение, то покажет и саму картинку public function input($name, $value, array $attr = NULL) { $input = Form::file($name, $attr); if ($this->value) { $input .= HTML::image($this->verbose()); } return $input; } // Sprig_Field_Boolean // если установлено свойство $append_label, будет добавлен текст к checkbox'у public function input($name, $value, array $attr = NULL) { $checkbox = Form::checkbox($name, 1, $value, $attr); if ($this->append_label) { $checkbox .= ' '.$this->label; } return $checkbox; } |
Также есть метод inputs($labels)
, который позволяет разом получить массив контролов. Параметр $labels
указывает, использовать ли в качестве ключей данного массива сгенерированный код для меток ($labels = TRUE
, по умолчанию), либо просто имена полей. Например, так:
foreach ($blog->inputs() as $label => $input) echo $label."<br />".$input."<br />"; |
Валидация
Раз есть генерация форм, надо как-то обрабатывать введенные данные. Для этого есть возможность установки свойств валидации, кроме того не стоит забывать об автоматически добавляемых фильтров и правил валидации для отдельных стандартных классов (например, проверка корректности email для Sprig_Field_Email). Обратимся к все той же модели Sprig_Model_User, так там объявлено поле `username` в методе _init()
:
'username' => new Sprig_Field_Char(array( 'empty' => FALSE, 'unique' => TRUE, 'rules' => array( 'regex' => array('/^[\pL_.-]+$/ui') ), |
Поскольку внутри используется стандартный класс Validate, синтаксис объявления свойств привычен. Сама проверка запускается методом check(array $data = NULL)
. Да, в Sprig можно (напрямую) передать в модель данные для валидации (тот же массив $_POST
), однако это немного другой механизм, отличный от валидации в ORM версии 2.3.4. Во время валидации сама модель не меняется, она предоставляет данные для создания объекта Validate (т.е. свойства $label
, $filters
, $rules
и $callbacks
соответствующих полей).
Если параметр
$data
не указан, будут проверяться измененные (и несохраненные) данные модели. Таким образом, проверять только что загруженный из БД объект бессмысленно.
Метод check()
возвращает массив обработанные данные в случае успеха (они могут отличаться от исходных, например из-за фильтрации). А вот если выявлено несоответствие, будет сгенерировано исключение Validate_Exception, так что придется оборачивать вызовы check()
в блоки try ... catch
:
try { $object->check(); } catch (Validate_Exception $e) { $errors = $e->array->errors('validation'); } |
Объект Validate_Exception содержит свойство
$array
, в которое помещаются данные при генерации исключения. В данном случае это сам объект Validate, так что$e->array->errors()
осуществляет запрос ошибок валидации.
Прочие полезные методы
- Получить значения объекта как массив можно с помощью метода
as_array($verbose)
, где параметр$verbose
определяет, применять ли методverbose()
на все поля, либо отдавать как есть (по умолчанию). - Проверить, менялось ли какое либо поле или значения модели в целом с момента последнего сохранения или загрузки, можно через метод
changed($field)
. Если параметр$field
не указан, вернется массив измененных полей (имя поля => значение), иначе — логическое TRUE (поле поменялось) или FALSE. - Метод
loaded()
полезен для проверки, был ли объект загружен из БД или он просто создан «на лету». - Метод
pk($table)
возвращает имя поля для первичного ключа. Если параметр $table указан, то он будет подставлен перед именем поля в качестве названия таблицы (значение TRUE используется для подстановки имени текущей таблицы модели). Также есть методtk($table)
, который возвращает т.н. «титульный» ключ, т.е. значение свойства$_title_key
. - Для формирования выпадающих списков есть знакомый нам метод
select_list($key = NULL, $value = NULL)
. Он возвращает массив$key => $value
всех записей в БД. - Метод
table()
возвращает имя таблицы модели.
Напоследок
- Я нашел зачатки поддержки составных первичных ключей (свойство
$_primary_key
может быть массивом полей), но пока это не везде работает. Возможно, в следующих коммитах ситуация прояснится. - Также не увидел как использовать напрямую методы Database_Query_Builder, например
limit()
илиwhere()
. Возможно, они появятся позже. Единственный доступный способ — передача параметра$query
(в котором можно настроить большинство необходимых условий) в методload()
.
Дополнительную информацию Вы можете получить в коротком ReadMe на странице проекта, а также в данном топике форума. Конечно, в данной статье практически ничего не сказано про использование связей в Sprig. Это будет темой следующего поста.
Спасибо, хорошая статья. Чем то мне этот модуль напоминает модели в django и это просто здорово
Корни именно из Django.
Спасибо, жду продолжения
С каждым днем все больше убеждаюсь: Kohana — культовый фреймворк.
Почему?
Да напишите вы раз, да в добавок — правильно, о том, как это надо использовать а не о том, как это работает (поверьте, у меня, как и у большинства — нету времени на изучение всего этого!), и будет все по-другому!
@Zares
Если бы все было так просто… Библиотека сложная, сравнима с ORM. Сразу после статьи по связям будет практикум, скорее всего на примере типичных взаимосвязанных объектов (блог/автор/тэги и т.д.).
great article.. I’m using Sprig right now and this article helps me a lot..
Thanks.
Спасибо BIakaVeron за интересную статью, постоянно слежу за публикациями. Теперь активно жду продложения по связям в sprig.
Вариант решения limit и offset для Sprig, и пример использования пагинации: http://forum.kohanaphp.com/comments.php?DiscussionID=4376