Введение
Казалось бы, зачем благородному дону условная компиляция в php? Однако, при разработке достаточно большого web-приложения на php(+html+css+javascript+…) возникают потребности слегка модифицировать код для того или иного сервера, той или иной среды. Изредка посещают крамольные мысли – сделать код одновременно для php4 и для php5. Иногда, к примеру, хочется вставить точные размеры фоновой картинки как размеры некоей области в разметке и этими размерами загадить несколько файлов. Для отладки приложения на локальной машине требуются один комплект настроек, а для выкладывания его на целевой хостинг – другой. Как правило, для таких целей используются файлы конфигурации, в которые, с соблюдением определенной ловкости рук, упакованы все кардинальные отличия проекта, в зависимости от погодных условий.
Однако, существует и другой путь. Традиционные системы программирования имеют этап, называемый компиляцией. J Как правило, в компиляторе одновременно собирается сразу несколько «целей» проекта, для отладки, для выкладки на целевую платформу. Если внести в процесс подготовки web-проекта этот этап, условная компиляция проекта обретет смысл и начнет приносить пользу.
Итак по условию задачи, наш потенциальный web-программист знает php. Так что будет достаточно здраво, если для нужд препроцессора мы постараемся обойтись именно этим языком, тем более, что он и задумывался (если судить по одному из вариантов расшифровки PHP) как в меру универсальный язык препроцессора.
По форме - препроцессор будет представлять собой отдельный php-скрипт, который мы с присущей нам фантазией назовем preprocessor.php. Собственно вызов препроцессора будет вызовом php в режиме CLI и передача ему нужных параметров.
Итак, постановим что инструкции препроцессора будут такими же инструкциями на PHP, как и обычные, но обрамленные в asp-теги и оживим их с помощью обычной замены asp тегов на обычные и наоборот:
function evalfile($src,$dst=''){
if(!is_file($src)) return;
$s=file_get_contents($src);
$s=str_replace(
array('<?','?>','<%','%>'),
array('<@','@>','<?php','?'.'>'),$s
);
ob_start();
eval('?'.'>'.$s);
$s=ob_get_contents();
$s=str_replace(
array('<@','@>'),
array('<?','?'.'>'),$s
);
ob_end_clean();
if(!empty($dst)){
file_put_contents($dst,$s);
}
}
Функция получает имя файла, который следует «исполнить», производит в нем подмену обычных тегов PHP на затычки, а asp-like теги на нормальные PHP-теги. После чего - исполняет файл естественным образом и возвращает теги к нормальному виду. Если указан второй параметр функции - результат выполнения записывается в файл с этим именем. К сожалению, эта функция не более чем иллюстрация этого процесса. На самом деле выполнение файла препроцессором сделано несколько сложнее. Процесс выполнения разбит на 3 этапа – подготовка файла, собственно eval и пост-обработка результата с сохранением его в файле. Все эти этапы заключены в цикл, который перебирает все определенные пары файлов (источник, назначение, операция). Сложность нужна, чтобы все eval’ы выполнялись в теле одной функции и могли, таким образом, пользоваться определяемыми друг другом переменными.
При этом совершенно бесплатно препроцессор приобретает все явные возможности программирования на PHP, которых так не хватает другим языкам препроцессинга. При этом, на этапе компиляции можно забыть об очень уж эффективной реализации такой работы, так как, по большому счету, без разницы – будет проект компилироваться 1 секунду или 2.
Ближе к телу
Чтобы мы могли достаточно безболезненно «компилировать» в меру большой проект, нам нужно, чтобы контекст выполнения (комплект констант и функций) препроцессором всех файлов проекта был примерно одинаков. Можно, конечно, в каждый файл добавить некий заголовочный комплект инклудов ( как в С ), который будет выравнивать окружение, но это предполагает модификацию всех файлов проекта. А можно описать все файлы проекта в отдельном проектном файле и «выполнить» их всех в одном цикле, чтобы можно было использовать функции и константы во всех файлах проекта одновременно. Кроме явного выигрыша по времени компиляции, такой подход позволяет не менять по мелочи все файлы, и вставить препроцессор на ходу в какой-то уже работающий проект в самом разгаре его разработки. Достаточно описать конфигурацию, в которой мы расскажем препроцессору что нужно делать, какие файлы и куда и как нужно транслировать. Руководствуясь этим файлом, препроцессор откомпилирует все и разместит в нужном месте.
При вызове препроцессора мы сможем определять значения некоторых переменных через дополнительные параметры вызова скрипта, например так
php -f preprocessor.php /Ddst=build/release config.xml
Файл описания - XML файл. Вот его пример и описание:
<config>
<var name=”model”>AeroWindow </var>
<files>
<file>rev.tmp</file>
<file>AeroWindow/AeroWindow.tpl.html</file>
<file>plugins/auth/auth_ajax.tpl.php</file>
</files>
<!-- Aerowindow color scheme -->
<files dir="AeroWindow" dstdir="$dst">
<file>js/*.js</file>
<file>css/*.*</file>
<copy>images/*.*</copy>
<copy>images/WindowsAeroStyle/active/*.*</copy>
<copy>images/WindowsAeroStyle/hover/*.*</copy>
<copy>images/WindowsAeroStyle/inactive/*.*</copy>
</files>
</config>
Config – объемлющий контейнер для файла.
VAR – определение константы, которой можно пользоваться внутри любого файла в теле кода препроцессора.
Переменную $dst предполагаем определенной через дополнительный параметр при вызове скрипта
FILES – представляет собой контейнер для хранения параметров, общих для группы файлов
· destdir – каталог, в который будет копироваться результат работы препроцессора
· dir – каталог, из которого будут браться исходные тексты
FILE – контейнер файлов. Его значение – маска файлов с каталогом. Параметры можно указывать такие же как и в FILES. Все файлы, попавшие в этот контейнер будут обработаны препроцессором
COPY – контейнер файлов. Его значение – маска файлов с каталогом. Параметры можно указывать такие же как и в FILES. Все файлы, попавшие в этот контейнер будут просто скопированы в нужное место.
Так , например, первая секция FILES определяет файлы, которые не будут реально скопированы (парамер dst опущен). Они просто будут обработаны скриптом и содержащиеся в них константы и функции можно будет использовать в остальных файлах проекта. Последняя секция примера, начиная с комментария «AeroWindow color cheme», определит нам что :
· все javascript-файлы, находящиеся в каталоге AeroWindow/js будут обработаны препроцессором и скопированы в каталог $dst/js
· все файлы из каталога AeroWindow/css будут обработаны препроцессором и скопированы в каталог $dst/css
· все файлы из каталога images и 4-х подкаталогов будут скопированы в соответствующие каталоги в $dst
Макро-ООП в действии.
Отвлечемся теперь от всей этой мути и представим себе работающий web-проект. Вообразим себе, что начальнику проекта приспичило вставить в проект фенечку, использующую диалоговое окошко, описанное в некоем плагине. Это плагин предполагает, что будут добавлены некие стили в файл стилей, добавлена некая разметка в html файл, добавлены некие файлы в каталог проекта и изменен один или несколько javaScript файлов проекта. Если наша фенечка использует ajax, при этом возникнет желание поменять и php-файлы. Изменения каждый раз невелики, однако они размазаны по большому количеству файлов, уследить за ними даже с использованием системы контроля версий может быть непросто. Особенно страшно становится, когда начальник в творческих муках перебирает многие варианты таких фенечек от разных производителей.
Как наш новоявленный компилятор может облегчить нашу судьбу в этом случае? Легко!
Мысленно представим себе куда и как мы будем вставлять компоненты фенечки. В функцию, вызывающуюся в методе onload документа будет вставлен код инициализации фенечки. Вставим туда код
<% insert_point ('js_onload'); %>
В том-же файле нам нужно описать поведение фенечки. Все это мы запихаем в область файла, в которой описываются все такие функции общего назначения
//<% insert_point ('js_body'); %>
В css файл мы будем добавлять некие стили. Туда запихаем:
<% insert_point ('css_styles'); %>
В html шаблон окна приложения нужно вставить заготовку диалогового окна. Туда поместим
<% insert_point ('html_body'); %>
Вот, вроде и все, если про ajax пока забыть…
А теперь, со всем этим хозяйством начнем взлетать. Соединим все конструктивные части фенечки в одном файле, однако разделим эти конструктивные части конструкциями point_start и point_finish.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>fenechka module</title>
<script type=”text/javascript”>
<% point_start('js_body'); %>
function Fenechka(){
}
<% point_start('js_onload'); %>
Fenechka();
<% point_finish(); %>
</script>
<style type=”text/css”>
<% point_start('css_style'); %>
#fenechka { visible:none;}
<% point_finish(); %>
</style>
</head>
<body>
<% point_start('html_body'); %>
<div id="fenechka">Hello world!</div>
<% point_finish(); %>
</div>
</body>
</html>
При некоторой ловкости рук, файл с модулем может одновременно оказаться простой тестовой площадкой для проверки фенечки в отдельном окне. Впрочем, не в этом счастье.
Добавим строчку <file>fenechka.html</file> в первый блок (без сохранения результатов) нашего файла конфигурации, откомпилируем проект и незамедлительно, после исправления совсем уж явных опечаток, случится чудо! Все необходимые части нашего нового функционала вставятся в правильные места нашего проекта. Если по какой-то причине использование фенечки не понравится заказчикам, чтобы убрать ее из проекта, достаточно выкинуть одну строку конфигурации.
Реализация механизма находится в файле point.ext.php исходника.
исходники препроцессорапро файлы исходника.
preprocessor.php - главный файл, анализ командной строки, чтение файла конфигурации и вызов препроцессора
preprocessor.class.php - класс с методами препроцессора - сборка и хранение пар Source-Destination, исполнение файла препроцессором, собственно чтение и аналих файла конфигурации.
point.ext.php - набор функций, для реализации MACRO-OOP.