====== Использование ACTIVE_RECORD в шаблонах WACT ======
Мы предполагаем, что вы используете пакет WEB_APP при работе с шаблонизатором WACT, так как именно пакет WEB_APP содержит необходимые теги, которые делают возможным тесную интерграция шаблонов и ActiveRecord. Мы также предполагаем, что вы немного разбираетесь в синтаксисе WACT-шаблонов и знаете назначение базовых тегов: для этого вполне достаточно выполнить [[limb3:ru:tutorials:basic|базовый туториал]] (некоторый материал здесь и в туториале пересекается).
Большое количество информации о WACT доступно в разделе [[limb3:ru:packages:wact|"Использование шаблонной системы WACT"]]. Некоторая информация на этой странице и в разделе о WACT пересекается, но это сделано специально. Нам кажется, что так получилось нагляднее.
В Limb3 существует 2 способа передачи данных в шаблон:
- получение данных в контроллере и ручная передача его в шаблон через View-объект,
- получение данных непосредственно в шаблоне.
Наша точка зрения состоит в том, что для большинства web-приложений второй способ предпочтительнее, так как в этом случае нам не приходится усложнять наши контроллеры или создавать дополнительну иерархию классов, которые бы занимались получением нужных данных и передачей этих данных в шаблон.
Итак, в Limb3 шаблон (WACT-шаблон если быть точнее) имеет средства для получения данных непосредственно из шаблона.
===== Как шаблон отображает данные из lmbActiveRecord. Для чего нужны getter-ы =====
Для начала немного информации, как WACT шаблоны работают с источниками данных и как это связано с lmbActiveRecord.
При использовании конструкций вида {$title} или {$^title и т.д.} шаблон обращается к текущему (или к родительскому) контейнеру данных с запросом вида $datasource->get('title'), если он поддерживает такой интерфейс доступа к данным или просто datasource['title'], если контейнер данных поддерживает только **ArrayAccess**. В качестве источника данных, таким образом, может выступать любой массив, объект, поддерживающий ArrayAccess или объект, поддерживащий метод **get()**.
lmbActiveRecord реализован таким образом, что он поддерживает и get() для:
* для тех полей, которые определены в его таблице,
* для всех полей, которые определены в классе и которые НЕ содержат в названии подчеркивание, например ($_table)
* для всех отношений вида has_one, has_many, has_many_to_many, belongs_to, many_belongs_to.
* для всех дополнительных getter-ов, реализованных в классе, например, getFullName() при запросе get('full_name')
Обратите внимание на последний пункт. Он позволяет создавать свои собственные методы для получения данных, в которых можно учитывать различные параметры, дополнительно обрабатывать данные и т.д. Пока приведем простейший пример:
class User extends lmbActiveRecord
{
function getFullName()
{
return $this->getLastName() . ' ' . $this->getName();
}
}
Теперь если в шаблоне написать:
Ваше полное имя: {$full_name}
Будет вызван метод getFullName(). Естественно, контейнером данных в данном куске шаблона должен быть объект класса User.
И еще один момент: такие конструкции как {$course.title} приводят к цепочке "разыменований", когда сначала вызывается get('course'), а потом к полученному объекты вызывается еще get('title').
===== Получение данных при помощи тега =====
При использовании пакета ACTIVE_RECORD для реализации модели самый очевидный способ для получения - это использование тега ****. Этот тег используется для вызовов статических методов ActiveRecord-ов и для передачи полученной информации в контейнеры данных WACT шаблона. Попробуем пояснить это на примере.
Допустим у нас есть классы Course и Lecture, которые мы использовани в разделе [[one_to_many|"Поддержка отношений вида один-ко-многим"]]:
class Course extends lmbActiveRecord
{
protected $_has_many = array('lectures' => array('field' => 'course_id',
'class' => 'Lecture'));
}
class Lecture extends lmbActiveRecord
{
protected $_many_belongs_to = array('course' => array('field' => 'course_id',
'class' => 'Course'));
}
Пока нам этого описания хватит.
==== Вывод списка объектов ====
Пусть нам необходимо вывести список заголовков всех курсов, тогда наш шаблон будет выглядеть следующим образом:
Атрибут тега **using** указывает на путь до класса Course(обычно такие классы кладутся в папку src/model/ проекта).
До версии WEB_APP 0.2 вместо using использовался атрибут **class_path**, например:
[...]
Атрибут **target** указывает, куда необходимо передать данные; в нашем случае это будет тег , который занимается отображением данных.
При выполнении тега на самом деле производится вызов метода lmbActiveRecord :: find($class_name). Рекомендуем вам просмотреть раздел [[find|"Поиск и сортировка объектов"]] еще раз чтобы лучше разбираться в механизмах взаимодействия шаблона и ActiveRecord-ов.
==== Вывод единичного объекта ====
Пусть теперь нам необходимо отобразить только один курс, идентификатор которого нам доступен из запроса (request):
Текущий курс: {$title}
Обратите внимание на атрибут тега **first**, который говорит тегу, что нужно передать не весь список данных, а только первый полученный объект из списка. Начиная с версии пакета WEB_APP 0.2 можно использовать также атрибут **one** вместо first.
Использование параметра **record_id** приводит к тому, что будет использован метод lmbActiveRecord :: findById();
Если бы мы не использовали параметр record_id, но все равно указали first='true', тогда это бы означало вызов метода find() вместо findById(), а затем paginate(0,1) у итератора, который был получен из метода lmbActiveRecord :: find().
В атрибуте **target** мы указали идентификатор тега - это так называемый единичный контейнер данных, в отличие от тега. Если вы забудете указать **first**, тогда шаблонизатор предпримет попытку передать весь итератор (даже если в нем будет всего 1 элемент) в и будет сгенерировано исключение во время работы шаблона.
Еще один момент важный момент, на который нужно обратить внимание. не выводит ничего из того, что находится у него внутри, если итератор был пуст. выводит свое содержимое в любом случае. В будущих версиях мы исправим это поведение или введем новый тег, который будет работать в этом отношении аналогично . А пока, если вы не уверены в том, что данные действительно придут, вставляйте в шаблон условие, например, так:
Текущий курс: {$title}
То есть, если контейнер с данными () содержит идентификатор, можно выводить содержимое внутри .
==== Задание сортировки данных ====
Допустим, нам необходимо вывести все курсы отсортировав их по заголовку. Если по-умолчанию курсы сортируются по идентификатору, есть способ изменить способ сортировки прямо из шаблона. Для этого можно использовать атрибут **order**. Это можно сделать так:
[...]
Или так:
[...]
Можно указать несколько сортировок, разделяя их запятыми:
[...]
Использование **order** на самом деле приводит к вызову метода sort() для итератора полученного из find()-метода, для последнего примера это будет $dataset->sort(array('title' => 'ASC', 'id' => 'DESC'));
Можно также указать "отсутствие" сортировки, то есть вывести записи в случайном порядке. Для этого (в MySQL) можно использовать значение **rand()** без указания поля сортировки, то есть:
[...]
==== Ограничение размеры выборки ====
Иногда перед нами стоит задача вывести только некоторое количество объектов в шаблоне, например, только 3 последние новости (News) (мы не будем приводить модель - покажем только шаблоны). Для этого мы можем использовать атрибуты **offset** и **limit**, например, давайте выведем только 3 новости, отсортировав их при этом по дате:
[...]
Это можно сделать и так:
[...]
Допустим нам нужно вывести 3 курса, начиная с 3-го:
[...]
Параметр offset нам,например, реально пригодился при выводе новостей из различных разделов, когда первые два раздела выводили новости одним способом, а остальные все - другим.
==== Использование своих find()-методов при помощи тега ====
Допустим, что заказчик ввел требование - на фронтовой части сайта нужно выводить только опубликованные курсы. Для этого мы введем статический метод Couse :: findPublished():
class Course extends lmbActiveRecord
{
protected $_has_many = array('lectures' => array('field' => 'course_id',
'class' => 'Lecture'));
static function findPublished()
{
return lmbActiveRecord :: find('Course', 'is_published = 1');
}
}
Теперь нам нужно модифицировать шаблон, чтобы он использовал именно этот метод findPublished(), а не стандартный find(). Для этого мы воспользуемся атрибутом **find**, в который передается "вторая" составляющая названия нужного нам метода в under_scores:
[...]
Параметр find можно также указывать при помощи тега.
==== Использование find()-методов с параметрами. Тег ====
Допустим у нас есть класс Node с таким статическим find()-методом findForParent($parent_id).
class Node extends lmbActiveRecord
{
static function findForParent($parent_id)
{
return lmbActiveRecord :: find('Node', 'parent_id = ' . (int)$parent_id);
}
}
То есть find()-метод требует дополнительного параметра. Чтобы передать параметр в find()-метод используется тег [[limb3:ru:packages:wact:tags:lmb_fetch_tags:lmb_find_params_tag|]].
Вот как будет выглядеть шаблон при вызове методе Node :: findForParent($parent_id):
[...]
Значение каждого атрибута тега передается в качестве параметра в find()-метод. Названия параметров не имею значения - важен только порядок их следования в шаблоне. Именно в этом порядке параметры и передаются.
===== Передача данных внутри шаблонов =====
Продолжим разбирать нам пример с курсами и лекциями. Допустим теперь у нас есть задача отобразить список лекций выбранного курса. Как получить в шаблон выбранный курс мы уже знаем, но как обратиться к лекциям которые к нему относятся.
==== Использование from атрибута ====
Самый простой способ - это использование атрибута **from** тега или , в зависимости от того, что нам необходимо - список объектов или только 1. Итак, вот как будет выглядеть шаблон, который отобразить выбранный курс и лекции по нему:
Текущий курс: {$title}
Лекции курса:
Обратите внимание на конструкцию . Эта конструкция приводит к вызову метода Course :: get('lectures'), который вернет коллекцию (итератор) лекций, так как именно так называется отношение один-ко-многим в классе Course.
Атрибут from можно использовать и в теге, например:
Текущая лекция: {$title}
Лекция курса: {$title}
Правда мы могли бы написать данный пример немного короче:
Текущая лекция: {$title}
Лекция курса: {$course.title}
Второй вариант, возможно, более наглядный, а с точки зрения скорости выполнения они приблизительно равны, поэтому мы предпочитаем именно второй метод.
==== Использование тега ====
Если вам необходимо отобразить, например, только 3 первые лекции курса, отсортировав их, например, по заголовку, тогда простое использование from уже не достаточно. Для таких целей можно использовать тег , например, так:
Текущий курс: {$title}
Лекции курса:
Источник данных указывается атрибутом **from**, тег, где будут отображаться данные, атрибутом **target**, остальные параметры, как нам кажется, обяснений не требуют.
Добавим еще, что в качестве значения from можно использовать составные конструкции, например, from='course.lectures' в случае, если вам необходимо вывести, какие еще есть лекции у курса, если вы отображаете выбранную лекцию.
===== Постраничный вывод данных в WACT-шаблонах =====
Если вы уже прошли [[limb3:ru:tutorials:basic|базовый туториал]], тогда вам должен быть знаком набор тегов и атрибут тега **navigator**, который указывает на то, какой тег связан с данными и разбивает их на страницы, например:
Здесь мы дополнительно укажем, лишь что атрибут **navigator** также поддерживается тегами и .
Подробнее о постраничном выводе в разделе [[limb3:ru:packages:wact:pagination|"Постраничный вывод данных в WACT-шаблонах"]].
===== Использование fetcher-ов. =====
==== Что такое fetcher-ы ====
В отдельных случаях простые find()-методы и тег без дополнительных параметров уже не справляются со своей работой. Например, у нас был класс Node, который использовался для реализации хранения дерева по алгоритму matherialized_path и для огранизации так называемых ЧПУ, а также являтся ActiveRecord-ом. У появилась необходимость выводить в шаблонах дочерние элементы какого-то родителя по определенному пути, по идентификатору, ограничивая выборки в некоторых случаях типом дочерних элементов и т.д. Все это привело к тому, что обычных find() и get() методов стало недостаточно и мы решили создать новый класс, который бы занимался подобными выборками и которому можно было бы передавать параметры выборки прямо из шаблона.
Для подобных задач в пакете **WEB_APP** существует такое понятие, как **fetcher**, которыми можно пользоваться к шаблонах через тег ****.
Два слова о том, что такое fetcher-ы. Это такие классы, наследники от lmbFetcher класса, которые поддерживают метод **getDataset()**. Этот метод всегда должен возвращать итератор.
В шаблонах fetcher-ы используются для получения данных через тег . Скажем сразу, что тег **** является аналогом тега (класс тега наследуется от класса тега ), просто он по-умолчанию использует класс limb/web_app/src/fetcher/lmbActiveRecordFetcher класс для получения данных.
fetcher-ы могут содержать различные методы, которыми их можно параметрировать. Эти методы имеют вид setSomeParam($value). По-умолчанию fetcher-ы поддерживают методы setOrder(), setLimit(), setOffset(). Различные fetcher-ы поддерживают другие методы для задания параметров, например, класс lmbActiveRecordFetcher поддерживает методы setRecordId(), setRecordIds, setFind().
В шаблонах для задания параметров fetcher-ов используется тег , каждый атрибут которого преобразуется в вызов метода вида setParamName($param_value). Мы его уже использовали часто в примерах, приводимых выше.
==== Создание своих fetcher-ов. ====
Итак, у нас есть задача - сделать свой fetcher для выборки объектов класса Node по своим параметрам. Приведем класс этого fetcher-а, который мы назвали NodeKidsFetcher:
type = $type;
}
function setParentId($parent_id)
{
if($parent_id)
$this->parent_id = $parent_id;
}
function setParentPath($path)
{
$this->path = $path;
}
protected function _createDataSet()
{
$toolkit = lmbToolkit :: instance();
if($this->path && !$this->parent_id)
{
if($node = Node :: findByPath('Node', $path))
$this->parent_id = $node->id;
}
$criteria = new lmbSQLRawCriteria("parent_id = " . (int)$this->parent_id);
if($this->type)
{
$type_id = NodeType :: generateIdFor($this->type);
$criteria->addAnd(new lmbSQLRawCriteria('type_id ='. $type_id));
}
return lmbActiveRecord :: find('Node', $criteria);
}
}
?>
Обратите внимание, что дочерние классы должны расширять защищенный метод **_createDataset()**. Это реализовано исходя из того, что родительский класс lmbFetcher содержит функционал по ограничению (offset, limit), по сортировке (order) полученного итератора и этот функционал должен быть всегда доступен клиентам fetcher-ов. Поэтому дочерние классы не перекрывают метод getDataset().
Итак, гляда на класс NodeKidsFetcher, можно сделать вывод, что в шаблонах мы можем использовать параметры parent_path, parent_id и type, например:
Атрибут **using** указывает на класс fetcher-а, при помощи которого будут получены данные.
В последнем примере, если в запросе ничего не придет, будут выбраны дочерние элементы родителя по пути /files.
Получается достаточно удобно, не так ли??
Справедливости ради отметим, что в последнее время мы предпочитаем как можно больше использовать finder-ы и getter-ы, а создавать fetcher-ы только в необходимых случаях.
Дополнительная информация о fetcher-ах в разделе [[limb3:ru:packages:wact:fetch_tags|"Использование fetcher-ов в WACT-шаблонах"]].
===== Декорирование данных в WACT-шаблонах. =====
Последний раздел, который мы разбирем в данном разделе - это декорирование. Под декорированием мы подразумеваем применение шаблона декоратор (Decorator или Wrapper) для данных, которые мы получили из какого-то источника данных.
Декораторы используются в тех случах, когда нужно к классу добавить поведение, незаметно для его клиентов и не изменяя самого класса.
==== Декораторы итераторов ====
Разбирем небольшой пример, где мы будем использовать декоратор итератора. Например, у нас есть объект навигации (Navigation), наследник lmbActiveRecord, содержащий поле с адресом страницы page_url. При выводе навигации на странице нам необходимо подсвечивать текущую страницу. Здесь можно применить декоратор (хотя есть и другие способы это реализовать).
Создадим класс HighLightDatasetDecorator:
lmb_require('limb/net/src/lmbUri.class.php');
lmb_require('limb/datasource/src/lmbPagedDatasetDecorator.class.php');
class HighLightDatasetDecorator extends lmbPagedDatasetDecorator
{
protected $path_field = 'url';
function setPathField($path_field)
{
$this->path_field = $path_field;
}
function current()
{
$record = parent :: current();
$this->_assignHighlight($record);
return $record;
}
protected function _assignHighlight($record)
{
$path = $record->get($this->path_field);
if(!$path)
return;
$compare = $this->_compareRequestUriWithRecordUri(new lmbUri($path));
if($compare === false || $compare < 0)
return;
$record->set('hightlight', 1);
}
protected function _compareRequestUriWithRecordUri($record_uri)
{
if($record_uri->getHost())
return -1;
$uri = lmbToolkit :: instance()->getRequest()->getUri();
return $uri->comparePath($record_uri);
}
}
?>
Этот класс можно параметрировать при помощи метода setPathField(). HighLightDatasetDecorator сравнивает url-ы (вернее пути) при помощи метода comparePath класса lmbUri() и при необходимости ставит в запись поле 'hightlight' со значением 1. Чуть ниже мы покажем, как это можно использовать в шаблоне.
Базовые классы для декоторов итераторов можно найти в пакете DATASOURCE.
==== Применение тегов и ====
При помощи тегов **** можно применять декораторы прямо в шаблоне, например:
{$page_title}
{$page_title}
Атрибут тега **using** указывает на класс декоратора, который будет применен к списку объектов класса Navigation.
Тег работает таким образом, что он вызывает метод setParamName($param_value) для всех агрументов тега, кроме using. В нашем случае, это приведет к вызову метода setPathField('page_url') и наш объект класса HighLightDatasetDecorator сможет использовать именно поле page_url из объектов Navigation для сравнения.
Тег **** работает аналогично тегу , с тем отличием, что он применяется внутри тега.
Дополнительно о декорировании см. раздел [[limb3:ru:packages:decorators_in_wact_templates|"Декорирование итераторов в WACT-шаблонах"]].