Template Toolkit: Руководство: Внутренности

Template Toolkit

(русская редакция)

[ Пособия ] [ Руководство ] [ Модули ] [ Библиотеки ] [ Утилиты ] [ Вопросы ] [ Релиз ] [ Perl-ресурсы ] Форум ]
 
Поиск
Template Toolkit | Руководство | Внутренности

Внутренности

[ ◄ Плагины ] [ Представления ► ]
Внутренняя архитектура Template Toolkit.

Оглавление

ОПИСАНИЕ

Индекс ] [ Руководство ] [ Наверх ]

Этот документ содержит обзор внутренней архитектуры Template Toolkit. Работа над документом продолжается и еще далека от завершения, и в настоящий момент в нем содержится немногим более обзора того, как основные компоненты взаимодействуют друг с другом. Тем не менее, это неплохая отправная точка для тех, кто желает порыться в исходниках и узнать как это все работает.

Двигаясь внутрь

Модуль Template - просто фронтенд, который создает и использует Template::Service и передает вывод туда, куда вы хотите (по умолчанию STDOUT, или может быть файл, скаляр и т.д.). Модуль Apache::Template (доступный отдельно на CPAN) - это другой фронтенд, который создает объект Template::Service::Apache, вызывает его при необходимости и отправляет вывод обратно в подходящий объект Apache::Request.

Эти фронтенд-модули существуют только для работы со специфичным окружением, в котором они используются. Например, фронтенд Apache::Template работает со спецификой Apache::Request и конфигурацией, переданной через httpd.conf. Обычный фронтенд Template имеет дело со STDOUT, ссылками на переменные и т.п. С другой стороны существует Template::Service (или подкласс), который выполняет всю работу.

Модуль Template::Service предоставляет высококачественную службу доставки шаблонов с колокольчиками, свисточками, сертификатом качества и 30-дневной гарантией возврата денег в случае предъявления претензии. Наш девиз: "Всегда проводите время с пользой".

На нижнем уровне Template Toolkit существует масса мелочей, о которых мы обычно не хотим беспокоится, такие как не найденные шаблоны, ошибка при разборе, возбуждение неперехваченных исключений, пропущенные модули плагинов или зависимостей и т.д. Template::Service прячет все это и делает все выглядящим просто. Он предоставляет такие дополнительные возможности как PRE_PROCESS, PROCESS и POST_PROCESS, и также содержит механизм нейтрализации ошибок через ERROR. Вы просите его обработать шаблон и он берет на себя ответственность за всю работу. Модуль Template::Service::Apache идет немного дальше, добавляя некоторые дополнительные заголовки в Apache::Request, устанавливая несколько дополнительных переменных шаблона и т.д.

По большей части, работа сервиса заключается в планировании и управлении процессом. Он получает запрос в виде вызова своего метода process() и планирует поочередную обработку указанного в аргументах шаблона, и возможно нескольких других шаблонов (PRE_PROCESS, и т.п.). Сам он не обрабатывает шаблоны, а вместо этого делает вызов метода process() объекта Template::Context.

Template::Context - движок времени исполнения Template Toolkit, который связывает все воедино на низком уровне Template Toolkit, и который делает большую часть основной работы хотя и путем делегирования различным другим вспомогательным модулям.

Метод контекста process() должен загрузить и скомпилировать переданный ему по имени шаблон (или возможно ссылку на скаляр или дескриптор файла), или получить закешированную копию предварительно скомпилированного шаблона, соответсвующего имени. Он выполняет это, вызывая поочередно один или несколько объектов Template::Provider (список LOAD_TEMPLATES), которые в свою очередь могут привлекать модуль Template::Parser, чтобы преобразовать исходный шаблон в исполняемый perl-код (подробнее об этом дальше). К счастью, все эти трудности скрыты за простым методом template(). Вы вызываете его, передавая имя шаблона в качестве аргумента, а он возвращает скомпилированный шаблон в виде объекта Template::Document, или в противном случае возбуждает исключение.

Template::Document - легкая объектная обертка вокруг скомпилированной функции шаблона. Объект предоставляет метод process(), который выполняет небольшие подготовительные действия и затем вызывает функцию шаблона. Объект также определяет метаданные шаблона (определенные в директивах '[% META ... %]') и имеет метод block(), который возвращает хеш дополнительных определений '[% BLOCK xxxx %]', найденных в исходнике шаблона.

Таким образом, контекст получает скомпилированный документ через свой собственный метод template() и после этого готов обработать его. Первым делом он обновляет хранилище (stash) (очень коротко - место, где определяются переменные шаблона), чтобы установить любые определения переменных шаблона, переданные во втором аргументе в виде ссылки на хеш. Затем он вызывает метод документа process(), передавая в качестве аргумента самого себя как ссылку на объект контекста. Делая это, он предоставляет возможность делать обратные вызовы в коде шаблона, чтобы получить доступ к ресурсам времени исполнения и функциональности Template Toolkit.

Что мы пытаемся сказать этим: объект Template::Context вызывается не только извне через вызовы метода process() объекта Template в пользовательском коде, но он также вызывается изнутри через директивы шаблона вида '[% PROCESS template %]'.

Прежде чем двигаться далее, приведем простую структурную диаграмму, показывающую движение от внешних слоев Template Toolkit вглубь с комментариями в виде символического кода, показывающего типичную последовательность вызовов.

     ,--------.
     | Caller |	    use Template;
     `--------'     my $tt = Template->new( ... );
          |	    $tt->process($template, \%vars);
          |                                                     Снаружи
  - - - - | - - - - - - - - - - - - - - - - - - - - - - - - - - - - T T
	  |         package Template;                            Внутри
          V
    +----------+    sub process($template, \%vars) {
    | Template |	$out = $self->SERVICE->process($template, $vars);
    +----------+	print $out or send it to $self->OUTPUT;
          |	    }
          |
          |         package Template::Service;
          |
	  |	    sub process($template, \%vars) {
	  |		try {
    +----------+	    foreach $p in @self->PRE_PROCESS
    | Service  |	        $self->CONTEXT->process($p, $vars);
    +----------+
	  |		    $self->CONTEXT->process($template, $vars);
          |
	  |		    foreach $p @self->POST_PROCESS
	  |			$self->CONTEXT->process($p, $vars);
	  |		}
          |  		catch {
	  |		    $self->CONTEXT->process($self->ERROR);
	  |		}
	  |	    }
          |
          V         package Template::Context;
    +----------+
    | Context  |    sub process($template, \%vars) {
    +----------+	# получить скомпилированный шаблон
	  |		$template = $self->template($template)
          |             # обновить хранилище (stash)
          |	        $self->STASH->update($vars);
          |	        # обработать шаблон
          |             $template->process($self)
          |         }
          V
    +----------+    package Template::Document;
    | Document |
    +----------+    sub process($context) {
			$output = &{ $self->BLOCK }($context);
		    }
        

Двигаясь изнутри

Чтобы понять больше о том что происходит на этих нижних уровнях, нам нужно посмотреть как выглядит скомпилированный шаблон. На самом деле, скомпилированный шаблон - это обычная функция Perl. Ниже приведена очень простая.

    sub my_compiled_template {
	return "This is a compiled template.\n";
    }

Вы не встретите такой простой скомпилированный шаблон, только если не напишите его сами, но он совершенно правильный. Все что требуется от функции шаблона, это вернуть какой-нибудь результат (который разумеется может быть и пустым). Если по какой-то причине это нельзя сделать, функция должна возбудить исключение через die().

    sub my_todo_template {
	die "This template not yet implemented\n";
    }

Чтобы быть более информативной, функция может возбудить ошибку в виде объекта Template::Exception. Объект исключения - это просто удобная обертка для полей 'type' и 'info'.

    sub my_solilique_template {
	die (Template::Exception->new('yorrick', 'Fellow of infinite jest'));
    }

Шаблоны, в общем случае, делают намного больше, чем просто генерируют статический вывод или возбуждают исключения. Они могут получать значения переменных, обрабатывать другие шаблоны, загружать плагины, запускать фильтры и т.д. Когда бы ни вызывалась функция шаблона, она получает по ссылке объект Template::Context. Через этот объект контекста код шаблона получает доступ к возможностям Template Toolkit.

Ранее мы описали как объект Template::Service вызывает Template::Context, чтобы обрабатывать запросы process() снаружи. Можно делать аналогичные запросы к контексту на обработку шаблона, но из кода другого шаблона. Это вызов изнутри.

    sub my_process_template {
	my $context = shift;
	my $output = $context->process('header', { title => 'Hello World' })
		   . "\nsome content\n"
		   . $context->process('footer');
    }

Это примерно эквивалентно такому исходному тексту шаблона:

    [% PROCESS header
	title = 'Hello World'
    %]
    some content
    [% PROCESS footer %]

Объект Template::Stash хранит и управляет переменными шаблона. Это приведенный (blessed) хеш, в котором определены переменные шаблона. Объект-обертка предоставляет методы get() и set(), которые реализуют все возможности.магических.переменных Template Toolkit.

Каждый объект контекста имеет собственное хранилище (stash), ссылку на которое возвращает названный подобающим образом метод stash(). Так чтобы вывести значение некоторой переменной шаблона, или, например, представить следующий код шаблона:

    <title>[% title %]</title>

нам нужно примерно такое определение функции:

    sub {
	my $context = shift;
	my $stash = $context->stash();
	return '<title>' . $stash->get('title') . '</title>';
    }

Метод хранилища get() скрывает детали лежащих в основе типов переменных, автоматически вызывая функции по ссылкам, проверяя возвращаемые значения и осуществляя другие подобные трюки. Если случится, что 'title' будет связан с функцией, то мы можем указать дополнительные параметры в виде ссылки на массив, передав его вторым аргументом get().

    [% title('The Cat Sat on the Mat') %]

Это транслируется с следующий вызов метода хранилища get():

    $stash->get([ 'title', ['The Cat Sat on the Mat'] ]);

Структурированные точечные переменные можно запрашивать передавая методу get() ссылку на единственный массив вместо имени переменной. Каждая пара элементов в массиве должна состоять из имени переменной и ссылки на список аргументов для каждого отделенного точкой элемента переменной.

    [% foo(1, 2).bar(3, 4).baz(5) %]

эквивалентно

    $stash->get([ foo => [1,2], bar => [3,4], baz => [5] ]);

Если у элемента нет никаких аргументов, можно указать пустое значение, ноль или пустой список.

    [% foo.bar %]
    $stash->get([ 'foo', 0, 'bar', 0 ]);

Метод set() работает схожим образом. Он принимает имя переменной и значение переменной, которое необходимо присвоить.

    [% x = 10 %]
    $stash->set('x', 10);
    [% x.y = 10 %]
    $stash->set([ 'x', 0, 'y', 0 ], 10);

Таким образом, хранилище дает доступ к переменным шаблона и контекст предоставляет функциональность более высокого уровня. Недалеко от метода process() находится метод include(). Как и у директив PROCESS / INCLUDE, ключевое различие этих методов заключается в локализации переменных. Перед обработкой шаблона, метод process() просто обновляет хранилище, установливая новые значения переменных, перезаписывая любые существующие. В противоположность ему метод include() создает копию существующего хранилища и затем использует ее для временного хранения переменных. Любые существовавшие ранее переменные остаются определенными, но любые изменения сделанные в них, включая установку новых значений, переданных в качестве аргументов, окажут влияние только на локальную копию хранилища (хотя отметим, что это поверхностная, а поэтому ненадежная копия). После обработки шаблона метод include() восстанавливает предыдущее состояние переменной убирая копию хранилища.

Контекст также предоставляет метод insert(), реализующий директиву INSERT, но не имеет метода wrapper(). Эта функциональность может быть реализована переписыванием perl-кода и вызовом include().

    [% WRAPPER foo -%]
       blah blah [% x %]
    [%- END %]
    $context->include('foo', {
	content => 'blah blah ' . $stash->get('x'),
    });

Помимо методов для обработки шаблонов process(), include() и insert() контекст определяет методы для получения объектов плагинов, plugin(), и фильтров, filter().

    [% USE foo = Bar(10) %]
    $stash->set('foo', $context->plugin('Bar', [10]));
    [% FILTER bar(20) %]
       blah blah blah
    [% END %]
    my $filter = $context->filter('bar', [20]);
    &$filter('blah blah blah');

Значительная часть остального, что вы захотите делать в шаблоне, может быть сделано в perl-коде. Директивы IF, UNLESS, FOREACH и т.д. имеют имеют прямые аналоги в Perl.

    [% IF msg %]
       Message: [% msg %]
    [% END %];
    if ($stash->get('msg')) {
	$output .=  'Message: ';
	$output .= $stash->get('msg');
    }

Лучший способ получить большее представление о том, что происходит под капотом, - установить флаг '$Template::Parser::DEBUG' в истинное значение и начать обработку шаблонов. Это приведет к тому, что парсер будет выводить сгенерированный perl-код каждого компилируемого им шаблона на STDERR. Возможно вы также захотите установить опцию '$Template::Directive::PRETTY', чтобы получить удобный для чтения человеком код.

    use Template;
    use Template::Parser;
    use Template::Directive;

    $Template::Parser::DEBUG = 1;
    $Template::Directive::PRETTY = 1;

    my $template = Template->new();
    $template->process(\*DATA, { cat => 'dog', mat => 'log' });

    __DATA__
    The [% cat %] sat on the [% mat %]

Вывод, отправленный на STDOUT, останется таким, как и следует ожидать:

    The dog sat on the log

Вывод, отправленный на STDERR, будет выглядеть примерно так:

    compiled main template document block:
    sub {
    	my $context = shift || die "template sub called without context\n";
    	my $stash   = $context->stash;
    	my $output  = '';
    	my $error;

    	eval { BLOCK: {
    	    $output .=  "The ";
    	    $output .=  $stash->get('cat');
    	    $output .=  " sat on the ";
    	    $output .=  $stash->get('mat');
    	    $output .=  "\n";
    	} };
    	if ($@) {
    	    $error = $context->catch($@, \$output);
    	    die $error unless $error->type eq 'return';
    	}

    	return $output;
    }

ВНЕСЕНИЕ ИЗМЕНЕНИЙ В TEMPLATE TOOLKIT

Индекс ] [ Руководство ] [ Наверх ]

Вы можете свободно изменять Template Toolkit. Если вы найдете ошибку, которую следует исправить, если у вас есть чувство, что чего-то не хватает, если вы хотите взяться за что-то из списка TODO, тогда, конечно, займитесь этим и сделайте!

Если вы продумываете что-то нетривиальное, тогда возможно вы захотите предварительно обсудить это в листе рассылки, чтобы узнать текущее состояние этого вопроса, выяснить не занимается ли уже кто-нибудь этим и т.д.

Когда вы начнете изменять Template Toolkit, пожалуйста, убедитесь, что работаете с последним релизом разработки. Стабильные версии загружаются на CPAN, и имеют цифровые номера версий, например, 2.04, 2.05. Релизы разработки доступны на сайте Template Toolkit и имеют символьный суффикс в номере версии, например, 2.04a, 2.04b и т.д.

Сделав свои изменения, пожалуйста, не забудьте обновить набор тестов, добавив дополнительные тесты в один из существующих скриптов-тестов в подкаталоге 't', или добавив свой новый скрипт-тест. И, конечно, запустите 'make test', чтобы убедиться, что все тесты проходят с вашим новым кодом.

Не забывайте, что любые добавленные вами файлы должны быть включены в MANIFEST. Запуск 'make manifest' сделает это за вас, но вам нужно убедиться, что рядом нет никаких других временных файлов, которые тоже могут попасть в список.

Документацией часто пренебрегают, однако она важна не менее чем код. Если вы обновляете существующую документацию, вам нужно загрузить пакет 'docsrc', из которого собирается вся документация Template Toolkit и внести ваши изменения в нем. Этот пакет также доступен на сайте Template Toolkit. Более подробную информацию можно найти в содержащемся в архиве файле README.

Если вы добавляете новый модуль, например, модуль-плагин, то включение в этот модуль POD документации - это хорошо, но, пожалуйста, размещайте ее всю одним блоком в конце файла, после кода (так как это сделано например в любом другом модуле Template::*). Я понимаю, что это вопрос убеждений, но у меня стойкое отвращение к POD-документации разбросанной внутри кода. Мое скромное мнение - это делает сложночитаемыми как код, так и документацию (проблема того же свойства, что и внедренный в HTML perl-код).

Отбросив в сторону эстетику, если я захочу извлечь документацию и поместить ее в пакет docsrc, то мне будет проще, если вся она будет написана куском, и крайне неудобно в проивном случае. То есть, хотя бы из практических соображений, пожалуйста, держите Perl и POD разделы отдельно. Разумеется, блоки комментариев в коде только приветствуются.

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

Следующий пример показывает типичную процедуру хака. Вначале мы распаковываем последний релиз разработки.

    $ tar zxf Template-Toolkit-2.05c.tar.gz

На этом этапе, неплохо переименовать каталог, чтобы как-то обозначить что он содержит.

    $ mv Template-Toolkit-2.05c Template-Toolkit-2.05c-abw-xyz-hack

Затем вносим изменения!

    $ cd Template-Toolkit-2.05c-abw-xyz-hack
      [ меняем ]
    $ cd ..

Когда все сделано и готово к изготовлению патча, распаковываем архив дистрибутива снова, чтобы получить оригинал для сравнения с новым кодом.

    $ tar zxf Template-Toolkit-2.05c.tar.gz

У вас теперь должно быть рядом два каталога: один с оригиналом дистрибутива, второй с модифицированной версией.

    $ ls
    Template-Toolkit-2.05c  Template-Toolkit-2.05c-abw-xyz-hack

Теперь запускаем diff и сохраняем вывод в соответствующим образом названный файл патча.

    $ diff -Naur Template-Toolkit-2.05c Template-Toolkit-2.05c-abw-xyz-hack > patch-TT205c-abw-xyz-hack

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

Если вы хотите наложить чей-то патч, вам нужно начать с тем же исходным дистрибутивом, на котором основан патч. Из корня дистрибутива запустите 'patch', скормив ему на стандартный вход файл патча. Опция 'p1' необходима, чтобы обрезать первый элемент в пути (например, Template-Toolkit-2.05c/README станет README, который является правильным путем к файлу).

    $ tar zxf Template-Toolkit-2.05c.tar.gz
    $ cd Template-Toolkit-2.05c
    $ patch -p1 < ../patch-TT205c-abw-xyz-hack

Вывод утилиты 'patch' будет выглядеть примерно так:

    patching file README
    patching file lib/Template.pm
    patching file lib/Template/Provider.pm
    patching file t/provider.t

АВТОР

Индекс ] [ Руководство ] [ Наверх ]

Энди Уардли (Andy Wardley <abw@andywardley.com>)

http://www.andywardley.com/

ВЕРСИЯ

Индекс ] [ Руководство ] [ Наверх ]

Template Toolkit версия 2.14, дата релиза - 4 октября 2004.

АВТОРСКИЕ ПРАВА

Индекс ] [ Руководство ] [ Наверх ]

  Copyright (C) 1996-2004 Andy Wardley.  All Rights Reserved.
  Copyright (C) 1998-2002 Canon Research Centre Europe Ltd.

Этот модуль является свободно-распространяемым программным обеспечением; вы можете распространять и/или модифицировать его на тех же условиях, что и Perl.