Friday 30 September 2011

Integrating Phing and Growl

I've been using Phing for more than a year now and it has become central to the way I work. Phing is a task based tool like make and Apache Ant that is used for automating processes. It is primarily aimed at PHP projects, though I suppose you could use it for any project that needs an automated element. Your project has a XML file called build.xml that specifies targets - a target is a job that you want to automate. Each target is built up of a number of tasks. These tasks might be 'run my unit tests' or 'copy these files.'

To execute a target, you enter 'phing targetname' on the command line. Importantly, one target can call other targets, so you can create complex routines that are run with just one command. I use just one command, 'phing ftp' to do all of the follow:
  • Run my Unit tests - if any fail the build process stops - and generate a code coverage report
  • Minify my JavaScript and CSS files
  • Switch off any debugging code in my PHP and remove any comments
  • Remove comments from my HTML
  • Upload the whole project to the staging server

Powerful stuff but the whole process takes a few minutes from start to end. I usually issue the command and then get on with something else while I wait for the files to be FTPed. What I noticed though was that I'd often forget to keep an eye on the terminal window and I'd miss that the process was complete. I wanted a popup notice to tell me that the files had all been transferred.

This is where Growl comes in. Growl is a really neat notifications system for Mac OSX and Windows. It integrates with a large number of applications. I've integrated it with Mac Mail, so I get a nice big popup message in the middle of my primary screen whenever I get an email.

There are a couple of ways to integrate Growl into an application, but for a system like Phing, the most appropriate is to use the command line tool growlnotify. This is an Growl extra - when you download Growl for Mac OSX, for example, it comes with three extra applications including growlnotify.
You call growlnotify from the command line like this:

growlnotify -n "Phing" -m "My message" -t "Phing"

The n parameter (for name) is the name that appears in your Growl preferences, so that you can specifically configure Growl for Phing. The m parameter is obviously the message that will appear in the popup notification and the t parameter sets the title in the popup. The only caveat is that the title parameter must be last.

You can also add a s switch to make the notification sticky.

Integrating this with Phing is actually quite straight forward. Phing already has a task that executes command line programs, so my solution was to extend that task's PHP class. That Phing task is called ExecTask and is located in phing/tasks/system/ExecTask.php. My task that extends this is called GrowlNotifyTask and is located in phing/tasks/mooduino/GrowlNotifyTask.php.

Lets start with your project's build file. Near the top, in side the <project> element, you need to tell Phing about the new task - this line declares the class, tells Phing where to find it, and allows use to start using a <grown> element inside any <target> elements.

<taskdef name="grown" classname="phing.tasks.mooduino.GrowlNotifyTask" />

I wanted to use the new task as follows but have the option to override some of the defaults if I wanted.

<grown message="Copying deployment files into build directory." />

The code for the task implementation is as follows. It's quite simple; it declares five private instance variables, with public setters and getters. These instance variables correspond to the attributes of the <grown> XML element. All but the $message variables have default values, so only the message attribute is required.

The class also has a main() method that pulls together the values into a command strings, and then uses parent methods in the ExecTask class to execute the command.

<?php
require_once 'phing/tasks/system/ExecTask.php';
/**
* A Task for calling the command line tool growlnotify as a phing task.
* 
* @author Michael Hodgins
*/
class GrowlNotifyTask extends ExecTask {

	private $name = 'phing';
	private $sticky = false;
	private $message = '';
	private $identifier = 'phing';
	private $title = 'Build';
	
	public function __construct() {
		parent::__construct();
		$this->setTaskName('grown');
	}
	
	public function init() {
		parent::init();
	}
	
	public function setName($name='phing') {
		$this->name = strval($name);
		return $this;
	}
	
	public function getName() {
		return $this->name;
	}
	
	public function setSticky($sticky=true)	{
		$this->sticky = $sticky ? true : false;
		return $this;
	}
	
	public function isSticky() {
		return $this->sticky;
	}
	
	public function setMessage($message='') {
		$this->message = strval($message);
		return $this;
	}
	
	public function getMessage() {
		return $this->message;
	}
	
	public function setIdentifier($identifer='phing') {
		$this->identifier = strval($identifier);
		return $this;
	}
	
	public function getIdentifier() {
		return $this->identifier;
	}
	
	public function setTitle($title='Build') {
		$this->title = strval($title);
		return $this;
	}
	
	public function getTitle() {
		return $this->title;
	}
	
	public function main() {
		$cmd = sprintf(
			"growlnotify %s-n '%s' -d '%s' -m '%s' -t '%s'",
			$this->isSticky() ? '-s ' : '',
			$this->getName(),
			$this->getIdentifier(),
			$this->getMessage(),
			$this->getTitle()
		);
		$this->setCommand($cmd);
		$this->setEscape(false);
		parent::main();
	}
}