Extending Phing

Phing was designed to be flexible and easily extensible. Phing's existing core and optional tasks do provide a great deal of flexibility in processing files, performing database actions, and even getting user feedback during a build process. In some cases, however, the existing tasks just won't suffice and because of Phing's open, modular architecture adding exactly the functionality you need is often quite trivial.

In this chapter we'll look primarily at how to create your own tasks, since that is probably the most useful way to extend Phing. We'll also give some more information about Phing's design and inner workings.

Extension Possibilities

There are three main areas where Phing can be extended: tasks, types, mappers. The following sections discuss these options.

Tasks

Tasks are pieces of codes that perform an atomic action like installing a file. Therefore a special worker class hast to be created and stored in a specific location, that actually implements the job. The worker is just the interface to Phing that must fulfill some requirements discussed later in this chapter, however it can - but not necessarily must - use other classes, workers and libraries that aid performing the operations needed.

Types

Extending types is a rare need; nevertheless, you can do it. A possible type you might implement is urlset, for example.

You may end up needing a new type for a task you write; for example, if you were writing the XSLTTask you might discover that you needed a special type for XSLTParams (even though in that case you could probably use the generic name/value Parameter type). In cases where the type is really only for a single task, you may want to just define the type class in the same file as the Task class, rather than creating an official stand-alone Type.

Mappers

Creating new mappers is also a rare need, since most everything can be handled by the RegexpMapper. The Mapper framework does provide a simple way for defining your own mappers to use instead, however, and mappers implement a very simple interface.

Source Layout

Files And Directories

Before you are going to start to extend Phing let's have a look at the source layout. You should be comfortable with the organization of files witchin the source tree of Phing before start coding. After you extracted the source distribution or checked it out from CVS you should see the following directory structure:

$PHING_HOME
  |-- bin
  |-- classes
  |    `-- phing
  |         |-- filters
  |         |    `-- util
  |         |-- mappers
  |         |-- parser
  |         |-- tasks
  |         |    |-- ext
  |         |    |-- system
  |         |    |    `-- condition
  |         |    `-- user
  |         `-- types
  |-- docs
  |    `-- phing_guide
  `-- test
       |-- classes
	   `-- etc

The following table briefly describes the contents of the major directories:

Phing source tree directories
Directory Contents

bin

The basic applications (phing, configure) as well as the wrapper scripts for different platforms (currently Unix and Windows).

classes

Repository of all the classes used by Phing. This is the base directory that should be on the PHP include_path. In this directory you will find the subdirectory phing/ with all the Phing relevant classes.

docs

Documentation files. Generated books, online manuals as well as the PHPDoc generated API documentation.

test

A set of testcases for different tasks, mappers and types. If you are developing in CVS you should add a testcase for each implementation you check in.

Currently there is no distinction between the source layout and the build layout of Phing. The figure above shows the CVS tree that carries some additional files like the Phing website. Later on there may be a buildfile to create a clean distribution tree of Phing itself.

File Naming Conventions

There are some filenaming conventions used by Phing. Here's a quick rundown on the most basic conventions. A more detailed list can be found in [See Naming And Coding Standards]:

Coding Standards

We are using PEAR coding standards. We are using a less strict version of these standards, but we do insist that new contributions have phpdoc comments and make explicitly declarations about public/protected/private variables and methods. If you have suggestions about improvements to Phing codebase, don't hesitate to let us know.

System Initialization

PHP installations are typically quite customized -- e.g. different memory_limit, execution timeout values, etc. The first thing that Phing does is modify PHP INI variables to create a standard PHP environment. This is performed by the init layer of Phing that uses a three-level initialization procedure. It basically consists of three different files:

At the first look this may seem to be unnecessary overhead. Why three levels of initialization? The main reason why there are several entry points is that Phing is build so that other frontends (e.g. PHP-GTK) could be used in place of the command line.

Wrapper Scripts

This scripts are technical not required but provided for the ease of use. Imagine you have to type every time you want to build your project:

php -qC /path/to/phing/bin/phing.php -verbose all distro snapshot

Indeed that is not very elegant. Furthermore if you are lax in setting your environment variables these script can guess the proper variables for you. However you should always set them.

The scripts are platform dependent, so you will find shell scripts for Unix like platforms (sh) as well as the batch scripts for Windows platforms. If you set-up your path properly you can call Phing everywhere in your system with this command-line (referring to the above example):

phing -v2 all distro

The Main Application (phing.php)

This is basically a wrapper for the Phing class that actually does all the logic for you. If you look at the sourcecode for phing.php you will see that all real initialization is handled in the Phing class. phing.php is simply the commandline entry point for Phing.

The Phing Class

Given that all the prior initialization steps passed successfully the Phing is included and Phing::startup() is invoked by the main application script. It sets-up the system components, system constants ini-settings, PEAR and some other stuff. The detailed start-up process is as follows:

After the main application completed all operations (successfully or unsuccessfully) it calls Phing::shutdown(EXIT_CODE) that takes care of a proper destruction of all objects and a gracefully termination of the program by returning an exit code for shell usage (see [See Program Exit Codes] for a list of exit codes).

System Services

The Exception system

Phing uses the PHP5 try/catch/throw Exception system. Phing defines a number of Exception subclasses for more fine-grained handling of Exceptions. Low level Exceptions that cannot be handled will be wrapped in a BuildException and caught by the outer-most catch() {} block.

Build Lifecycle

This section exists to explain -- or try -- how Phing "works". Particularly, how Phing procedes through a build file and invokes tasks and types based on the tags that it encounters.

How Phing Parses Buildfiles

Phing uses an ExpatParser class and PHP's native expat XML functions to handle the parsing of build files. The handler classes all extend the phing.parser.AbstractHandler class. These handler classes "handle" the tags that are found in the buildfile.

Core tasks and datatypes are mapped to XML tag names in the defaults.properties files -- specifically phing/tasks/defaults.properties and phing/types/defaults.properties.

It works roughly like this:

  1. phing.parser.RootHandler is registered to handle the buildfile XML document
  2. RootHanlder expects to find exactly one element: <project>. RootHandler invokes the ProjectHandler with the attributes from the <project> tag or throws an exception if no <project> is found, or if something else is found instead.
  3. ProjectHandler expects to find <target> tags; for these ProjectHandler invokes the TargetHandler. ProjectHandler also has exceptions for handling certain tasks that can be performed at the top-level: <resolve>, <taskdef>, <typedef>, and <property>; for these ProjectHandler invokes the TaskHandler class. If a tag is presented that doesn't match any expected tags, then ProjectHandler assumes it is a datatype and invokes the DataTypeHandler.
  4. TargetHandler expects all tags to be either tasks or datatypes and invokes the appropriate handler (based on the mappings provided in the defaults.properties files).
  5. Tasks and datatypes can have nested elements, but only if they correspond to a create*() method in the task or datatype class. E.g. a nested <param> tag must correspond to a createParam() method of the task or datatype.

... More to come ...

Writing Tasks

Creating A Task

We will start creating a rather simple task which basically does nothing more than echo a message to the screen. See [below] for the source code and the following [below] for the XML definition that is used for this task.

<?php

require_once "phing/Task.php";

class MyEchoTask extends Task {
    
    /**
     * The message passed in the buildfile.
     */
    private $message = null;

    /**
     * The setter for the attribute "message"
     */
    public function setMessage($str) {
        $this->message = $str;
    }
    
    /**
     * The init method: Do init steps.
     */
    public function init() {
      // nothing to do here
    }
    
    /**
     * The main entry point method.
     */
    public function main() {
      print($this->message);
    }
}

?>

This code contains a rather simple, but complete Phing task. It is assumed that the file is named MyEchoTask.php and placed in classes/phing/tasks/my directory. We'll explain the source code in detail shortly. But first we'd like to discuss how we should register the task to Phing so that it can be executed during the build process.

Using the Task

The task shown [above] must somehow get called by Phing. Therefore it must be made available to Phing so that the buildfile parser is aware a correlating XML element and it's parameters. Have a look at the minimalistic buildfile example given in [the buildfile below] that does exactly this.

<?xml version="1.0" ?>

<project name="test" basedir="." default="myecho">
    <taskdef name="myecho" worker="phing.tasks.my.MyEcho" />

    <target name="test.myecho">
      <myecho message="Hello World" />
    </target>
</project>

Besides the XML document prolog and the shell elements that are required to properly execute the task (project, target) you'll find the <taskdef> element (line 4) that properly registers your custom task to Phing. For a detailed synopsis of the taskdef element see the [description of this task].

Now, as we have registered the task by assigning a name and the worker class ([see source code above]) it is ready for usage within the <target> context (line 8). You see that we pass the message that our task should echo to the screen via an XML attribute called "message".

Source Discussion

No that you've got the knowledge to execute the task in a buildfile it's time to discuss how everything works.

Task Structure

All files containing the definition of a task class follow a common well formed structure:

Package Imports

Always import all the packages/files needed for this task in full written notation. Furthermore you should always import phing.Task at the very top of your import block. Then import all other required system or proprietary packages. Import works quite similar to PHP's native include_once but with some Java-stylish additions providing a file system independent notation.

For a more in-depth explanation of the used package mechanism and the package support API reference, see [package support] For a list of stock packages provided with Phing, see [package list].

Class Declaration

If you look at line 5 in [the source code of the task] you will find the class declaration. This will be familiar to you if you are experienced with OOP in PHP (we assume here that you are). Furthermore there are some fine-grained rules you must obey when creating the classes (see also,[naming and coding standards]):

Class Properties

The next lines you are coding are class properties. Most of them are inherited from the Task superclass, so there's not need to redeclare them. Nevertheless you should declare the following ones by your own:

In the MyEchoTask example the coded properties can be found in lines 7 to 11. Give you properties meaningful descriptive names that clearly state their function within the context. A couple of properties are inherited from the superclass that must not be declared in the properties part of the code.

For a list of inherited properties (most of them are reserved, so be sure not to overwrite them with your own) can be found in the "Phing API Reference" in the docs/api/ directory.

The Constructor

The next block that follows is the class's constructor. It must be present and call at least the constructor or the parent class. Of course, you can add some initialization data here. It is recommended that you define your prior declared properties here.

Setter Methods

As you can see in the XML definition of our task ([see buildfile above] , line 9) there is an attribute defined with the task itself, namely "message" with a value of the the text string that our task should echo. The task must somehow become aware of the attribute name and the value. Therefore the setter methods exist.

For each attribute you want to import to the task's namespace you have to define a method named exactly after the very attribute plus the string "Set" prepended. This method accepts exactly one parameter that holds the value of the attribute. No you can set the value an class internal property to the value incoming via the setter method.

In out example the setter is named SetMessage , because the XML attribute the echo task accepts is "message". SetMessage now takes the string "Hello World" provided by the parser and sets the value of the internal class property $strMessage to "Hello World". It is now available to the task for further disposal.

Creator Methods

Creator methods allow you to manage nested XML tags in your new Phing Task.

init() Method

The init method gets called when the <taskname> xml element closes. It must be implemented even if it does nothing like in the example above. You can do init steps here required to setup your task object properly. After calling the Init-Method the task object remains untouched by the parser. Init should not perform operations related somehow to the action the task performs. An example of using init may be cleaning up the $strMessage variable in our example (i.e. trim($strMessage)) or importing additional workers needed for this task.

The init method should return true or an error object evaluated by the governing logic. If you don't implement init method, phing will shout down with a fatal error.

main() Method

There is exactly one entry entry point to execute the task. It is called after the complete buildfile has been parsed and all targets and tasks have been scheduled for execution. From this point forward the very implementation of the tasks action starts. In case of our example a message (imported by the proper setter method) is Logged to the screen through the system's "Logger" service (the very action this task is written for). The Log() method-call in this case accepts two parameters: a event constant and the message to log.

For a in-depth list of system constants see See System Constants. For the detailed reference on the system's logger see [REF] and the Phing API docs located in the docs/ subdirectory.

Arbitrary Methods

For the more or less simple cases (as our example) all the logic of the task is coded in the Main() method. However for more complex tasks common sense dictates that particular action should be swapped to smaller, logically contained units of code. The most common way to do this is separating logic into private class methods - and in even more complex tasks in separate libraries.

private function myPrivateMethod() {
    // definition
} 

More reading on this particular topic can be sound in See Naming And Coding Standards.

Summary

You now have learned how to create and use a task. However we guess there are much questions open concerning task development: "How do I use filesets and mapper" or "How do I implement custom nested tags in my task". Most of these concepts and the proper usage will be clear if you continue reading this doc. Additionally you might check out the appendices for the advanced examples (See Advanced Task Example).

Writing Types

You should only create a standalone Type if the Type needs to be shared by more than one Task. If the Type is only needed for a specific Task -- for example to handle a special parameter or other tag needed for that Task -- then the Type class should just be defined within the same file as the Task. (For example, phing/filters/XSLTFilter.php also includes an XSLTParam class that is not used anywhere else.)

For cases where you do need a more generic Type defined, you can create your own Type class -- similar to the way a Task is created [Writing Tasks ].

Creating a DataType

Type classes need to extend the abstract DataType class. Besides providing a means of categorizing types, the DataType class provides the methods necessary to support the "refid" attribute. (All types can be given an id, and can be referred to later using that id.)

In this example we are creating a DSN type because we have written a number of DB-related Tasks, each of which need to know how to connect to the database; instead of having database parameters for each task, we've created a DSN type so that we can identify the connection params once and then use it in all our db Tasks.

require_once "phing/types/DataType.php";

/**
 * This Type represents a DB Connection.
 */
class DSN extends DataType {

  private $url;
  private $username;
  private $password;
  private $persistent = false;

  /**
   * Sets the URL part: mysql://localhost/mydatabase
   */
  public function setUrl($url) {
    $this->url = $url;
  }
  
  /**
   * Sets username to use in connection.
   */
  public function setUsername($username) {
    $this->username = $username;
  }

  /**
   * Sets password to use in connection.
   */
  public function setPassword($password) {
    $this->password = $password;
  }

  /**
   * Set whether to use persistent connection.
   * @param boolean $persist
   */
  public function setPersistent($persist) {
    $this->persistent = (boolean) $persist;
  }

  public function getUrl(Project $p) {
    if ($this->isReference()) {
      return $this->getRef($p)->getUrl($p);
    }
    return $this->url; 
  }

  public function getUsername(Project $p) {
    if ($this->isReference()) {
      return $this->getRef($p)->getUsername($p);
    }
    return $this->username; 
  }

  public function getPassword(Project $p) {
    if ($this->isReference()) {
      return $this->getRef($p)->getPassword($p);
    }
    return $this->password; 
  }

  public function getPersistent(Project $p) {
    if ($this->isReference()) {
      return $this->getRef($p)->getPersistent($p);
    }
    return $this->persistent; 
  }

  /**
   * Gets a combined hash/array for DSN as used by PEAR.
   * @return array
   */
  public function getPEARDSN(Project $p) {
    if ($this->isReference()) {
      return $this->getRef($p)->getPEARDSN($p);
    }

    include_once 'DB.php';
    $dsninfo = DB::parseDSN($this->url);
    $dsninfo['username'] = $this->username;
    $dsninfo['password'] = $this->password;
    $dsninfo['persistent'] = $this->persistent;

    return $dsninfo;
  }
  
  /**
   * Your datatype must implement this function, which ensures that there 
   * are no circular references and that the reference is of the correct 
   * type (DSN in this example).
   * 
   * @return DSN
   */
  public function getRef(Project $p) {
    if ( !$this->checked ) {
      $stk = array();
      array_push($stk, $this);
      $this->dieOnCircularReference($stk, $p);
    }
    $o = $this->ref->getReferencedObject($p);
    if ( !($o instanceof DSN) ) {
      throw new BuildException($this->ref->getRefId()." doesn't denote a DSN");
    } else {
      return $o;
    }
  }

}

Using the DataType

The TypedefTask provides a way to "declare" your type so that you can use it in your build file. Here is how you would use this type in order to define a single DSN and use it for multiple tasks. (Of course you could specify the DSN connection params each time, but the premise behind needing a DSN datatype was to avoid specifying the connection parameters for each task.)

<?xml version="1.0" ?>

<project name="test" basedir=".">

  <typedef name="dsn" worker="myapp.types.DSN" />

  <dsn
      id="maindsn" 
      url="mysql://localhost/mydatabase"
      username="root"
      password=""
      persistent="false" />

  <target name="main">

    <my-special-db-task>
	     <dsn refid="maindsn"/>
    </my-special-db-task>

    <my-other-db-task>
      <dsn refid="maindsn"/>
    </my-other-db-task>

  </target>

</project>

Source Discussion

Getters & Setters

You must provide a setter method for every attribute you want to set from the XML build file. It is good practice to also provide a getter method, but in practice you can decide how your tasks will use your task. In the example above, we've provided a getter method for each attribute and we've also provided an additional method: DSN::getPEARDSN() which returns the DSN hash array used by PEAR::DB, PEAR::MDB, and Creole. Depending on the needs of the Tasks using this DataType, we may only wish to provide the getPEARDSN() method rather than a getter for each attribute.

Also important to note is that the getter method needs to check to see whether the current DataType is a reference to a previously defined DataType -- the DataType::isReference() exists for this purpose. For this reason, the getter methods need to be called with the current project, because References are stored relative to a project.

The getRef() Method

The getRef() task needs to be implemented in your Type. This method is responsible for returning a referenced object; it needs to check to make sure the referenced object is of the correct type (i.e. you can't try to refer to a RegularExpresson from a DSN DataType) and that the reference is not circular.

You can probably just copy this method from an existing Type and make the few changes that customize it to your Type.

Writing Mappers

Writing your own filename mapper classes will allow you to control how names are transformed in tasks like CopyTask, MoveTask, XSLTTask, etc. In some cases you may want to extend existing mappers (e.g. creating a GlobMapper that also transforms to uppercase); in other cases, you may simply want to create a very specific name transformation that isn't easily accomplished with other mappers like GlobMapper or RegexpMapper.

Creating a Mapper

Writing filename mappers is simplified by interface support in PHP5. Essentially, your custom filename mapper must implement phing.mappers.FileNameMapper. Here's an example of a filename mapper that creates DOS-style file names. For this example, the "to" and "from" attributes are not needed because all files will be transformed. To see the "to" and "from" attributes in action, look at phing.mappers.GlobMapper or phing.mappers.RegexpMapper.

require_once "phing/mappers/FileNameMapper.php";

/**
 * A mapper that makes those ugly DOS filenames.
 */
class DOSMapper implements FileNameMapper {
  
  /**
   * The main() method actually performs the mapping.
   *
   * In this case we transform the $sourceFilename into
   * a DOS-compatible name.  E.g.
   * ExtendingPhing.html -> EXTENDI~.DOC
   *
   * @param string $sourceFilename The name to be coverted.
   * @return array The matched filenames.
   */
  public function main($sourceFilename) {
	   
    $info = pathinfo($sourceFilename);
    $ext = $info['extension'];
    // get basename w/o extension
    $bname = preg_replace('/\.\w+\$/', '', $info['basename']);
    
    if (strlen($bname) > 8) {
      $bname = substr($bname,0,7) . '~';
    }
    
    if (strlen($ext) > 3) {
      $ext = substr($bname,0,3);
    }
    
    if (!empty($ext)) {
      $res = $bname . '.' . $ext;
    } else {
      $res = $bname;
    }
    
    return (array) strtoupper($res);
  }

  /**
   * The "from" attribute is not needed here, but method must exist.
   */
  public function setFrom($from) {}

	 /**
   * The "from" attribute is not needed here, but method must exist.
   */
  public function setTo($to) {}

}

Using the Mapper

Assuming that this mapper is saved to myapp/mappers/DOSMapper.php (relative to a path on PHP's include_path or in PHP_CLASSPATH env variable), then you would refer to it like this in your build file:

<mapper classname="myapp.mappers.DOSMapper"/>