Phing is a PHP tool that allows us to automate processes -- typically, it's used for building and deploying software. It's similar in functionality to the Apache Ant project and uses XML files to execute tasks that are defined as PHP classes. Best of all, it has a tonne of cool tasks already baked in! These include tasty things such as unit testing, file system operations, code sniffer integration, SQL execution, shell commands, 3rd party services and version control integration.
Ok, that sounds good. How do I use it?
You can install Phing easily through PEAR.
$> pear channel-discover pear.phing.info
$> pear install phing/phing
The default name for a Phing build file is build.xml. This file contains a number of targets, which are like different functions that you might find in a PHP file.
<?xml version="1.0" encoding="UTF-8" ?>
<project name="MyProject" default="hello">
<!-- ============================================ --> <!-- (DEFAULT) Target: hello --> <!-- ============================================ --> <target name="hello" description="Says Hello"> <echo msg="Hello, world!" /> </target>
<!-- ============================================ --> <!-- Target: cheese --> <!-- ============================================ -->
<target name="cheese" description="Expresses feelings about cheese"> <echo msg="I like cheese" /> </target>
</project>
Let's go ahead and run the above build, in which we've specified the default target to be "hello", by running the "phing" command from the directory where this file is saved,:
$ phing
Buildfile: /Users/sal/phing/build.xml
MyProject > hello:
[echo] Hello, world!
BUILD FINISHED
Total time: 0.6856 seconds
Now, try and run the "cheese" target
$ phing cheese
Buildfile: /Users/sal/phing/build.xml
MyProject > cheese:
[echo] I like cheese
Yo Dawg, I heard you like targets
Running multiple targets can be done by separating them with a space
$ phing cheese hello
We can also set targets to be dependencies of other targets. Let's modify our default.
<target name="hello" depends="cheese" description="Says Hello">
<echo msg="Hello, world!" />
</target>
$ phing
Buildfile: /Users/sal/phing/build.xml
MyProject > cheese:
[echo] I like cheese
MyProject > hello:
[echo] Hello, world!
Tasks and Properties
Tasks are the actions that our targets are going to step through so we can get stuff done- we've already used the EchoTask above. We can pass them string, integer and boolean parameters as well as more complex data types such as lists of files. Tasks are defined as PHP classes and it's straightforward to roll your own should you need to, however most of what you'll need is probably already there. Documentation on all the built in Tasks can be found in the Phing User Guide. You can also check out the PHP source for all the tasks which will be located in your PHP installation's lib folder.
Properties are the equivalent of variables and let us use and manipulate stored values in our tasks.
<property name="drupal.version" value="7.10" />
A simple Drupal deploy
We're going to clone the Drupal core repository and deploy it to a server. For this deploy, I'm going to grab Drupal from http://git.drupal.org/project/drupal.git (see http://drupal.org/project/drupal/git-instructions). We'll keep it simple for this example by having everything in one task, but you should split tasks into reusable chunks as you would with PHP code/functions.
<?xml version="1.0" encoding="UTF-8" ?>
<project name="DrupalDeploy" default="deploy">
<!-- ============================================ -->
<!-- (DEFAULT) Target: deploy -->
<!-- ============================================ -->
<property name="drupal.workingdir" value="./drupal" />
<!-- Get the full directory path -->
<resolvepath propertyName="drupal.workingdir.resolved" file="${drupal.workingdir}" />
<target name="deploy" description="Deploys a copy of Drupal Core">
<mkdir dir="${drupal.workingdir}" />
<!-- Clone drupal core -->
<gitclone repository="http://git.drupal.org/project/drupal.git" targetPath="${drupal.workingdir.resolved}" />
</target>
</project>
This may fail for you if you don't have PEAR's VersionControl_Git package installed, go ahead and install that if you need to
$ pear install VersionControl_Git-alpha
This is going to take several minutes since cloning a repository with Git will retrieve the entire history of the Drupal project, so it's time to go make yourself a cup of tea. If you want to understand more about what exactly git is retrieving at this point, I recommend Blake Hall's excellent Vision for Version Control video on Drupalize.me. For the sake of saving us some time whilst we're learning, we're going to reuse this clone so we don't have to wait each time we run the Phing build file (you could try "git archive --remote" if your remote repository supports that). We'll do it by checking to see if the drupal/.git directory exists or not. For your own deployment scripts it's better to make no assumptions about files that are already on your system and start from a clean checkout of your code.
<target name="getDrupal" description="Clone the Drupal repository">
<delete dir="${drupal.workingdir.resolved}" includeemptydirs="true" verbose="true" failonerror="false" />
<mkdir dir="${drupal.workingdir.resolved}" />
<!-- Clone drupal core -->
<gitclone repository="http://git.drupal.org/project/drupal.git" targetPath="${drupal.workingdir.resolved}" />
</target>
Now you can run "phing getDrupal" if you want to remake your git repository.
Steps for Deployment
It usually helps to write the steps down in a list before building the XML.
- Get Drupal
- Switch to the version of Drupal I want
- Add database credentials to settings.php
- Put the code in the webroot (and remove unwanted files)
- Make the code live
I then like to put these in as comments and fill them out. You can follow them in the full build file below.
<?xml version="1.0" encoding="UTF-8" ?>
<project name="DrupalDeploy" default="deploy">
<!-- ============================================ -->
<!-- (DEFAULT) Target: deploy -->
<!-- ============================================ -->
<property name="drupal.version" value="7.10" />
<property name="drupal.git" value="http://git.drupal.org/project/drupal.git" />
<property name="drupal.workingdir" value="./drupal" />
<!-- Get the full directory path -->
<resolvepath propertyName="drupal.workingdir.resolved" file="${drupal.workingdir}" />
<property name="server.webroot" value="/Users/sal/Sites/mysite" />
<tstamp>
<format property="build.time" pattern="%Y_%m_%d__%H_%M_%S" />
</tstamp>
<property name="drupal.settings" value="$databases = array (
'default' =>
array (
'default' =>
array (
'database' => 'mysite',
'username' => 'username',
'password' => 'password',
'host' => 'mydb',
'port' => '',
'driver' => 'mysql',
'prefix' => '',
),
),
);
$drupal_hash_salt = 'abc123';" />
<target name="deploy" description="Deploys a copy of Drupal Core">
<!-- Switch to the version of Drupal I want -->
<gitcheckout repository="${drupal.workingdir.resolved}" branchname="${drupal.version}" />
<!-- Add database credentials to settings.php -->
<delete file="${drupal.workingdir.resolved}/sites/default/settings.php" failonerror="false" />
<copy file="${drupal.workingdir.resolved}/sites/default/default.settings.php" tofile="${drupal.workingdir.resolved}/sites/default/settings.php" />
<append destFile="${drupal.workingdir.resolved}/sites/default/settings.php" text="${drupal.settings}" />
<chmod file="${drupal.workingdir.resolved}/sites/default/settings.php" mode="0444" />
<!-- Put the code in the webroot (remove unwanted files) -->
<copy todir="${server.webroot}/drupal-${build.time}" >
<fileset dir=".">
<include name="drupal/**" />
<exclude name="drupal/.git" />
<exclude name="drupal/.git/**" />
</fileset>
</copy>
<!-- Make the code live. -->
<symlink target="${server.webroot}/drupal-${build.time}/drupal" link="${server.webroot}/live" overwrite="true" />
</target>
<target name="getDrupal" description="Clone the Drupal repository">
<delete dir="${drupal.workingdir.resolved}" includeemptydirs="true" verbose="true" failonerror="false" />
<mkdir dir="${drupal.workingdir.resolved}" />
<!-- Clone drupal core -->
<gitclone repository="http://git.drupal.org/project/drupal.git" targetPath="${drupal.workingdir.resolved}" />
</target>
</project>
$ phing
Buildfile: /Users/sal/phing/build.xml
[resolvepath] Resolved ./drupal to /Users/sal/phing/drupal
DrupalDeploy > deploy:
[gitcheckout] git-checkout command: /usr/bin/git checkout '7.10'
[gitcheckout] git-checkout: checkout "/Users/sal/phing/drupal" repository
[gitcheckout] git-checkout output:
[delete] Deleting: /Users/sal/phing/drupal/sites/default/settings.php
[copy] Copying 1 file to /Users/sal/phing/drupal/sites/default
[append] Appending string to /Users/sal/phing/drupal/sites/default/settings.php
[chmod] Changed file mode on '/Users/sal/phing/drupal/sites/default/settings.php' to 444
[copy] Created 120 empty directories in /Users/sal/Sites/mysite/drupal-2012_01_16__13_13_55
[copy] Copying 1035 files to /Users/sal/Sites/mysite/drupal-2012_01_16__13_13_55
[symlink] Linking: /Users/sal/Sites/mysite/drupal-2012_01_16__13_13_55/drupal to /Users/sal/Sites/mysite/live
Run it a few times and you should see your new code being deployed each time.
Custom Tasks
One of the best things about Phing is that we can build our own custom tasks using PHP. See the Extending Phing docs for more information. Custom tasks can be placed in a folder called "tasks" next to your build XML.
Here's an example of a task and build file that prints a random string.
<pre>
<code class="language-php">
require_once 'phing/Task.php';
class RandomStringTask extends Task
{
private $propertyName;
/**
* Set the name of the property to set.
* @param string $v Property name
* @return void
*/
public function setPropertyName($v) {
$this->propertyName = $v;
}
public function main() {
if (!$this->propertyName) {
throw new BuildException("You must specify the propertyName attribute", $this->getLocation());
}
$project = $this->getProject();
$c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxwz0123456789";
$length = 12;
for(;$length > 0;$length--) $s .= $c{rand(0,strlen($c))};
$random = str_shuffle($s);
$this->project->setProperty($this->propertyName, $random);
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<project name="Random" default="random">
<taskdef name="randomstring" classname="tasks.RandomStringTask" />
<property name="mystring" value="null" />
<!-- ============================================ -->
<!-- (DEFAULT) Target: random -->
<!-- ============================================ -->
<target name="random" description="Prints a random string">
<randomstring propertyName="mystring" />
<echo message="${mystring}" />
</target>
</project>
$ phing
Buildfile: /Users/sal/Downloads/phing/build.xml
Random > random:
[echo] 0uH282IFqirG
Jenkins Usage
Jenkins is a very popular open-source continuous integration server that executes and monitors repeated jobs such as testing code, building software and running cron jobs. The good news is it has Phing support and you can enable it under the Plugin Manager.
Once you've enabled the plugin, you'll see an "Invoke Phing targets" build step option when you configure a job. From there you can specify which Phing targets to run and pass values of properties to your build.
You can pass a number of very useful things from the Jenkins build environment into your Phing script, such as the workspace location and build tag.
A common use for this is deploying automated builds to test environments every time code is committed to version control.
Homework
- Use an external property file to hold credentials or if using Jenkins use secret files
- Use drush commands
- Deploy code to remote servers
- Switch the live symlink with an atomic operation by creating a temporary symlink and then using mv
- Automate as much as possible! e.g. running database updates, getting contrib and custom modules