Saturday 18 April 2009

Creating and using Phing ad hoc tasks

Sometimes there are build scenarios where you'll badly need a functionality, like adding a MD5 checksum file to a given project, that isn't provided neither by the available Phing core nor the optional tasks. Phing supports developers with two ways for extending the useable task pool: by writing 'outline' tasks that will end up in a directory of the Phing installation or by utilizing the AdhocTaskdefTask, which allows to define custom tasks in the buildfile itself. The following post will try to outline how to define and use these inline tasks, by sketching an ad hoc task that enables the build orchestra to clone Git repositories from GitHub during a hypothetical workbench setup.

Creating the inline/ad hoc task

The AdhocTaskdefTask expects a name attribute i.e. github-clone for the XML element which will later referr to the ad hoc task and a CDATA section hosting the task implementation. Similar to 'outline' tasks the ad hoc task extends Phing's Task class, configures the task via attributes and holds the logic to perform. Unfortunately inline task implementations don't allow to require or include external classes available in the include_path, like Zend_Http_Client which I initially tried to use for an example task fetching short Urls from is.gd. This limits the available functions and classes to craft the task from to the ones built into PHP. The following buildfile snippet shows the implementation of the github-clone ad hoc task which is wrapped by a private target to encourage reusability and limit it's callability.
<target name="-init-ad-hoc-tasks" 
description="Initializes the ad hoc task(s)">
<adhoc-task name="github-clone"><![CDATA[
class Github_Clone extends Task {

private $repository = null;
private $destDirectory = null;

function setRepos($repository) {
$this->repository = $repository;
}
function setDest($destDirectory) {
$this->destDirectory = $destDirectory;
}
function main() {
// Get project name from repos Uri
$projectName = str_replace('.git', '',
substr(strrchr($this->repository, '/'), 1));

$gitCommand = 'git clone ' . $this->repository . ' ' .
$this->destDirectory . '/' . $projectName;

exec(escapeshellcmd($gitCommand), $output, $return);

if ($return !== 0) {
throw new BuildException('Git clone failed');
}
$logMessage = 'Cloned Git repository ' . $this->repository .
' into ' . $this->destDirectory . '/' . $projectName;
$this->log($logMessage);
}
}
]]></adhoc-task>
<echo message="Initialized github-clone ad hoc task." />
</target>

Using the ad hoc task

With the ad hoc task in the place to be, it's provided functionality can now be used from any target using the tasks XML element according to the given name i.e. github-clone in the AdhocTaskdefTask element earlier and by feeding it with the required attributes i.e. repos and dest. The next snippet allows you to take a peek at the complete buildfile with the ad hoc task in action.
<?xml version="1.0" encoding="UTF-8"?>
<project name="recordshelf" default="init-work-bench" basedir=".">

<property name="github.repos.dir" value="./github-repos" override="true" />

<target name="init-work-bench"
depends="-init-ad-hoc-tasks, -clone-git-repos"
description="Initializes the hypothetical workbench">
<echo message="Initialized workbench." />
</target>

<target name="-clean-git-repos"
description="Removes old repositories before initializing a new workbench">
<delete dir="${github.repos.dir}" includeemptydirs="true" failonerror="true" />
</target>

<target name="-init-ad-hoc-tasks"
description="Initializes the ad hoc task(s)">
<adhoc-task name="github-clone"><![CDATA[
class Github_Clone extends Task {

private $repository = null;
private $destDirectory = null;

function setRepos($repository) {
$this->repository = $repository;
}
function setDest($destDirectory) {
$this->destDirectory = $destDirectory;
}
function main() {
// Get project name from repos Uri
$projectName = str_replace('.git', '',
substr(strrchr($this->repository, '/'), 1));

$gitCommand = 'git clone ' . $this->repository . ' ' .
$this->destDirectory . '/' . $projectName;

exec(escapeshellcmd($gitCommand), $output, $return);

if ($return !== 0) {
throw new BuildException('Git clone failed');
}
$logMessage = 'Cloned Git repository ' . $this->repository .
' into ' . $this->destDirectory . '/' . $projectName;
$this->log($logMessage);
}
}
]]></adhoc-task>
<echo message="Initialized github-clone ad hoc task." />
</target>

<target name="-clone-git-repos" depends="-clean-git-repos"
description="Clones the needed Git repositories from GitHub">
<github-clone repos="git://github.com/abc/abc.git"
dest="${github.repos.dir}" />
<github-clone repos="git://github.com/xyz/xyz.git"
dest="${github.repos.dir}" />
</target>

</project>

Favouring inline over 'outline' tasks?

The one big advantage of using inline tasks over 'outline' tasks is that they are distributed with the buildfile and are instantly available without the need to modify the Phing installation. Some severe disadvantages of inline tasks are the limitation to use only the core PHP functions and classes for the implementation, the introduction of an additional hurdle to verify the task behaviour via PHPUnit as it's located in a CDATA section of the buildfile and the fact that the use of several inline tasks will blow up the buildfile, and thereby obfuscate the build flow.

Regrettably Phing doesn't provide an import task like Ant which might enable a refactoring to pull the ad hoc task definitions into a seperate XML file and include them at buildtime; in case you might have some expertise or ideas for a suitable workaround hit me with a comment. So far I tried to get it working, with no success, by utilizing Phing's PhingTask and XML's external entities declaration.

3 comments:

Anonymous said...

I have some problem with adhoc-task in my build file. So I copied the example in your "Creating and using Phing ad hoc tasks" in my buildfile and tried to test your adhoc-task “github-clone“. Unfortunetely I received the same error message as to my own adhoc-task: “Could not create task/type: 'github-clone'. Make sure that this class has been declared using taskdef / typedef." Perhaps I made somewhere a mistake. Do you have any idea what could went wrong? Thank! /TGDLEJU1

Raphael Stolt said...

Hi,

My first wild guess would be that there's maybe a typo a/k/a invalid PHP code in the adhoc-task definition. Try to extract the PHP code into a single file and run it against php -l to verify the syntax of the adhoc-task.

My second wild guess would be that the adhoc-task isn't initialized before using it. Therefor make sure it is by wrapping the adhoc definition into an own target, like in the blog post's -init-ad-hoc-tasks target, that's called as a dependency before the execution of the target including the adhoc task.

Hope this might be somehow helpful.

Cheers.

Anonymous said...

Yes, the problem was indeed missing of dependency. After adding the target with "init-adhoc-task" as dependency, the error is gone. THANKS! /TGDLEJU1