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 */ public function setUrl($url) { $this->connection= new HttpConnection($url); } /** * Sets whether to be verbose * */ 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:
public function setFirstArgument($v) { } public function setClassname($v) { } public function setVerbosity($v) { } 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(); ...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:
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:
function setTrace($cat) { } 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.
CategoriesNews General PHP5 Announcements RFCs Further reading Examples Editorial EASC Experiments Unittests Databases 5.8-SERIES Unicode Language 5.9-SERIES
RelatedFind related articles by a search for «XPCLI:».
|