Где взять. Как скачать?

http://ksnk.github.com/preprocessor/preprocessor.tar.gz - здесь можно скачать последнюю актуальную версию.

Сам проект находится здесь - https://github.com/Ksnk/preprocessor

Оглавление

Условная компиляция в PHP. Фантастику в жизнь

Введение или зачем оно нужно

Казалось бы, зачем благородному дону условная компиляция в php?

Однако, при создании web-проектов зачастую не хватает этапа "компиляции" скриптов проекта. К примеру, в шапки всех текстов, выкладываемых на целевой сервер неплохо бы вставить информацию о проекте, ревизии SVN и какую-нибудь сопроводительную муть. При создании ajax приложения возникает настойчивое желание спрятать в javascript-строку формочки и стили, созданные в обычном html редакторе.

При рисовании какой-нибудь заковыристой формочки, основанной на картинке, размеры этой картинки "гармонично" вливаются в javascript, css и разметку. Отсутствие констант в CSS служит источником вдохновения для создателей SASS...

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

Изредка посещают крамольные мысли – сделать код одновременно для php4 и для php5.

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

Однако решение некоторых этих проблем уже есть. Нужно ввести дополнительный язык препроцессора и научится с ним работать...

Как оно работает?

Web-программист - специалист по PHP и достаточно логично, что язык препроцессора будет тем же PHP. Тем более, что сам PHP и есть, с какой-то точки зрения, достаточно универсальный язык препроцессора. При этом совершенно бесплатно препроцессор приобретает всю могутность PHP, которой так не хватает другим языкам препроцессинга. Вот только теги препроцессора должны отличатся от тегов PHP, чтобы не возникало недоразумений. Пусть они будут asp-like <% %>. Чтобы "выполнить" такой текст - достаточно заменить все PHP теги на что-то другое, поменять asp-like на php теги и исполнить их обычным eval'ом. После этого - вернуть php теги взад. It's easy... Хотя, чтобы сделать эту простую идею еще и полезной - нужно ее усложнить и понять, что же обычно требуется для сборки проекта.

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

Часто приходится собирать несколько "целей" - комплектов файлов для разных условий содержания. Для отладки на локальном сервере, для отладки на целевом сервере, для выкладывания конечному клиенту. Так что наш проект - это комплект файлов, которые берутся из разных мест с девелоперской машины и помещаются в определенные каталоги "билда". Различать варианты сборок будем по имени "цели", которую будем хранить в макропеременной $target.

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

Сам препроцессор будет набором скриптов на php и будет запускаться в CLI режиме. Достаточно несложно включить такой вариант использования в ant/phing-сборщик или просто в батник, для хардкорщиков. При запуске скрипта мы передадим ему параметры

/usr/local/php5/php.exe -f /preprocessor/preprocessor.php /Dtarget=release /Ddst=build/$target config.xml

Имя config.xml не является обязательно именно таким, однако для определенности, всегда в этом описании это имя будет обозначать файл, конфигурации, переданный на обработку препроцессору.

параметр /D - описание переменных c именами target и dst соответственно. Потом эти переменные можно будет использовать.

Все переменные окружения доступны в скрипте с префиксом env_.

set common=/project

макропеременная будет доступна с именем $env_common

В результате выполнения, появится комплект файлов, модифицированный для того или иного варианта сборки. Время модификации файлов будет скопировано (touch) из времени модификации исходных файлов. Для файлов, описанных в секции copy или file, которые не содержат тегов препроцессора, время будет браться из исходного файла. Для файлов, собранных препроцессором - время будет максимальным из времен всех "исходных" файлов, включая config.xml.

если "время" исходных файлов осталось не больше, чем время готового файла - собственно копирования и обработки файла не производится, что существенно ускоряет работу с большими объемами файлов проекта.

диагностика ошибок выполнения eval'уируемого кода проверена на php 5.2.8.8 Мне она представляется странной, однако, вроде работает ;)

Описание файла конфигурации

Для того, чтобы сообщить препроцессору какие файлы входят в проект - создадим xml файл (config.xml). Вот такого, примерно, вида

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <var name="license"><![CDATA[
----------------------------------------------------------------------------
License GNU/LGPL - Serge Koriakin - June 2010
http://forum.vingrad.ru/users/ksnk
----------------------------------------------------------------------------
]]></var>
    <files>
        <file>rev.tmp</file>
    </files>
    <files dir="../cms/plugins">
        <file>altname.php</file>
    </files>
    <files dstdir="$dst">
        <file>index.php</file>
        <file name=".htaccess">htaccess.txt</file>
        <file>project.php</file>
        <file>*.css</file>
        <file>.htaccess</file>
        <copy>style/*.*</copy>
        <file>engine/hosts.*</file>
        <file>engine/main.*</file>
        <file>engine/project_core.*</file>
        <copy>img/*.*</copy>
        <file>js/*.*</file>
        <file>templates/*.*</file>
    </files>
    <files dstdir="$dst" dir="../debug/debug">
        <copy>Debug/HackerConsole/*.*</copy>
    </files>
    <files  dstdir="$dst">
        <copy>uploaded/*.*</copy>
    </files>
</config>

Все используемые здесь пути вычисляются относительно расположения самого config.xml

config - объемлющие скобки этого xml

var - завести макропеременную с именем name. В текстах проекта этой переменной можно пользоваться внутри тегов препроцессора. Можно пользоваться параметром default, чтобы определить переменную, которой не было присвоено значение ранее. Областью видимости переменной будет тот блок групповых тегов, в котором она описана

import - параметр NAME тега указывает на xml файл, файлы из которого будут вставлены в этот список. При этом имя XLM файла задается относительно текущего XML, а имена файлов в новом XML - относительно его самого Таким образом можно импортировать файлы из других проектов.

files - объединяет группу файлов с одинаковыми параметрами dir и dstdir, а также служит ограничителем области видимости тегов var и remove. dir - каталог, откуда будут браться файлы, dstdir - куда они будут помещаться. name - имя группы файлов. Эту группу можно исключить по имени тегом remove. depend - маски файлов через ";", по которым определяется максимальное время модификации, или depend='{время}' - явно указанное время. Все фалы билда, время модификации которых менее заданного, будут пересобраны

file - маска файлов, с которыми будет проводится "выполнение" препроцессором. Каталог перед именем файла означает, что в каталоги билда будет создана такая же структура. Кстати, здесь могут быть заданы свои параметры dir и dstdir, которые перекрывают параметры "верхнего" уровня. В дополнение может быть задан параметр name - имя файла, в который будет копироваться исходный

<files dstdir="$dst" dir="../debug/debug">
    <copy>Debug/HackerConsole/*.*</copy>
</files>

этот кусок, к примеру, означает, что все файлы ../debug/debug/Debug/HackerConsole/*.* будут размещены в каталоге $dst/Debug/HackerConsole

<file name=".htaccess">htaccess.txt</file>

означает, что файл htaccess.txt будет помещен в нужный каталог под именем .htaccess. (одна из причин такого переименования та, что Eclipse не любил показывать файлы с точкой в начале имени в списке файлов проекта, к тому же для локальной отладки файлов проекта встроенным сервером, такой файл может мешать нормальной работе.).

$dst - переменная, которая задается в командной строке.

copy - все файлы, подходящие под маску будут просто скопированы в каталог назначения без eval'а с теми же правилами по поводу каталогов.

echo - весь текст внутри тега воспринимается как содержимое файла. Если необходимо записать содержимое в файл, нужно ставить параметр NAME.

<files dstdir="$dst">
    <echo name="readme.txt"><![CDATA[<%=point('readme','wiki_txt');%>]]></echo>
</files>

remove - выкидывает из сформированного списка файлы по маске. при этом выкидываются файлы по "исходному" имени. Следует понимать, что "сокращенное" имя при формировании пары "файл - место назначения", заменяется на полное имя до исходного файла. Файл, который попадает под маску "выкидывания" не выполняется. В качестве символов для маски используются

<remove>**/chat/*</remove> - будут выкинуты все имена, находящиеся в каталоге chat. Например /project/version/chat/chat.php. но не /project/version/chat.php

<files name="tdd.debug.email_test">... </files>
<files name="debug.chat_debug">... </files>
...
<remove name="tdd.*"/>

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

Если конструкция внутри тега remove начинается и заканчивается символом тильда ~, то она (конструкция) считается регулярным выражением и непосредственно применяется ко всем именам в парах.

область действия remove ограничена тегом files, в котором он описан. Если тег встречается вне тега files, функция применяется ко всему уже сформированному списку файлов. Список файлов заполняется последовательно, по мере чтения файла конфигурации, так что после remove можно опять вставить файлы, которые предыдущим правилом уже удалялись-вставлялись.

Макро ООП.

Отвлечемся теперь от всей этой мути и представим себе работающий web-проект. Вообразим себе, что начальнику проекта приспичило вставить в проект фенечку, использующую диалоговое окошко, описанное в некоем плагине. Этот плагин предполагает, что будут добавлены некие стили в файл стилей, добавлена некая разметка в html файл, добавлены некие файлы в каталог проекта и изменен один или несколько javaScript файлов проекта. Если наша фенечка использует ajax, при этом возникнет желание поменять и php-файлы. Изменения каждый раз невелики, однако они размазаны по большому количеству файлов, уследить за ними даже с использованием системы контроля версий может быть непросто. Особенно страшно становится, когда начальник в творческих муках перебирает многие варианты таких фенечек от разных производителей.

Как наш новоявленный компилятор может облегчить нашу судьбу в этом случае? Легко!

Мысленно представим себе куда и как мы будем вставлять компоненты фенечки. В функцию, вызывающуюся в методе onload документа будет вставлен код инициализации фенечки. Вставим туда код

<%=POINT::get ('js_onload'); %>

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

//<%=POINT::get ('js_body'); %>

В css файл мы будем добавлять некие стили. Туда запихаем:

<%=POINT::get ('css_styles'); %>

В html шаблон окна приложения нужно вставить заготовку диалогового окна. Туда поместим

<%=POINT::get ('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> в первый блок (без указания параметра dir, чтобы не произошло копирование самого файла) нашего файла конфигурации, откомпилируем проект и незамедлительно, после исправления совсем уж явных опечаток, случится чудо! Все необходимые части нашего нового функционала вставятся в правильные места нашего проекта. Если по какой-то причине использование фенечки не понравится заказчикам, чтобы убрать ее из проекта, достаточно выкинуть одну строку конфигурации.

Реализация механизма находится в файле point.ext.php исходника.

Как его запускать, как его использовать.

Запуск и использование препроцессора можно посмотреть в исходниках самого препроцессора. Готовые для исполнения файлы утилиты собраны с использованием самого препроцессора.

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

Запуск препроцессора может быть выполнен с помощью phing. Для этого в нужное место в каталоге дополнений phing нужно разместить содержимое каталога build/phing.

Можно пользоваться build.bat, который включен в проект. Нужно убедиться, что в нем корректно указан путь до php-интерпретатора.

Исходные файлы препроцессора.

Исходные тексты препроцессора находятся в каталоге src. Каталог build является результатом "препроцессинга" исходных файлов. В файлах выполняется внедрение версии в help - функцию, дополнение шапок в файлах и кое-что еще... Файл readme.txt и readme.html изготовлен из этого файла с использованием markdown-html и markdown-txt фильтров.

Для создания препроцессора использовался PHPstorm с Phing’ом и система с установленным TortoiseGIT что накладывает отпечаток на используемые инструменты.

Build.xlm – make-файл для Phing’а. Он описывает пару макрокоманд – init – собственно вызов препроцессора и property – определение параметров + вызов утилиты SubWCRev для внедрения номера ревизии SVN в текст.

Config.xml – файл конфигурации препроцессора, список файлов проекта

preprocessor.php - главный файл, анализ командной строки и вызов препроцессора

preprocessor.class.php - класс с методами препроцессора - сборка и хранение пар Source-Destination, исполнение файла препроцессором, собственно чтение и анализ файла конфигурации.

point.ext.php - набор функций, для реализации MACRO-OOP.

wiki.ext.php - класс для работы с wiki-разметкой. разметка сильно упрощенная, поддерживаться, вероятно, не будет. Пользуйтесь Markdown разметкой.

markdown.filter\*.* - набор фильтров для обслуживания markdown разметки. Сейчас используется оригинальный markdown от John Gruber http://daringfireball.net/projects/markdown/ , но в планах заменить его на свой.

Некоторые правила и соглашения.

Рецепты

Отладка файла конфигурации

В некоторых случаях, файлы, указанные в config.xml, упорно не попадют в сборку. Метод внимательного просмотра кода тоже не выявляет ошибки. В этом случае оможет "отладка файла конфигурации". Запустить скрипт в режиме отладки просто. Нужно указать уровень выдачи диагностических сообщений. Для запуска из build.xml - PHING'овского файла, указать level=debug как параметр задачи preprocess.

Как вставить "шапку" в каждый файл проекта

Вот рецепт, которым я сам пользовался для SVN.

Обратите внимание, что после POINT::get(...); пропущены строки! Дело в том, что шапка занимает в получившемся файле несколько строк, так что сообщения об ошибках и варнингах окажутся смещенными на эти несколько строк в бOльшую сторону. Чтобы это не раздражало - не помешает добавить несколько пустых строк.

в файле git.hat.xml конструируется шапка в зависимости от установленной системы контроля версий. Для SVN требуется установить command-line утилиту SVN, так как стандартная установка TortuesSVN не устанавливает ее по умолчанию. То же самое с GIT. Желательно, чтобы эти утилиты были в переменной окружения PATH, иначе придется править команду запуска в файле git.hat.xml.

Внешний вид вставляемой шапки также определяется внутри файла git.hat.xml.

Как вставить название и версию в текстовую строку внутри проекта.

Если вы пользовались предыдущим рецептом со вставкой шапки, у вас должны быть проинициализированные переменные $version и $tag или $svn_revision, так что после

    define('VERSION',"<%=$trim($version).', ',$tag;%>");

у вас получится достаточно удобная, в некоторых отношениях, строчка с номером версии.

Ajax обработчик предполагает выдавать HTML клиенту.

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

При этом, в строковом изображении HTML будут удалены комментарии, удалены лишние пробелы. В Javascript и style секциях, если они есть, также будут удалены комментарии.

Вставить кусок файла в файл проекта

Если некоторый файл не пришло еще время оформлять как плагин, а пользоваться им уже хочется, можно сделать так:

Мой редактор не любит ASP-like теги и говорит, что это ошибки

При генерации текстов препроцессором автоматически удаляются пустые теги-комментарии ? если внутри только пробельные символы. Так что теги препроцессора можно вставлять прямо в /* ... */ комментариях. В некоторых случаях нужно, чтобы тег выводил значение в текст

 /* <%=POINT::get('mypoint');%>*/

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

Можно использовать вставки кода препроцессора в комментариях другого вида

// <%=POINT::get('mypoint');%>
## <%=POINT::get('mypoint');%>

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

Добавить в проект документацию в виде txt и в виде html

Можно готовить внутреннюю документацию в виде markdown разметки. Синтаксис markdown достаточно прост, но, тем не менее, хорошо подходит для документирования небольших проектов.

Дополнительным бонусом оказывается то, что markdown является одним из языков разметки, автоматически обрабатываемых github'ом.

Принцип построения документации может быть таким:

Мне нужно вставить кусок текста в комментарий

пользуйтесь фильтром 'comment' при вставке. Предположим, что в текст нужно вставить двухстрочный текст.

Часть проекта ABCDEFG,
Copyright (c) XXXX

Фильтр позволяет распознать место, в которое делается попытка вставить текст и определить как его нужно оформлять.

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

Для PHING можно указать текст конфигурации прямо в задаче. Примерно так

<target name="test captcha" description="build project">
     <preprocess  force="force">
              <config><![CDATA[
             <files dir="plugins/captcha" dstdir="$dst">
                 <file name=".htaccess">.htaccess.sample</file>
                 <file>captcha.new.php</file>
                 <file>captcha.test.php</file>
                 <copy>*.ttf</copy>
             </files>
         ]]></config>
         <param name="dst_dir" value="/calc/test.captcha"/>
         <param name="dst" value="z:/home/localhost/www/calc/test.captcha"/>
      </preprocess>
 </target>

В конце файла хочется поместить текст лицензионного соглашения

В конец файла вставляем что-то вроде вот такого

    /************************************************************************************
     *
     * <% if($target!='allinone')
        POINT::file('license','mit.licence.ru.txt');
     else
       POINT::inline('license','# License agreement

    follow <http://www.gnu.org/copyleft/lesser.html> to see a complete text of license');

         echo POINT::get('license','markdown-txt|comment') ;
    %> ***********************************************************************************
     */

Как можно заметить, заполнение точки хранения лицензии можно произвести из файла или непосредственно в тексте. Вывод точки проходит через фильтры markdown-txt - преобразует markdown - разметку в текстовый формат с выравниванием по 70 литер в строке. Затем - фильтр comment добавляет комментарии перед каждой новой строкой. Выглядит достаточно стильно.

    /************************************************************************************
     *
     * Лицензионное соглашение.
     * ========================
     *
     *     Copyright (c) 2012 Serge Koriakin <sergekoriakin@gmail.com>
     *
     * Данная  лицензия  разрешает  лицам,  получившим   копию   данного   программного
     * обеспечения и сопутствующей документации (в дальнейшем  именуемыми  «Программное
        ...
     * ТPЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТPАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,  ИМЕЮЩИМ
     * ПPИЧИНОЙ  ИЛИ  СВЯЗАННЫМ   С   ПPОГPАММНЫМ   ОБЕСПЕЧЕНИЕМ   ИЛИ   ИСПОЛЬЗОВАНИЕМ
     * ПPОГPАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПPОГPАММНЫМ ОБЕСПЕЧЕНИЕМ.
     *
     *
     ***********************************************************************************
     */