Эта страница будет полезной для разработчиков, которым необходимо создать свои собственные теги или фильтры. Она позволит понять, каким образом WACT-компилирует шаблоны. Мы подробно разберем процесс компиляции шаблонов, укажем на основные классы, участвующие в компиляции, покажем, как генерится php-код компилированных шаблонов, как отрабатывают выражения для вывода данных и т.д.
Очень глубоких деталей реализации WACT-компилятора мы все же касаться не будем, но полученных знаний будет достаточно чтобы создать новый тег или фильтр.
Мы предполагаем, что Вам знаком материал раздела "Компиляция и выполнение шаблона. Активные компоненты шаблона. Дерево контейнеров данных".
Вы уже знаете, что WACT обрабатывает шаблоны в две стадии:
На этапе компиляции шаблон собирается целиком и переводится в php-скрипт с 2-мя функциями и зоной include-ов, подключающих классы, необходимые для нормальной отработки шаблона.
После создания дерева компиляции, компилятор проходит по этому дереву и просит каждый из компонентов фазы компиляции добавить что-либо в компилируемый шаблон. В итоге мы получаем php-скрипт с 2-мя функциями:.
Во время фазы исполнения (RunTime) запускается сначала инициализирующая функция шаблона и создается еще дерево активных компонентов фазы исполнения. Элементы этого дерева - это объекты класса WactRuntimeComponent (limb/wact/src/components/WactRuntimeComponent.class.php). К любому элементу этого дерева вы можете получить доступ через API шаблонной системы. Это позволяет контролировать певедение компонентов фазы исполнения.
Между деревом фазы компиляции и деревов активных компонентов фазы исполнения есть прямая связь, однако не все элементы дерева фазы компиляции имеют соответствующий активный компонент фазы исполнения.
При рендеринге шаблона происходит выполнение второй, выполняющей, функции откомпилированного шаблона.
Схематично схему работы шаблонизатора можно представить следующим образом:
Мы подробно опишем, как происходит компиляция на примере <list:list> и <list:item> тегов.
Рассмотрим простейший пример вывода на экран произвольного списка заголовков:
<html> <body> <h1>List example</h1> <list:list id="items"> <table border="0"> <list:item> <tr> <td>{$title}</td> </tr> </list:item> </table> </list:list> </body> </html>
Теперь мы подробно покажем как WACT откомпилирует данный шаблон.
Процесс компиляции состоит из нескольких этапов:
Весь процесс компиляции управляется из класса WactCompiler (limb/wact/src/compiler/WactCompiler.class.php).
Во время компиляции шаблон анализируется и создается так называемое дерево фазы компиляции.
Это дерево состоит из элементов - нодов. Базовый класс для всех нодов дерева компиляции - WactCompilerNode, limb/wact/src/compiler/compile_tree_node/WactCompilerNode.class.php).
В это дерево в качестве элементов попадают практически все элементы шаблона:
В нашем примере мы получим следующее дерево:
Root | |-WactTextNode | |-WactListListTag | | | |-WactTextNode | | | |-WactListItemTag | | | | | |-WactTextNode | | | | | |-WactOutputExpression | | | | | |-WactTextNode | | | |-WactTextNode | |-WactTextNode
Класс WactCompileTreeNode - это основной класс ноды дерева фазы компиляции.
Каждый объект класса WactCompileTreeNode содержит следующие атрибуты:
WactCompileTreeNode содержит необходимые методы для навигации по дереву и поиску необходимых элементов:
Класс WactCompileTreeNode также содержит набор методов, которые участвуют в процессе компиляции:
Параметр $code_writer содержит код откомпилированного шаблона.
Метод WactCompileTreeNode :: generateConstructor($code_writer) перекрывается в первый раз только в классе WactRuntimeComponentTag.
Еще стоит упомянуть следующие важные методы класса WactCompileTreeNode:
WactCompileTreeNode :: getComponentRefCode() перекрыт в классе WactRuntimeComponentTag и WactCompileTreeRootNode. Методы isDataSource() и getDataSource() перекрыты в WactRuntimeComponentDatasourceTag и в WactCompileTreeRootNode.
Параметр $code_writer методов генерации класса WactCompileTreeNode - это объект класса WactCodeWriter (limb/wact/src/compiler/WactCodeWriter.class.php).
Класс WactCodeWriter содержит следующие часто используемые методы:
Класс WactCodeWriter автоматически переключает контекст с html на php. Поэтому вы можете легко смешивать вызовы writeHtml() и writePhp().
Класс WactCompilerTag - это основной класс для тегов. Он наследуется от WactCompilerTreeNode, но добавляет функционал по работе с атрибутами тегов.
Вот список наиболее значительных методов:
Вот так будет выглядеть код откомпилированного шаблона, который может быть получен после компиляции шаблона, использованного в примере (мы немного поправили его форматирование для улучшения читабельности) :
<?php require_once 'limb/wact/src/components/list/WactListComponent.class.php'; ?> <?php function tpl35fd34aefe6e79d5c94f7cd0e40644ef1($root, &$components) { $A = new WactListComponent('items'); $components['A'] = $A; $root->addChild($A); $B = new WactDatasourceRuntimeComponent('id0017'); $components['B'] = $B; $components['A']->addChild($B); } function tpl35fd34aefe6e79d5c94f7cd0e40644ef2($root, &$components) { $template = $root; ?> <html> <body> <h1 align="center">List example</h1> <?php $components['A']->rewind(); if ($components['A']->valid()) { ?> <table border="0"> <?php do { $components['B']->registerDataSource($components['A']->current()); ?> <tr> <td><?php echo htmlspecialchars($components['B']->get('title'), ENT_QUOTES); ?></td> </tr> <?php $components['A']->next(); } while ($components['A']->valid()); ?> </table> <?php } ?> </body> </html> <?php } $GLOBALS['TemplateRender'][$compiled_template_path] = 'tpl35fd34aefe6e79d5c94f7cd0e40644ef2'; $GLOBALS['TemplateConstruct'][$compiled_template_path] = 'tpl35fd34aefe6e79d5c94f7cd0e40644ef1'; ?>
Откомпилированный шаблон состоит из 4 частей:
Обратите внимание на инициализирующую функцию. Все активные компоненты, помимо регистрации в своих родительских элементах, также регистрируются в массиве $components, который также является параметром выполняющей функции. Сделано это из соображения эффективности обращения к активным компонентам внутри откомпилированным шаблонам. Код вида $components['A'] генерится в методе getComponentRefCode() класса WactRuntimeComponentTag.
Рассмотрим тег <list:list> и на его примере продемонстрируем, каким образом формируется компилированный шаблон.
Код класса WactListListTag можно найти в файле limb/wact/src/tags/list/list.tag.php:
<?php /** * The parent compile time component for lists * @tag list:list */ class WactListListTag extends WactRuntimeComponentTag { protected $runtimeIncludeFile = 'limb/wact/src/components/list/WactListComponent.class.php'; protected $runtimeComponentName = 'WactListComponent'; function preGenerate($code_writer) { parent::preGenerate($code_writer); if ($this->hasAttribute('from')) { $this->generateDereference($code_writer, $this->getAttribute('from')); } $code_writer->writePHP($this->getComponentRefCode() . '->rewind();' . "\n"); $code_writer->writePHP('if (' . $this->getComponentRefCode() . '->valid()) {' . "\n"); } function generateDereference($code_writer, $from) { $from_dbe = new WactDataBindingExpression($from, $this); $from_dbe->generatePreStatement($code_writer); $code_writer->writePHP($this->getComponentRefCode() . '->registerDataset('); $from_dbe->generateExpression($code_writer); $code_writer->writePHP(');' . "\n"); $from_dbe->generatePostStatement($code_writer); } function postGenerate($code_writer) { $code_writer->writePHP('}' . "\n"); $emptyChild = $this->findImmediateChildByClass('WactListDefaultTag'); if ($emptyChild) { $code_writer->writePHP(' else { ' . "\n"); $emptyChild->generateNow($code_writer); $code_writer->writePHP('}' . "\n"); } parent::postGenerate($code_writer); } } ?>
Разберем подробно код класса.
Итак:
class WactListListTag extends WactRuntimeComponentTag { protected $runtimeIncludeFile = 'limb/wact/src/components/list/WactListComponent.class.php'; protected $runtimeComponentName = 'WactListComponent'; [...] }
WactListListTag является потомком от WactRuntimeComponentTag, то есть тегом с активным компонентом фазы исполнения. Это приведек к генерации необходимого кода в инициализирующей функции. Защищенный атрибут класса $runtimeComponentName указывает как то, какого класса активный компонент следует создать. В итоге в откомпилированной шаблоне мы получили:
$A = new WactListComponent('items'); $components['A'] = $A; $root->addChild($A);
Защищенный атрибут класса $runtimeIncludeFile указывает компилятору шаблонов, какой файл следуют подключить. В откомпилированном шаблоне мы получили:
<?php require_once 'limb/wact/src/components/list/WactListComponent.class.php';?>
Генерация кода тегами производится в методе generate($code_writer) (об этом мы уже указывали в описании класса WactCompileTreeNode).
Расширение именно методов preGenerate и postGenerate в классе тега <list:list> в принципе не слишком обосновано и скорее всего имеет исторические корни. Тег <list:item> имеет другую реализацию - в нем все реализовано в методе generateContents.
Рассмотрим следующие строки класса WactListListTag:
function preGenerate($code_writer) { [...] $code_writer->writePHP($this->getComponentRefCode() . '->rewind();'); $code_writer->writePHP('if (' . $this->getComponentRefCode() . '->valid()) {'); } [...] function postGenerate($code_writer) { $code_writer->writePHP('}'); [...] }
Здесь в компилируемый шаблон по сути вставляется перемотка внутренного контейнера данных (итератора) на начало и условие на то, чтобы этот итератор содержал данные.
Обратите внимание на код $this→getComponentRefCode() - этот метод код, который позволяет обращаться к активному компоненту тега в откомпилированном шаблоне, для нашего примера он вернет что-то вида $components['A']
В результате в компилированном шаблоне мы получили следующий код:
<?php $components['A']->rewind(); if ($components['A']->valid()) { ?> [... откомпилированное содержимое внутри тега <list:list>] } <?php } ?>
Data Binding Expressions (DBE) - это название механизма, который позволяет WACT-шаблонам указывать, какую именно переменную из из какого контейнера данных необходимо использовать.
DBE автоматически формируются в выражениях для вывода переменных. Если вы желаете использовать DBE для значений атрибутов тегов, тогда необходимо эти значения обворачивать в DBE самостоятельно.
В <list:list> теге DBE используются для атрибута from, который позволяет указывать, откуда взять данные для списка. Побробнее об использовании from атрибута можно прочитать в разделе "Передача данных внутри шаблонов".
Код, который отвечает за атрибут from в классе WactListListTag:
function preGenerate($code_writer) { [...] if ($this->hasAttribute('from')) { $this->generateDereference($code_writer, $this->getAttribute('from')); } [...] } function generateDereference($code_writer, $from) { $from_dbe = new WactDataBindingExpression($from, $this); $from_dbe->generatePreStatement($code_writer); $code_writer->writePHP($this->getComponentRefCode() . '->registerDataset('); $from_dbe->generateExpression($code_writer); $code_writer->writePHP(');' . "\n"); $from_dbe->generatePostStatement($code_writer); }
Поясним некоторые моменты:
$from_dbe = new WactDataBindingExpression($from, $this);
Создается объект DBE класса WactDataBindingExpression. В конструктор DBE передается выражение DBE и ссылка на контекст, начиная с которого нужно искать контейнер с данными (по сути это ссылка на текущий контейнер с данными).
$from_dbe->generatePreStatement($code_writer);
Эта строка создает в компилированном шаблоне код, который получает необходимый контейнер с данными, учитывая все модификаторы и путь до переменной. Поясним на примере. Допустим у нас есть шаблон:
<list:list id="news" from='#article.author.books'> [...] </list:list>
То есть мы желаем получить стачала переменную article из корневого контейнера данных. Затем из article - переменную atuhroк и наконец - список $books, который и будет массивом данных для тега <list:list>. Так вот generatePreStatement() отвечает на все, что идет до .books. В результате в откомпилированном шаблоне мы получим:
$A1 = WactTemplate :: makeObject($root, 'article'); $A2 = WactTemplate :: makeObject($A1, 'author');
Следующие три строки регистрируют нужные нам данные в активном компоненте <list:list> тега. Другие словами они заполняют контейнер данных:
$code_writer->writePHP($this->getComponentRefCode() . '->registerDataset('); $from_dbe->generateExpression($code_writer); $code_writer->writePHP(');');
В результате в откомпилированном шаблоне мы получим:
$components['A']->registerDataset($A2->get('books'));
Ну и наконец строка:
$from_dbe->generatePostStatement($code_writer);
Вызов этого метода необходим для генерации какого завершающего кода.
Теперь кратко покажем, как генерят компилированный шаблон остальные элементы дерева компиляции.
Все, что не является WACT-тегами, выраженями или php-кодом, WACT интерпретирует как обычный текст и выдает его в компилированный шаблон «как есть», то есть внутри класса WactTextNode есть такой код:
function generateContents($code_writer) { $code_writer->writeHTML($this->contents); parent :: generateContents($code_writer); } } ?>
где $this→contents - и есть сам текст.
Все, что заключено в рамки <?php ?> интерпретируется WACT-ом как php-элемент дерева компиляции и отдается в шаблон как есть (WactPhpNode):
function generateContents($code_writer) { $code_writer->writePhp($this->contents); parent :: generateContents($code_writer); } } ?>
По умолчанию выражения формируют в компилированном шаблоне только код <?php echo …; ?>. Все остальное зависит от фильтров. Например, выражение {$title} фактически означает применение фильтра html к данным (этот фильтр применяетя по-умолчанию), а title - это простейшее DBE. В результате из {$title} в откомпилированном шаблоне получим следующее:
<?php echo htmlspecialchars($components['B']->get('title'), ENT_QUOTES); ?>
Обсуждение