19.10.2011

Автоматизируем build для PHP-проекта с помощью Phing

Традиционно с процессом build связывают такие действия, как компиляция, линковка, в итоге представление в исполняемый код. Для PHP и других скриптовых языков во всем этом нет необходимости, однако, build проекта на PHP довольно частая задача. С чем это связано? Прежде всего в понятие build веб-проектов сейчас вкладывают, помимо задач вроде компиляции, такие активности, как:
  • Тестирование;
  • Проведение необходимых изменений в БД;
  • Анализ исходного кода;
  • Генерация документации;
  • Минимизация;
  • Какие-то другие специфические для проекта задачи, которые можно выполнять автоматизированно.

Для PHP-проекта фактическим стандартом для задач build'а является приложение Phing, очень похожее своим подходом на Ant для Java. Подробно изучить Phing можно с помощью документации на официальном сайте, а также будет полезна вот эта развернутая статья (кстати не только про Phing). В этом же посте я расскажу, что Phing довольно стабильно развивается с релизом примерно раз в три месяца, в заключении приведу пример build-скрипта.

Итак начнем с установки (возможностей у Phing'а много, для некоторых типов задач понадобится установить дополнительные библиотеки):
pear channel-discover pear.phing.info
pear install phing/phing

Теперь создадим простой build-скрипт build.xml в корне проекта.
<?xml version="1.0" encoding="UTF-8"?>
<project name="Phing example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default" depends="run.speech.engine">
        <echo msg="Hello world!" />
    </target>

    <!-- ============================================ -->
    <!-- Target: run.speech.engine                    -->
    <!-- ============================================ -->
    <target name="run.speech.engine">
        <echo msg="I can speak now!" />
    </target>
</project>
Выполнив команду phing (можно также запускать phing -f build.xml для указания имени запускаемого скрипта) мы получим такой вывод:
Buildfile: /home/username/build-example/build.xml

Phing example > run.speech.engine:

     [echo] I can speak now!

Phing example > default:

     [echo] Hello world!

BUILD FINISHED

Total time: 0.1666 seconds

Одной из интересных практик для написания build-скриптов является создание несколько разных файлов для разных сред. Например: build-dev.xml, build-stage.xml, build-production.xml. Это действительно имеет смысл, т.к. задачи для каждой среды исполнения разные. Дальше мы попробуем написать два скрипта: build-dev.xml и build-production.xml.

Первый скрипт для среды разработки будет решать задачи проверки кода, запуска тестов и генерации документации. Начнем с проверки кода на синтаксические ошибки, для этого нужно использовать тип задач PhpLintTask:
<?xml version="1.0" encoding="UTF-8"?>
<project name="Phing example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default" depends="lint.legacy">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: lint.legacy                          -->
    <!-- ============================================ -->
    <target name="lint.legacy">
        <phplint>
            <fileset dir="${build.dir}/application/lib">
                <include name="**/*.php" />
            </fileset>
        </phplint>
    </target>
</project>
После запуска вывод будет примерно таким:
Buildfile: /home/username/build-example/build-dev.xml

Phing example > lint.legacy:

  [phplint] ./application/lib/auth.class.php: No syntax errors detected
  [phplint] ./application/lib/session.class.php: No syntax errors detected
  [phplint] ./application/lib/video.class.php: No syntax errors detected

Phing example > default:

     [echo] All done.

BUILD FINISHED

Total time: 2.3877 seconds

Переходим к запуску тестов, и здесь можно использовать задачи PHPUnitTask или SimpleTestTask в зависимости от используемого фреймворка для тестирования.
<?xml version="1.0" encoding="UTF-8"?>
<project name="Phing example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default" depends="lint.legacy, run.tests">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: lint.legacy                          -->
    <!-- ============================================ -->
    <target name="lint.legacy">
        <phplint>
            <fileset dir="${build.dir}/application/lib">
                <include name="**/*.php" />
            </fileset>
        </phplint>
    </target>
    
    <!-- ============================================ -->
    <!-- Target: run.tests                            -->
    <!-- ============================================ -->
    <target name="run.tests">
        <phpunit haltonfailure="true" haltonerror="true">
            <formatter type="plain" usefile="false" />
            <batchtest>
                <fileset dir="${build.dir}/application/tests">
                    <include name="*Test.php" />
                </fileset>
            </batchtest>
        </phpunit>
    </target>
</project>
Последнее, что мы решили делать, это получать документацию API исходного кода. Для этого я рекомендую использовать задачу DocBloxTask.
<?xml version="1.0" encoding="UTF-8"?>
<project name="Phing example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default" depends="lint.legacy, run.tests, api.docs">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: lint.legacy                          -->
    <!-- ============================================ -->
    <target name="lint.legacy">
        <phplint>
            <fileset dir="${build.dir}/application/lib">
                <include name="**/*.php" />
            </fileset>
        </phplint>
    </target>
    
    <!-- ============================================ -->
    <!-- Target: run.tests                            -->
    <!-- ============================================ -->
    <target name="run.tests">
        <phpunit haltonfailure="true" haltonerror="true">
            <formatter type="plain" usefile="false" />
            <batchtest>
                <fileset dir="${build.dir}/application/tests">
                    <include name="*Test.php" />
                </fileset>
            </batchtest>
        </phpunit>
    </target>
    
    <!-- ============================================ -->
    <!-- Target: api.docs -->
    <!-- ============================================ -->
    <target name="api.docs">
        <docblox title="API Docs" destdir="${build.dir}/public/apidocs"
            quiet="true">
            <fileset dir="${build.dir}/application/lib">
                <include name="**/*.php" />
            </fileset>
        </docblox>
    </target>
</project>
В итоге при запуске phing -f build-dev.xml у нас получается такой вывод:
Buildfile: /home/username/build-example/build-dev.xml

Phing example > lint.legacy:
  ...

Phing example > run.tests.legacy:
  ...

Phing example > api.docs:
  ...

Phing example > default:

     [echo] All done.

BUILD FINISHED

Total time: 26.5158 seconds

Переходим к build-production.xml. Здесь задачи уже другие, нам более интересно минимизировать некоторые файлы (css, js) и объединить небольшие файлы одного типа. У нас получится примерно такой build-скрипт:
<?xml version="1.0" encoding="UTF-8"?>
<project name="Phing example" default="default">
    <property name="build.dir" value="./public" />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default"
        depends="minify.css.all, minify.js.all, concatenate.css, concatenate.js">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: minify.css.all                       -->
    <!-- ============================================ -->
    <target name="minify.css.all">
        <echo msg="Compressing CSS with YUI Compressor" />
        <fileset dir="${build.dir}/css" id="fileset.css">
            <include name="global.css" />
            <include name="blocks.css" />
        </fileset>
        <foreach param="filename" absparam="absfilename" target="minify">
            <fileset refid="fileset.css" />
        </foreach>
        <move todir="${build.dir}/css" overwrite="true">
            <mapper type="glob" from="*.css" to="*.min.css" />
            <fileset refid="fileset.css" />
        </move>
    </target>

    <!-- ============================================ -->
    <!-- Target: minify.js.all                        -->
    <!-- ============================================ -->
    <target name="minify.js.all">
        <echo msg="Compressing JS with YUI Compressor" />
        <fileset dir="${build.dir}/js" id="fileset.js">
            <include name="global.js" />
        </fileset>
        <foreach param="filename" absparam="absfilename" target="minify">
            <fileset refid="fileset.js" />
        </foreach>
        <move todir="${build.dir}/js" overwrite="true">
            <mapper type="glob" from="*.js" to="*.min.js" />
            <fileset refid="fileset.js" />
        </move>
    </target>

    <!-- ============================================ -->
    <!-- Target: minify                               -->
    <!-- ============================================ -->
    <target name="minify">
        <echo msg="Minimizing: ${filename}" />
        <exec command="java -jar yuicompressor.jar ${absfilename} -o ${absfilename}" />
    </target>

    <!-- ============================================ -->
    <!-- Target: concatenate.css                      -->
    <!-- ============================================ -->
    <target name="concatenate.css" depends="minify.css.all">
        <echo msg="Concatenating CSS" />
        <delete file="${build.dir}/css/all.min.css" />
        <append destFile="${build.dir}/css/all.min.css">
            <fileset dir="${build.dir}/css">
                <include name="global.min.css" />
                <include name="blocks.min.css" />
            </fileset>
        </append>
    </target>

    <!-- ============================================ -->
    <!-- Target: concatenate.js                       -->
    <!-- ============================================ -->
    <target name="concatenate.js" depends="minify.js.all">
        <echo msg="Concatenating JS" />
        <delete file="${build.dir}/js/all.min.js" />
        <append destFile="${build.dir}/js/all.min.js">
            <fileset dir="${build.dir}/js">
                <include name="global.min.js" />
                <include name="gallery.min.js" />
            </fileset>
        </append>
    </target>
</project>
Здесь для минимизации мы использовать YUI compressor, а для объединения файлов — директиву append. Вывод будет примерно таким:
Buildfile: /home/username/build-example/build-dev.xml

Phing example > minify.css.all:

     [echo] Compressing CSS with YUI Compressor

Phing example > minify.js.all:

     [echo] Compressing JS with YUI Compressor

Phing example > concatenate.css:

     [echo] Concatenating CSS
     ...

Phing example > concatenate.js:

     [echo] Concatenating JS
     ...

Phing example > default:

     [echo] All done.

BUILD FINISHED

Total time: 3.3199 seconds

На этом пример build для PHP проекта можно считать завершенным. Спасибо за внимание, и делитесь, для чего вы используете build в ваших проектах!

2 коммент.:

Mikhail Krestjaninoff комментирует...

В предыдущем каменте есть некоторая несогласованность - следствие творческих мук при написании камента и невозможности отредактировать сообщение :)

Евгений Хамухин комментирует...

Миша, так а предыдущий коммент ты удалил? Не против, если я его восстановлю :)

"Дефолтную цель использую для инициализации проекта после чекаута из SVN - настройка прав на каталоги, инициализация разработческого конфига и т.д.

Так же есть цель для сборки/выкладки документации (вызов doxygen).

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

Phing не умеет (не умел) полноценно работать с командами типа svn, scp, fakeroot, test, ssh, dpkg, sudo. Поэтому большая часть моих целей сотоит
Писать для всего этого расширения Phing было как0то влом, поэтому всё сводиться к банальному вызову exec. Дёшево, но сердито.

Для всего этого, конечно можно было использовать ant, bash или что-нибудь ещё. Но phing всё же как-то ближе к PHP :)"