Контент


Ko3: модуль Sprig. Начало работы.

Библиотека 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. Это будет темой следующего поста.

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.

Теги: , , .


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

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

  1. ANT пишет:

    Спасибо, хорошая статья. Чем то мне этот модуль напоминает модели в django и это просто здорово :D

  2. BIakaVeron пишет:

    Корни именно из Django.

  3. Alexander Kupreev пишет:

    Спасибо, жду продолжения :)

  4. Zares пишет:

    С каждым днем все больше убеждаюсь: Kohana — культовый фреймворк.
    Почему?
    Да напишите вы раз, да в добавок — правильно, о том, как это надо использовать а не о том, как это работает (поверьте, у меня, как и у большинства — нету времени на изучение всего этого!), и будет все по-другому! :|

  5. BIakaVeron пишет:

    @Zares
    Если бы все было так просто… Библиотека сложная, сравнима с ORM. Сразу после статьи по связям будет практикум, скорее всего на примере типичных взаимосвязанных объектов (блог/автор/тэги и т.д.).

  6. hungmac пишет:

    great article.. I’m using Sprig right now and this article helps me a lot..

    Thanks.

  7. taggi пишет:

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

  8. boston пишет:

    Вариант решения limit и offset для Sprig, и пример использования пагинации: http://forum.kohanaphp.com/comments.php?DiscussionID=4376



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

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