Creating a Custom Phing Task

Nov 17 2009

Out of the box, Phing provides numerous features for creating high-powered source code management scripts. In an earlier post, I talked about metaprogramming in Phing, merely scratching the surface of what Phing can do. In this article, I want to illustrate how easy it is to extend Phing.

Phing can move, copy, package, unpackage, modify, touch, and delete files. But one thing it can't do out of the box is download files. In this short article, I will illustrate how easy it is to build a new Phing task to download a file from an arbitrary URL.

The desired goal it to make it possible to write a phing task that looks like this (in the build.xml):

  <download url="http://example.com/mypackage.tgz" tofile="SomeFile.tgz"/>

This task, when executed, should download the package from the target url and, if tofile is set, store the file with the name given there.

Doing this is going to involve two parts:

  • Writing the task in PHP.
  • Telling Phing, in the build.xml, where the new task is.

The first part is the harder of the two... it'll take a couple dozen lines of very basic code. <!--break-->

Writing a Custom Phing Task

Our custom Phing task needs to handle downloading an arbitrary URL and storing it on the file system.

Writing a Phing task is as simple as extending the Task Phing abstract base class. Every task needs to override two methods:

  • init(): Do any one-time initialization when loaded.
  • main(): Perform the task.

In addition to these two, Phing treats setters (mutators) as special. When an attribute appears in the XML tag, an associated setter will be called. In other words, if our task XML has an attribute url, then the task class needs to define a method setUrl($value) that takes the value of the attribute.

Our ideal download tag looked like this:

  <download url="http://example.com/mypackage.tgz" tofile="SomeFile.tgz"/>

It has two attributes: url and tofile. Consequently, our class will need to have two additional setter methods:

  • setUrl($url): Set the URL.
  • setToFile($file): Set the filename to write to.

In summary, then, our task class will need four methods. Here's what the complete code looks like:

<?php
class HttpDownloadTask extends Task {

  protected $url = NULL;
  protected $file = NULL;

  public function init() {}

  public function main() {
    if (empty($this->url) 
        || filter_var($this->url, FILTER_VALIDATE_URL) === FALSE) {
      throw new Exception('Invalid URL: You must specify a valid URL.');
    }
    if (empty($this->file)) {
      $this->file = basename($this->url);
    }
    copy($this->url, $this->file);
  }

  public function setURL($url) {
    $this->url = $url;
  }

  public function setToFile($filename) {
    $this->file = $filename;
  }
}
?>

Note that our new task, HttpDownloadTask, extends Task. Much of the task functionality is encapsulated in the parent Task class, and we don't have to worry about it.

We have no initialization tasks, so init() is empty. The main() function, on the other hand, contains most of the logic. Before we can really dive in, though, we need to take a quick glance at the setters.

The setURL method simply stores the URL in a property of the HttpDownloadTask object. Similarly, setToFile() simply stores the file name in the object's '$file variable.

Both of these variables, $file and $url are used in the main method. That method first validates the URL (note that it uses filtervar(). In a later post I will explain why filtervar() is a good idea.). It also checks the $name variable. If no name is set, it uses basename() to generate a filename based on the URL.

With that done, the only thing left to do is download the file and save it locally. We are going to assume that stream wrappers for HTTP are enabled, and we are going to use the simplest method possible of downloading and saving: We are going to use the PHP copy() function, which simply copies the first file's contents into the second file. Since it supports stream wrappers, it can take a URL as the first parameter. And that's what we do.

That's all there is to creating a custom task in code. Now we need to save this code in a file that uses the class's name as its name: HttpDownloadTask.php. I'm storing this in lib/task.

Making a Custom Phing Task Available in build.xml

Now that we have our code, the only thing we need to do is tell Phing that we are using this task, and that it will be called download. That is done using a taskdef element.

So somewhere near the top of the build.xml file, before the task is first used, we need to include a line that looks something like this:

  <taskdef classname="lib.Task.HttpDownloadTask" name="download"/>

This declaration simply tells Phing that the task is located in lib/Task/HttpDownloadtask.php (Phing uses a Java-style naming convention) and that the name of the tag that will expose HttpDownloadTask is download.

Once this is declared, we can use the task exactly as we defined it.

See it in Action

This class is not just a demo. A slightly modified version is used in the Drupal Distro Builder code.



comments powered by Disqus