XPCLI: Command line classes

at 2007-07-22 in Examples by friebe (0 comments)

It's been a while since RFC #0102 has been implemented, and since then a couple of questions have arisen on how to use the runnable classes proposed within it and what their benefits over plain-old PHP scripts are.

Let's start with an overview of command line scripts' requirements:

  • Parameter handling
  • Usage
  • Output informational text to standard output, errors to standard error
  • Simulation mode/testability

A "plain-old" PHP script
Here's a script called "head.php" which will perform a HTTP HEAD request to a given url and prints a string representation of the server's response:

  require('lang.base.php');
xp::sapi
('cli');
uses
('peer.http.HttpConnection');

$p= new ParamString();
if
($p->exists('help', '?')) {
Console::
$err->writeLine('Usage: php head.php url [--verbose] [--help]');
exit
(1);
}

$c= new HttpConnection($p->value(1));
$p->exists('verbose') && Console::writeLine('Opening connection to ', $c);
Console::writeLine
($c->head()->toString());

The following should become clear when looking at the above example:
  • Command line argument handling needs to be performed manually
  • The usage needs to be maintained manually
  • This script is by no means reusable

The "runnable class" approach
The same script as above written using the util.cmd.Command class and the xpcli runner:
  uses('util.cmd.Command', 'peer.http.HttpConnection');

/**
* Performs a HTTP HEAD request on a given URL
*
* @see xp://peer.http.HttpConnection
* @purpose CLI
*/
class Head extends Command {
protected
$connection = NULL,
$verbose = FALSE;

/**
* Sets URL to connect to
*
* @param string url
*/
#[@arg(position= 0)]
public function setUrl($url) {
$this->connection= new HttpConnection($url);
}

/**
* Sets whether to be verbose
*
*/
#[@arg]
public function setVerbose() {
$this->verbose= TRUE;
}

/**
* Main runner method
*
*/
public
function run() {
$this->verbose && $this->out->writeLine('Opening connection to ', $this->connection);
$this->out->writeLine($this->connection->head()->toString());
}
}

Although the plain number of lines in this example clearly exceeds that of the "plain-old" version, here's some of the key benefits:
  • Command line argument handling is performed by simply annotating methods
  • You don't see any usage - it is generated from the apidoc comments you write
  • This approach is reusable

Command line argument handling
In the above example, you can see that command line arguments are handled via the "Inversion Of Control" (IOC) pattern - that is, instead of having to access them yourself, the xpcli environment will pass them to your class' instance when requested.
Here's an overview:
  // First command-line argument will be passed as $v
#[@arg(position= 0)]
public function setFirstArgument($v) { }

// Argument value from "--classname=[value]" or "-c [value]" will be passed as $v
#[@arg]
public function setClassname($v) { }

// Argument value from "--verbosity=[value]" or "-v [value]" will be passed as $v
#[@arg(name= 'verbosity')]
public function setVerbosity($v) { }

// Method will be called if (and only if) "--debug" or "-d" exist
#[@arg]
public function setDebug() { }

Usage
The usage text is auto-generated from the class' and methods' apidoc comments and is shown when the command line contains -? or --help (at any place after the class' name). This is what the usage for the Head class looks like:
  $ xpcli Head -?
Performs a HTTP HEAD request on a given URL
========================================================================
Usage: $ xpcli Head <url> [--verbose]
Arguments:
* #1
Sets URL to connect to

* --verbose | -v
Sets whether to be verbose

Reusability
Because you are writing a class, you can also make use of all OOP features. For example, you could create an abstract HttpCommand and let Head, Get and Post classes extend from it. The "plain-old" way would require copy and pasting sourcecode to get.php and post.php or a helper class (and with that, you're already quite close to an XPCLI, aren't you?:))


Up until now, the use-case has not been really ambitious, and this has kept the "plain-old" approach pretty straight-forward and the class approach - though easier to maintain and extend and with comfortable features such as auto-usage and self-documenting argument handling - might still not appeal to you; maybe because you don't need it to be extensible (trust me, there is a good chance you will:)) or you can do without the usage because you're the scripts only user (would you bet your head on that?).

When it gets more complex, the xpcli classes provide you with more features that will really start saving you time:

Error handling
In command classes, the case that a required argument is missing is handled by the runner and will thus not have to be checked by the class itself. While the "plain-old" way will throw an exception when $p->value(1) is first accessed (and preventing this would mean an extra if), xpcli will print a message to standard error without you having to do anything.

It gets even better if you provide an erroneous argument, say a URL starting with an unsupported scheme such as foo:// or bar://: xpcli will know the error resulted from your command line argument #1, whereas the plain-old approach will simply throw an exception somewhere down the road:
  $ xpcli Head borg://php3.de
*** Error for argument #1: Scheme "borg" not supported

$ php5 head.php borg://php3.de
Uncaught exception: Exception lang.IllegalArgumentException (Scheme "borg" not supported)
at [...]

Reusability from within
In a long script, you may feel the need to refactor certain repeated parts into a function. In the "plain-old" approach, you'll be creating global functions and will have to either pass all context information as argument (e.g., whether to be verbose) or use - shudder - global variables. In Command classes, you can simply use member variables for this.

Database access
One use-case for command line scripts is accessing a database - for example, for batch operations which exceed what can be done with SQL statements or involve logging. To do this in the "plain-old" way, you'd probably insert code such as:

  $conn= DriverManager::getConnection('sybase://user:pass@dbserv01');
$conn->connect(); // Ensure connectivity is given before continuing

...at the top of your script. The downsides should be clear:
  • You have database credentials inside your script. If you're only remotely paranoid you won't be able to distribute the script without censorship:)
  • Again, if you need the connection in a function, you either have to pass it in, use a global variable or - for example, the ConnectionManager singleton

In the xpcli case, again using IOC principles, we simply request a connection as follows:
  #[@inject(type= 'rdbms.DBConnection', name= 'news')]  
public function setConnection($conn) {
$conn->connect();
}

The connection configuration is kept in separate file called database.ini, the directory this file resides in can be passed to xpcli as follows:
$ xpcli -c etc/ [ClassName] [Argument [Argument [ ... ]]

This way, you can not only redistribute the code without disclosing credentials but also easily switch between production and staging systems by simly supplying different configurations, e.g. xpcli -c etc/production and xpcli -c etc/staging.

Obviously, if the connection is needed later on, you can assign a member variable to it in the setConnection() method.

Inject more!
The same way database connections are passed to your command class, properies and logging categories can be injected:
  // Logging
#[@inject(type= 'util.log.LogCategory', name= 'default')]
function setTrace($cat) { }

// Properties
#[@inject(type= 'util.Properties', name= 'app')]
function setApplicationConfig($conf) { }

Logging configuration will be read from log.ini in the directory given to xpcli via the -c switch, and properties will be read from files in that directory with names supplied in the annotation (in above example, that would be app.ini).


Testability
XPCLIs also give quite a fair gain for testability - remember the idea of passing two different configuration sets for database connections from above? - and, because they are classes, they are easily testable via unittesting, and last but not least, because they use dependency injection, it makes it even easier to inject a MockConnection instead of a real one.

This could fill an entire new article, so I'll cut short here and will continue this at another time and place.


This article should have given you a good overview of possibilities using xpclis.

Some further reading:



Subscribe

You can subscribe to the XP framework's news by using RSS syndication.


Categories

News
General
PHP5
Announcements
RFCs
Further reading
Examples
Editorial
EASC
Experiments
Unittests
Databases
5.8-SERIES
Unicode
Language
5.9-SERIES

Related

Find related articles by a search for «XPCLI:».