До того, как я начал работать над текущим проектом, я не думал, что мне когда либо придется использовать инструменты для автоматической сборки проектов. Ведь работаю я исключительно с интерпретируемыми языками, которым не нужна компиляция. Однако, как оказалось, они могут быть полезными и при разработке на PHP, и особенно при работе с Joomla!
Раньше, работая над каким либо сайтом, я вносил необходимые правки в файлы и базу данных на своем локальном сервере, проверял все ли правильно работает, затем посредством ftp-клиента и phpMyAdmin копировал файлы и изменения базы на рабочий сайт. Если я не помнил, какие именно файлы я правил, а обычно так и случается, копировал все.
В текущем проекте работы ведутся на локальном сервере, затем правки переносятся на тестовый, и только затем на рабочий. Причем последнее действие заказчик должен иметь возможность выполнять самостоятельно. Решение для этого в Joomla есть — установочные файлы для расширений плюс упаковка всех расширений в пакет. Таким образом заказчик может обновить все расширения выполнив всего одно действие. А что же должен сделать разработчик? Для компонента надо скопировать: административную и фронтальную части, папку из директории media (раньше надо было копировать еще и языковые файлы, но используя новый стандарт их размещения вы освобождаетесь от этого). Затем надо переместить файл манифеста из административной папки в корень и все заархивировать. Еще, желательно, в имени архива указать номер версии. Когда все расширения собраны, упаковать их все и файл манифеста пакета в один архив. Вроде бы ничего сложного, но только пока расширений не много. А если их больше десятка (в моем случае 21, и их количество растет)? А что если время от времени необходимо оперативно внести правки и передать обновленный пакет?
Вот тут-то я и озадачился вопросом автоматической сборки. На тот момент я знал только об Ant. Начав изучать его возможности, узнал еще про Maven. Но меня смущало, что если вдруг мне не хватит имеющихся возможностей, придется писать на Java. Поэтому я продолжил поиски и нашел Phing — сборщик проектов для PHP. Ознакомившись с его возможностями я быстро понял, что в нем есть все необходимое:
- операции с файлами и директориями;
- работа с архивами;
- чтение свойств из xml-файлов (можно использовать данные из манифеста);
- операции для работы с системами контроля версий (cvs, svn, git);
- поддержка протоколов ftp и http;
- выполнение внешних команд;
- выполнение PHP-кода;
- запуск юнит-тестов;
- работа с базой данных;
- много других возможностей которые расширяются при помощи готовых либо самодельных плагинов на PHP.
Установка Phing
Проще всего установить Phing используя PEAR. Для этого достаточно в консоли ввести всего две команды:
pear channel-discover pear.phing.info
pear install phing/phing
Если вы не используете PEAR, то на официальном сайте есть инструкцию, как установить Phing вручную.
Подключение к NetBeans
Следующий шаг после установки — подключение к используемой IDE. Я использую NetBeans, для него существует плагин phingKing для запуска задач из IDE. Начиная с версии 7.3 его можно установить непосредственно из менеджера плагинов. В более ранних версиях придется предварительно скачать плагин и установить из файла.
После установки в разделе меню Окна появится пункт Phing Targets. В этом окне будут отображаться задачи Phing:
- Favorites — избранные задачи;
- Subtargets — подзадачи (задачи выполнение которых включено в более общую задачу);
- Default target — задача по умолчанию.
В настройках NetBeans в разделе PHP появиться закладка PhingKing в которой необходимо указать путь к Phing. В свойствах проекта в соответствующем разделе необходимо указать путь к xml-файлу с задачами (об этом чуть ниже).
Использование из командной строки
Использовать Phing из командной строки также просто. Разместите задачи в файле build.xml и запустите скрипт Phing в той же директории, либо укажете путь к xml-файлу в параметре -buildfile. Получить полный набор параметров можно запустив phing -help.
Создание задач
Задачи для Phing записываются в xml-файл (по умолчанию build.xml). По умолчанию запускается только одна задача указанная в свойствах проекта как default, но в ней можно указать зависимости от других задач, каждая из которых также может иметь свои зависимости, и все они будут выполнены. Весь проект заключается в тег project, а каждая задача в тег target.
Простейший файл задач выглядит так:
<?xml version="1.0" encoding="UTF-8"?>
<project name="test" default="build">
<target name="task1">
<echo msg="Task 1" />
</target>
<target name="task2">
<echo msg="Task 2" />
</target>
<target name="build" depends="task1, task2">
<echo msg="Build" />
</target>
</project>
В результате будет выведено:
Task 1
Task 2
Build
Сборка расширений Joomla
Теперь заставим сделать Phing что-то полезное — собрать пакет с расширениями для Joomla.
Исходные условия:
- разработка ведется в директории /opt/lampp/htdocs/mysite
- готовый пакет поместим в /home/user1/mysite
- исходные файлы расширений в /home/user1/mysite/src
- установочные файлы расширений с номерами версий в /home/user1/mysite/zip
- установочные файлы расширений без номеров версий в /home/user1/mysite/cache
Примечание: файлы с номерами версий удобно использовать как самостоятельные, а без номеров для упрощения сборки пакета.
<project name="make_project" default="build">
<!-- Пути храним в переменных, так удобнее -->
<property name="src_dir" value="/opt/lampp/htdocs/mysite" />
<property name="res_dir" value="/home/user1/mysite" />
<target name="clear">
<echo>============= Удаление старых версий =============</echo>
<!-- Команда delete удаляет указанные в ней наборы файлов fileset -->
<delete includeemptydirs="true">
<!-- Вместо ${res_dir} будет подставлено значение res_dir -->
<fileset dir="${res_dir}">
<!-- Можно перечислить файлы и/или указать маску -->
<include name="**"/>
</fileset>
</delete>
</target>
<target name="com_test">
<echo>============= Компонент Test =============</echo>
<!-- Прочитать установочный xml-файл. Префикс нужен для разграничения расширений. -->
<xmlproperty file="${src_dir}/administrator/components/com_test/test.xml" prefix="com_test." keepRoot="false" />
<!-- Копировать файлы в папку исходников компонента →
<!-- Пользовательская часть -->
<copy todir="${res_dir}/src/com_test/site" overwrite="true">
<fileset dir="${src_dir}/components/com_test">
<include name="**" />
</fileset>
</copy>
<!-- Административная часть -->
<copy todir="${res_dir}/src/com_test/admin" overwrite="true">
<fileset dir="${src_dir}/administrator/components/com_test">
<!-- Копировать все, кроме файла манифеста -->
<exclude name="${src_dir}/administrator/test.xml" />
</fileset>
</copy>
<!-- Файл манифеста копируем в корень папки расширения -->
<copy file="${src_dir}/administrator/components/com_test/test.xml" tofile="${res_dir}/src/com_test/test.xml" overwrite="true"/>
<!-- Файлы из папки media -->
<copy todir="${res_dir}/src/com_test/media" overwrite="true">
<fileset dir="${src_dir}/media/com_test">
<include name="**" />
</fileset>
</copy>
<!-- Упаковать исходники в архив -->
<zip destfile="${res_dir}/cache/com_test.zip" basedir="${res_dir}/src/com_test"/>
<!-- Создать установочный файл с номером версии. Номер прочтен из xml-файла -->
<copy file="${res_dir}/cache/com_test.zip" tofile="${res_dir}/zip/com_test-${com_test.version}.zip" overwrite="true"/>
</target>
<!-- Задание на сборку модуля. Здесь все аналогично предыдущему -->
<target name="mod_test">
<echo>============= Модуль Test =============</echo>
<xmlproperty file="${src_dir}/modules/mod_test/mod_test .xml" prefix="mod_test." keepRoot="false"/>
<copy todir="${res_dir}/src/mod_test" overwrite="true">
<fileset dir="${src_dir}/modules/mod_test">
<include name="**" />
</fileset>
</copy>
<zip destfile="${res_dir}/cache/mod_test.zip" basedir="${res_dir}/src/mod_test"/>
<copy file="${res_dir}/cache/mod_test .zip" tofile="${res_dir}/zip/mod_test-${mod_calendar.version}.zip" overwrite="true"/>
</target>
<target name="build" depends="clear, com_test, mod_test">
<echo>============= Сборка пакета =============</echo>
<!-- build/pkg_test.xml — это установочный файл пакета -->
<xmlproperty file="${src_dir}/build/pkg_test.xml" prefix="pkg." keepRoot="false" />
<copy file="${src_dir}/build/pkg_test.xml" tofile="${res_dir}/cache/pkg_test.xml" overwrite="true"/>
<zip destfile="${res_dir}/pkg_test-${pkg.version}.zip" basedir="${res_dir}/cache"/>
</target>
</project>
На первый взгляд выглядит немного устрашающе, но на самом деле здесь нет ничего сложного. Для большинства расширений код можно копировать лишь с небольшими изменениями. Да и править его приходится крайне редко. Зато всего в один клик мы получаем пакет с актуальными версиями всех разрабатываемых расширений.
Можно еще улучшить процесс добавив автоматическое увеличение номеров версий при каждой сборке, но я не стал этого делать ради совпадения номеров версий с именами файлов миграций базы данных. Если у кого-то есть соображения по этому поводу, пишите в комментариях.
Автор: marenkov