Five Ways to Metaprogram PHP with Phing
There are a handful of PHP development tools that I find myself using project after project. One of them is Phing, a PHP-centered build tool similar to Ant, rake, or GNU Make.
The primary purpose of Phing is to make it easy for PHP developers to write build scripts for our applications. If you are familiar with Ruby on Rails, you might compare this to rake
's usage. rake
makes it easy to manage your Rails application. You can rebuild the database, start and stop the webserver, and so on from within rake.
Phing uses a declarative XML language to create targets (commands you can run) that are composed of tasks (individual steps for each command). Phing is built entirely in PHP, and is easily extended. Even without writing a line of PHP, you can squeeze a seemingly endless number of tasks out of a Phing script.
Here are some common tasks I use Phing for in my PHP projects. The first four meet some common needs. The last one is by far the coolest, though.
- Let Phing catch your typos.
- Automate Your Automated Testing.
- Let Phing Write Your Documentation
- Package Your Applications
- Build Your Own Toolchain
<!--break-->
1. Let Phing Catch Your Typos
I generally configure a Phing task for checking the syntax of all of my code. When I realize that I've been editing code for a while without running any tests, I often typephing lint
, which runs a quick syntax check on all of my files.
Here's an example:
$ phing lint
Buildfile: /Users/mbutcher/Code/Fortissimo/build.xml
Fortissimo Framework > lint:
[phplint] ./Fortissimo/skel/src/core/Fortissimo/FortissimoContextDump.cmd.php: No syntax errors detected
[phplint] ./Fortissimo/skel/src/core/Fortissimo/FortissimoEcho.cmd.php: No syntax errors detected
[phplint] Parse error: syntax error, unexpected '&' in ./Fortissimo/skel/src/Fortissimo.php on line 1085
[phplint] ./Fortissimo/skel/src/index.php: No syntax errors detected
BUILD FINISHED
Total time: 0.9858 seconds
In the example above, Phing's linter scanned all PHP files in my project. It found an error in the file ./Fortissimo/skel/src/Fortissimo.php
and reported it. The Phing task definition (found in the application's build.xml file) looks like this:
<target name="lint" description="Check syntax of source.">
<phplint>
<fileset refid="sourcecode" />
</phplint>
</target>
This simply defines a task named lint
and tells Phing to run the built-in phplint
task on all files that I have labeled as source code.
More often than explicitly calling it, I configure Phing to run lint before it runs other, more complex tasks. For example, I have the lint task executed before unit testing, documentation, and packaging tasks. This just adds that one final layer of sanity checking onto my coding, and numerous times it has caught syntax errors that crept in at the 11th hour as a result of one of my "quick fixes".
3. Automate Your Automated Testing
As I indicated above, phing can also be used to run automated tests, and has built-in integration with the sensational PHPUnit unit testing framework for PHP. Automated testing of at least one variety (unit, functional, behavioral) is all but necessary. Well-constructed automated tests can greatly add to the quality of code, and often identify bugs or quirks in a system well before they would typically be discovered by a developer.
I use a fairly sophisticated unit testing configuration so that I can run various sorts of tests, and I can have them generate various sorts of output. For QueryPath, I use Phing to run these tests:
- A quick test that prints results to standard output.
- A complete test (with generated HTML reports) that displays the results in my editor (TextMate).
- A complete test that stores HTML reports of my test results for later examination.
- A coverage analysis that tells me how well I have written tests for my tool.
Here's an example of the quick testing functionality:
$ phing ftest
Buildfile: /Users/mbutcher/Code/QueryPath/build.xml
QueryPath > ftest:
[phpunit] Test: CssTokenTest, Run: 1, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00202 s
[phpunit] Test: QueryPathCssEventHandlerTests, Run: 40, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.14719 s
[phpunit] Test: CssEventParserTests, Run: 21, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.08020 s
[phpunit] Test: QPDBTest, Run: 2, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.08305 s
[phpunit] Test: QPListTests, Run: 2, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.03598 s
[phpunit] Test: QPTPLTest, Run: 8, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.06918 s
[phpunit] Test: QPXMLTests, Run: 3, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.04158 s
[phpunit] Test: QueryPathEntitiesTest, Run: 4, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00714 s
[phpunit] Test: QueryPathExtensionTest, Run: 100, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.43106 s
[phpunit] Test: QueryPathOptionsTest, Run: 4, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00671 s
[phpunit] Test: QueryPathTest, Run: 92, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.37399 s
[phpunit] Test: XMLishTest, Run: 2, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00398 s
[delete] Deleting: /Users/mbutcher/Code/QueryPath/test/db/qpTest.db
[delete] Deleting: /Users/mbutcher/Code/QueryPath/test/db/qpTest2.db
BUILD FINISHED
Total time: 2.3184 seconds
The output above shows Phing running all of the tests defined for QueryPath, giving a brief one-line report on the status of each test. This quick test is defined in the build.xml
like this:
<target name="ftest" description="Run a quick unit test.">
<!-- Fast test. -->
<phpunit>
<formatter type="summary" usefile="no"/>
<batchtest>
<fileset refid="unittests"/>
</batchtest>
</phpunit>
<delete file="./test/db/qpTest.db" />
<delete file="./test/db/qpTest2.db" />
</target>
The above instructs Phing to use its built-in PHPUnit task to run a batch unit test on all of the tests named in the unittests
file set. During unit testing, a couple of files are created. The last two elements in the target (the delete
tasks) remove the test files.
PHPUnit has many additional features, including performance analysis, that you can take advantage of via Phing.
3. Let Phing Write Your Documentation
Okay, Phing doesn't exactly write the documentation, but it can extract doc blocks from your source code and turn it into API documentation. In fact, it does this in conjunction with the justifiably popular PhpDocumentor tool.
One of the other important PHP packages I find myself using for just about every project is the PhpDocumentor. This provides doc-block based PHP code documentation similar to Javadoc or Doxygen. PhpDocumentor extracts this documentation and converts it into HTML (or CHM, PDF, XML, and similar formats). In addition to API documentation, it supports DocBook XML, converting these documents into HTML and interlinking them with the API docs.
Phing provides support for building documentation using PhpDocumentor. You can see an example of the API documentation generated from Phing + PhpDocumentor at the QueryPath API site.
A very basic configuration in the build.xml
might look something like this:
<target name="doc" depends="lint,setup" description="Generate API docs.">
<phpdoc title="QueryPath ${version}"
sourcecode="yes"
destdir="${docsdir}"
output="HTML:frames:DOM/phphtmllib"
quiet="true"
>
<fileset refid="sourcecode"/>
</phpdoc>
</target>
The above simply runs PHPDocumentor on all of the files in the sourcecode
fileset. The generated HTML (or PDF, or CHM) files provide formatted and navigable API documentation.
Going beyond basic documentation, you can configure Phing to do more advanced pre-processing (or post-processing) of your documentation, too. Select only a certain subset of your files to document. Add DocBook documentation. Replace tokens in the documentation on-the-fly, or use regular expressions to dynamically change the documentation as it is generated.
4. Package Your Applications
Another frequently needed task that can be handled easily in Phing is the process of packaging up your application.
The process of defining the package is a little more tedious, and for that reason I won't go into detail. The QueryPath and Fortissimo projects both have complete examples of such a task.
But the basic components are the same:
- Do any interaction with your version control system (SVN is supported by default).
- Tag your release
- Copy the files to a build directory
- Prepare the files for distribution
- Package them into a single file
All of these steps are readily accomplished by Phing. The last one deserves a quick elaboration.
Phing can package files into Tar (and Gzipped) archives. It can also create Zip archives. Along with these, though, it is capable of producing PEAR packages. PEAR is the distribution format currently favored by the PHP project. A PEAR package can be retrieved from a server using the pear
tool and installed directly into the PHP library.
Using a well-crafted Phing script, you can automate the entire process of building and packaging your project, preparing it for distribution.
5. Build Your Own Toolchain
Rails' use of rake was one of my big infatuation points with that framework. I've heard PHP users either (a) lament the missing analogous functionality in PHP, or (b) claim to be writing their own home-grown version (see Drupal's Drush project for an example). Both of these responses point to the same unfortunate fact, though: Too few people have heard of (and had the opportunity to use) Phing.
But just as Rails uses rake
do build a database, and Java tools use ant
to load an application onto a server, Phing can be leveraged to provide all kinds of additional functionality. And with a little careful construction, build.xml
files can be strong and sustainable tools for keeping your application development process under control.
Here are a few other examples of ways I have used Phing to turn it into a key toolchain component a la rake or ant:
- Adding special features to my coding environment. I use TextMate, whose toolchain can be extended using shell scripts and other UNIX tools. I have configured TextMate so that I can run Phing from within TextMate, allowing me to automated code processing.
- Framing new applications. A little creative tweaking of your
build.xml
file can turn Phing into a code generation tool to help scaffold applications quickly. - Tailoring existing applications. One application I work with frequently is Drupal. The core Drupal distribution comes with a variety of themes and modules that I don't want, and it lacks some that I invariably need. I use Phing to build myself a custom version of Drupal. And with my Phing script, I can re-run it each time that Drupal issues a new release.
- Creating a quick-and-dirty command line interface to your application. Phing provides all of the bells and whistles necessary for building a useful command line tool. And writing your own tasks is as easy as extending a simple Phing base class. I've whipped up simple command line tools in ten minutes using Phing.
I'm so excited about this last one that I will likely write another article to exhibit how easy this is.