Showing posts with label tutorial. Show all posts
Showing posts with label tutorial. Show all posts

Monday, June 24, 2013

Managing Raspberry Pi with Chef & Bitbucket

The problem:
You have a Pi.
You've overcome that hurdle of thinking up a neat idea.
... but you don't have a means to deploy it to the Pi, and someone keeps coming along to steal the power supply for their Samsung phone.
That, plus your Pi depends on a lot of network configuration or other services to work fully.

The solution:

1) Go and put your Pi next to your Router and ensure both are out of reach. 
Neat cabling often implies secure cabling, so liberal use of zipties will protect your Pi from being beaten out by the collection of hungry smartphones in the house.

2) Make sure you have a passing familiarity with

  • Ruby
  • Bundler
  • Git
  • Bitbucket
  • SSH keys


3) Create a new git repo locally, and a skeleton Chef, Berkshelf setup.
mkdir mypi
cd mypi

# Add bundler
bundle init

# Add some helpful gems
echo 'gem "knife-solo" >> Gemfile
echo 'gem "berkshelf" >> Gemfile

bundle
rbenv rehash
knife solo init
berks init

git add .
git commit -m "Just adding a skeleton"

4) Ensure your SSH keys are on the pi, and go set up your ssh config.
echo "" >> ~/.ssh/config
echo "Host pi.local" >> ~/.ssh/config
echo "User pi" >> ~/.ssh/config

# ssh-keygen if you need to
ssh-copy-id pi@pi.local

5) Finally, start configuring your pi!
echo '{"run_list": [] }' > nodes/pi.local.json
git add nodes/pi.json

bundle exec knife solo prepare pi@pi.local
bundle exec knife solo cook pi@pi.local

5) Go make a pi specific cookbook
cd site-cookbooks
berks cookbook pi
echo 'package "weather-util" do :action install end' >> pi/recipies/default.rb
echo '{"run_list": ["recipe[pi]"] }' > nodes/pi.local.json
git add .
git commit -m "Making some custom pi"
cd ..


6) Run it!
bundle exec knife solo cook -VV pi@pi.local


What just happened?

We added knife-solo, a gem that lets you run chef commands without a chef server.

We made sure our ssh config was right, so we could connect to the machine.

We added berkshelf, a gem to manage chef dependencies/cookbooks (more on this later)

We created nodes/pi.local.json which describes the machine called pi.local. By default, your pi will try to make itself available on this address.

In that recipe, we first gave it an empty run list, but later we added a recipe for your Pi... called... pi.

We added
package "weather-util" do
  action :install
end
to site-cookbooks/pi/recipes/default.rb

... and then updated the nodes/pi.local.json

Finally, we cooked out recipe - the -VV put it into verbose mode.

That installed the 'weather' command onto your pi.

Wouldn't have been easier to do by hand?

Probably, or with Bash scripts + SSH, commititng those to git. What you get from Chef though is more than just bash scripts with a few prebuilt 'test' commands - you can use the entire community of Chef cookbooks to solve a vast majority of your needs.

For example, adding your typically LAMP stack is about as difficult as adding to pi/recipes/default.rb

["mysql", "apache", "php5"].each do |pkg|
  package pkg do
    action :install

  end
end

But then you have to muck around with configuration, starting services, etc.

Instead, I recommend you do something like http://community.opscode.com/cookbooks/apache2

Go do
echo 'cookbook "apache2"'  >> Berksfile
berks install -p cookbooks/

and edit your recipe (pi/recipes/default.rb) to add in

web_app "my_site" do
  server_name node['hostname']
  server_aliases ["pi.local"]
  docroot "/srv/www/my_site"
end

Don't forget to add the relevant depends to the metadata
echo "depends "apache2"' >> site-cookbooks/pi/metadata.rb

And to do the installation steps (remove any previous lines about installing apache you added)
$ cat nodes/pi.local/json

{
  "run_list": ["recipe[apache2::default]", "recipe[pi]"],
  "hostname": "pi.local"
}


and 
bundle exec knife solo cook -VV pi@pi.local

All of a sudden you should have Apache, a named virtualhost, and the contents of "my_site" being served up.

Compare that with manually trying to update a virtualhost, a2ensite it, etc - quite a bit of time can be saved.

More importantly, if you simply add this to git, and publish it to Bitbucket, you instantly have a way to upgrade to a newer server, create a cluster of Pis running your software, or a way to trivially migrate onto the cloud.

Where to from here?

I encourage you to look at cookbooks like newrelic (newrelic is a bad example, as it doesn't have binaries for the pi), so that you can tell if your Pi is up and running when away from the home, as well as things like hamachi - things that let you remotely admin, even behind your home router.

There's plenty of version control cookbooks, allowing you to check out your application from git, or more capistrano like mechanisms.

In general, think of whatever you might need and google "chef opscode (foo) cookbook"

Enhanced by Zemanta

Thursday, March 21, 2013

My recommended pathway into Ruby / Rails


I was asked what I'd encourage a complete outsider to RoR to look into.

Here's my list - what would you add?

- Linux intro
  - User accounts
  - File permissions
  - su / sudo
  - Installing programs (apt)
  - What's a distro? (Ubuntu, Debian, etc)
  - Gnome desktop
    - Alternatives
  - Bourne Again SHell (BASH)
    - cd, ls, touch, vim, top, kill, pgrep, pkill, mkdir, chmod, chown
  - Basic bash scripts
- Git intro
  - Cloning
  - Commits
  - Log
  - Push
  - Pull
  - Fetch, Merge
  - Branches
  - Tags
  - Revert
  - Reset
- Ruby intro
  - Hello world
  - Functions, Control flow, Loops
  - Classes
  - Modules
  - Blocks! Lambas! Currying!
    - Chunky Bacon and Foxes!
  - DSLs
  - Libraries (gems)
  - Require / include path
  - Metaprogramming
  - OOP Intro
  - OOP: SOLID principles
  - OOP: TDD
- HTML intro
  - HTML5
    - what is it?
    - differences to HTML4/xhtml?
    - Data attributes
    - New inputs
  - Javascript basics
    - JQuery
    - AJAX
    - Backbone, Ember, etc
    - D3
  - CSS basics
    - CSS3
    - Responsive design
  - SVG basics
- JSON! YAML! Data structures & serializations.
- Rails intro
  - Generators
  - Rake
  - Organisation
  - Scaffolds
  - Convention, not configuration
  - Rspec
- Sinatra intro
- SASS / Less
  - What is it?
  - Bootstrap/Compass
- Continuous Integration
  - Jenkins
  - Travis CI
  - Post commit hooks
  - Rake tasks
- DevOps!
  - Chef
  - Puppet

Friday, November 02, 2012

How to put a semantically enabled autocomplete control into your applications

One of the most common application design patterns is to implement a lookup table - some piece of business data has been given a description, and possible a code or identifier.

When creating new data, a user is often needing to select a code/identifier for a piece of information. This is usually done as a dropdown, or if they are many entries, an autocomplete control is often used.

This works well - some people will just make hashes storing the key/value pair in their code, others will ensure it's published into their relational data store.

Where it starts to fall down is in multiple applications working together - who can agree on the meaning of a code?
Your code of CASE_NIGHTMARE_GREEN is applied by a user and treated by one application as the coming of Chthulu, but after an ETL, CSV export or webservices message, the next application treats it as something different - users not up to date with the latest Lovecraftian spy thrillers start to misinterpret the data and apply it to anything involving green suitcases in horrible colours.

How do you fix this?
The next logical step often becomes to add a description, so that a UI can explain the term, but in a non services oriented environment, that's trapped in your datastore.

This won't work in a multiple vendor scenario, at least not unless you want to share your DB with them.

Another approach is the Code Table service - a service that has a focus on only retrieving data about a given input identifier.

I've seen this done in at least one SOA, and it's not a terrible pattern - but each vendor still has to stand up their own code table services, and there's a lot of repetition.

What else can you do?
Soon it becomes obvious that you want a decent way to find a code and the related data, but you also want to support aliases - my CASE_NIGHTMARE_GREEN is your WALK_IN_THE_PARK.

This gets tricky, quickly, as 1-1 mappings are difficult - and either a collection of vendors pull together and standardise on a list and the mappings, or no one really collaborates and fragile mapping code is introduced.

By this point, fear of change often sets in as the interfaces between parties are fragile, or to push changes through the consortium of vendors becomes a nightmare of project management and communication.

If you haven't had to roll out minor enhancements to a standard with a number of other parties who just aren't quite interested, take my word for it - it's painful.

All is not lost, there is another way - and it's simple.


What's the way forward?

My recommendation here is to push your codes into a triplestore. It doesn't fix everything, but it becomes trivial to relate information to the code - aliases, for example, or descriptions.

A triplestore is a RESTful service that allows you to execute queries - if you can deal with mongodb or mysql, you should be able to comprehend what's going on.

Don't just take my word for it - here's one prepared earlier - SNOMED, SPARQL powered autocomplete UI components. Pretty neat stuff.

Here's what wikipedia has to say about SNOMED, if you haven't heard of it.
SNOMED CT Concepts are representational units that categorize all the things that characterize health care processes and need to be recorded therein. In 2011, SNOMED CT includes more than 311,000 concepts, which are uniquely identified by a concept ID, i.e. the concept 22298006 refers to Myocardial infarction. All SNOMED CT concepts are organized into acyclic taxonomic (is-a) hierarchies; for example, Viral pneumonia IS-A Infectious pneumonia IS-A Pneumonia IS-A Lung disease. Concepts may have multiple parents, for example Infectious pneumonia is also a child of Infectious disease. The taxonomic structure allows data to be recorded and later accessed at different levels of aggregation. SNOMED CT concepts are linked by approximately 1,360,000 links, called relationships
That's one big code table, and you can see it's grown beyond just code/name pairing to include more data.

One of the key things that has been highlighted by the freebase folks and a few other places is the common problem - from a bunch of user input, go locate an object or identifier related to that term.

The moment you have an autocomplete control like these, it instantly kicks your application from "user is entering data into a text field" into "user is describing a semantic object, and I can grab all of the information about it that is relevant to my user".

Unlike standard, relational powered applications, SKOS + SPARQL makes this trivial - you simply write out a preferred label (skos:prefLabel), and many alternative labels (skos:altLabel).
What does that look like? Here's a sample query showing a user searching for... ear wax.

Note the URIs (try clicking on them to find out more information), and the preferred label/aliased label in the resultset, and try the resultset as JSON.

Even if no other parts of your application is aware of linked data, you can see how this graph of information can be flattened and pushed into a standard data store, for later use.




How can I build myself one of these?

Installing 4store


For this exercise, let's install some of the requirements:
$ sudo apt-get install 4store

Now we'll instantiate a new store (think database):

$ sudo 4s-backend-setup reference_store
4store[5196]: backend-setup.c:185 erased files for KB reference_store
4store[5196]: backend-setup.c:310 created RDF metadata for KB reference_store

Fire up the backend service (think of it like /etc/init.d/mysql start)
$ sudo 4s-backend reference_store

Populate some data - we'll use something I've prepared earlier as in turtle format. It helps to think of turtle as yaml but with URIs and a bit more magic.


$ git clone git://github.com/CloCkWeRX/4store-reference-service.git
$ cd 4store-reference-service
$ 4s-import reference_store --format turtle data.ttl



We're good to go - let's put the endpoint up
$ 4s-httpd -p 8000 reference_store



Now there's a (restful) endpoint living at
http://127.0.0.1:8000/sparql/

and you can run queries on it via http://127.0.0.1:8000/test/ - though until Issue #93 is solve, you probably just want to open the test-query.html page - this query will bring back both sets of data.

$ chrome test-query.html

From here, you can see the plain text, csv, JSON or XML results.

How do I do this in PHP, Rails, etc?

There's a lot of client libraries out there - I'd suggest having a quick read through of http://www.jenitennison.com/blog/node/152 for most rails developers, or looking at the sparql-client gem.

Failing that, peruse the ClientLibraries.


Where can I learn more about SPARQL?


Step 1, learn Turtle. If you can comprehend YAML, you should feel fairly comfortable.

Step 2, I'd try SPARQL by example. There's a good chance that if you are thinking of an SQL concept you want, such as LIKE matching; there's a SPARQL equivalent (FILTER regexp).

Luckily 95% of what you learned with turtle is simply reused by SPARQL - it introduces variables, where clauses/graphs, filters, and a few other things... but that's really all that's new.

Where to from here?

If you were to deploy this internally within an organisation, your service is pretty much good to go. You may want to look at Graph Access Control to add in some security, and the related SparqlServer/Update APIs.

Was this easy enough?

In comparison to the other approaches I have seen, it's fairly good.
  • It's trivial to put a front-end on your triplestore.
    You can roll your own with a minimum of fuss, or use things like https://github.com/kurtjx/SNORQL to provide an 'expert user' ability to inspect your data.
  • Adding, removing, etc aliases is trivial - there's no schema to migrate or anything else troublesome, and you can add in extra data at the drop of a hat - even if it's unrelated to your core set.
  • It's trivial to relate concepts to each other.
  • Your ontology (schema) is already there for code tables - http://www.w3.org/TR/skos-reference/ - you'll never have to reinvent that
  • There's products available that let you tie in your application behaviour/code tables right into Confluence or other platforms.

Tuesday, June 02, 2009

Net_LDAP2 - Stable!

Net_LDAP2 is an object oriented interface for searching and manipulating LDAP-entries of an LDAP server like ActiveDirecory or OpenLDAP.

It started life as a copy of Net_LDAP but introduced some API changes so beni created a new package instead of breaking the old Net_LDAP one.

But, as of now: Net_LDAP is deprecated, old and broken!

Going forward: how do I migrate?


Pretty simply replacing calls to Net_LDAP* with Net_LDAP2* will get you 90% of the way.

You can see there is little difference between the two, apart from the latter being E_STRICT friendly.


With this release, Net_LDAP will only be maintained for bugfixes and security issues. Please move to Net_LDAP2 which is actively developed and constantly gets new features.

Sunday, January 25, 2009

Using Image_Graph neatly

Here are my two best tips around using Image_Graph for projects. They aren't necessarily right, but have worked fantastically for me.

Use it like Google Chart API (on demand)


Build a simple page which takes a number of arguments via GET variables, and serves up an image. You can then use simple commands to render whatever you like.

# Rendering code:
require_once 'Net/URL.php';
function make_graph_url($data) {

$url = new Net_URL('graph.php');
$url->gt;querystring['data'] = $data;
$url->gt;querystring['type'] = 'pie';
return $url->gt;getURL(); // "graph.php?type=pie&data[Cats]=1&data[Fish]=2";
}

# HTML / Presentation bit
<img src="%3C?php%20print%20make_graph_url%28$data%29;%20?%3E" alt="Graph of Cats and Fish" />

#Graph.php
require_once 'Image/Graph.php';

$graph = new Image_Graph();
// read in $_GET and construct your graph

$graph->gt;done();


Its worth thinking about maintaining a pretty similar approach to google's API, so that you can swap one for the other almost trivially.

Pre-rendering


Say you have a set of reports you must run. The amount of data is huge, so you really don't want to try and do things on the fly. You have to update the data periodically - ie, once a week or month.

Steps here:
1. Denormalize in the database - precalculate answers and render them into tables. It will save you loads of time.
2. When you have the data you need, pre-render the graphs and save them to disk. Do it with an easy naming scheme.

Now when someone hits your pages to look at information, you've got everything already there - its a matter of wiring it together.


These two things are pretty obvious and self explanatory, but worth keeping in mind. The last thing you want to do is build a page which assembles data, then realize Image_Graph renders in a different stream (ie, not inline), and resort to copy and paste coding.


Reblog this post [with Zemanta]

Sunday, June 03, 2007

php style: foreach is better

Since it seems that everyone wants to know about reading and writing csv with php, I thought I'd take a moment to push the virtues of the foreach loop.

First: It's neater. Consider that
foreach ($collection as $item) {

}
is a billion times neater than:
for ($i=0; $i<count($collection); $i++) {
$item = $collection[$i];
}

But time and time again, I see the latter used.


Second: What happens if your collection isn't neatly ordered? In this example, the size of the collection is 2, and the for loop will completely miss things. This rarely happens, but can be very frustrating when it does.
$collection = array(1,2,3); unset($collection[1]);
for ($i=0; $i<count($collection); $i++) {
$item = $collection[$i];
}

Third: I cannot think of more than one situation where I've ever had to do anything other that forward-traversal. So why waste time with a for() loop unless you really, really need it. Consider the readability of this code below, and the nightmare people can experience when trying to maintain your code.
This was some code for interacting with an open office document, extending the DOM:
if ($stylesList->length > 0) {
$styles = $stylesList->item(0)->getElementsByTagName('style');

if ($styles) {
//Load styles
for ($i = 0; $i < $styles->length; $i++) {
$style = $styles->item($i);
$name = strtolower($style->getAttribute('name'));
$this->styles[$name] = $style;
}
}
}
and this is the same code, after refactoring:
$nodeList = $this->getElementsByTagNameNS(self::XMLNS_OFFICE, 'automatic-styles');

if (!self::checkNodeList($nodeList)) {
return array();
}

$node = $nodeList->item(0);
$nodeList = $node->getElementsByTagNameNS(self::XMLNS_STYLE, 'style');

if (!self::checkNodeList($nodeList)) {
return array();
}

foreach ($nodeList as $node) {
$name = strtolower($node->getAttributeNS(self::XMLNS_STYLE, 'name'));
$this->styles[$name] = $node;
}
Notice how everything is easier to read, but the level of complexity has increased? Foreach makes life simpler...

Forth: It's faster.

Wednesday, May 23, 2007

Mock Database objects, in pearweb

Some things to get you started. pearweb is a pear installable version of the pear website and package management tools.

It would be fair to say it's a little bit... old. However, there are signs of life in the old beast yet, with a slow but sure unit testing suite being built up.

I'm a phpunit junkie, but I haven't used half of the features in it. As betrand points out, there's a lot of complexity under the hood of phpunit.

It's probably for that reason that pearweb have rolled their own very simple unit testing framework. It provides all of your basics; assertTrue, assertFileExists, and what have you. It's called PEAR_PHPTest.

That isn't what I want to talk about, though. I want to talk about the implementation of mock database objects in pearweb, for unit testing purposes.

So: a unit test isn't a unit test if it relies on a database. At work, we usually don't care, and just expect a database to be there. pearweb is a little bit more strict.

So pearweb uses PEAR::DB, and to implement a mock database object, they have created a mock db driver.

To use it is pretty simple, if a little tedious. I'm sure some kind of code generator helper would save heaps of time here.

All you do is:
$mock->addDataQuery("SELECT * FROM categories ORDER BY name",
array(array('id' => 1,
'parent' => null,
'name' => 'test',
'summary' => null,
'description' => 'hi there',
'npackages' => 0,
'pkg_left' => 0,
'pkg_right' => 0,
'cat_left' => 1,
'cat_right' => 2)),
array('id', 'parent', 'name', 'summary', 'description', 'npackages', 'pkg_left',
'pkg_right', 'cat_left', 'cat_right'));


The first parameter is obviously the sql you are executing, the second is an array of result arrays, the third is the columns you expect in the result.

This means you can freeze the exact state of an object in the database, and run unit tests against it, without having to hit the database. Say bye bye to slow tests!


There are methods to simulate updating, inserting, and deleting too.

$sql = "INSERT INTO notes (id,uid,nby,ntime,note) VALUES(%s,%s,'%s','%s','%s')";
$sql = sprintf($sql, $data['id'], $data['uid'], $data['nby'], $data['ntime'], $data['note']);
$mock->addInsertQuery($sql, array(), 1);


So what, you say. That's pretty pointless, you say. Well, not if you expect a sequence of database queries to happen, and happen exactly how you want them to.

For that, we have the queries property of a mock db object.


$db = DB::factory('mock');

$myObject = new MyObject();
$myObject->removeLosers();

$phpunit->assertTrue($db->queries ==
array("SELECT id FROM losers",
"DELETE FROM users");


With a little bit of help from a code generator, there's a definite potential to never be in the dark about what queries were executed again.

Neat, hey. In order to bring some of this goodness to the rest of you, I've logged #11107: Add a PEAR::DB driver for mock objects and #11108: add a PEAR::MDB2 driver for mock objects.

Even if neither of those bugs get implemented, you can always grab the code from CVS

Monday, May 14, 2007

Developing PHP applications for the command line

So you have a problem. You've got your entire application neatly made, and you've glued it to a web interface. Unfortunately, what can be done by simply typing in a few details comes with a lot of overhead when you add in html - you have to render everything, output forms, do security so people don't get at your sensitive administration tools...

Ugh. Hard work, isn't it. Many of these things can be fixed with a good command line application. You can then rely on server-level permissions, you can quickly type, and you can set things up to run with cron jobs.

So what's out there on PHP in the command line? A few articles with Hello world? What if you want something more?

This is my solution - it's not for everyone and has its limitations.

CommandLineApplication.php

<?php
require_once 'Console/Getopt.php';
require_once 'Console/Table.php';
require_once 'Console/ProgressBar.php';

/**
* A generic class for building command line applications on top of
*/
class CommandLineApplication {
protected $path;
protected $options;

private static $stdin;
private static $stdout;
private static $stderr;

public static function main(CommandLineApplication $application) {
$options = $application->parseArguments();

$application->dispatch($options);
}

public function defaults() {
return array();
}

public function getConsoleOptions() {
return array(array(), array());
}

public function parseArguments() {
global $args;

$con = new Console_Getopt;
$args = $con->readPHPArgv();

list($shortOptions, $longOptions) = $this->getConsoleOptions();

try {
$lines = $con->getopt($args, $shortOptions, $longOptions);
} catch (PearWrapperException $pwe) {
die($pwe->ggetMessage());
}


$defaults = $this->defaults();

$found = array();

$options = $lines[0];

foreach ($defaults as $key => $value) {
foreach ($options as $option) {
if ($option[0] == $key) {
$found[$key] = true;
}
}
}

foreach ($defaults as $key => $value) {
if (empty($found[$key])) {
array_unshift($options, array($key, $value));
}
}

$this->setOptions($options);

return $this->getOptions();
}

public function setOption($name, $value) {
$this->options[$name] = $value;
}
public function getOption($name) {
return $this->options[$name];
}

public function className($name) {
return sprintf('%s_Command_%s', get_class($this), $name);
}

public function resolveCommand($name) {
return $name . ".php";
}

public function dispatch($options) {

foreach ($options as $option) {
$name = str_replace('--','',$option[0]);

$file = $this->resolveCommand($name);
if (file_exists($file)) {
$class_name = $this->className($name);
require_once $file;
$class = new $class_name();
$class->handle($option, $this);
} else {
$this->setOption($name, $option[1]);
}
}
}

public function setOptions($options) {
$this->options = $options;
}

public function getOptions() {
return $this->options;
}

/**
* Singleton method to open an input stream.
*/
public static function inputStream() {
if (!isset(self::$stdin)) {
self::$stdin = fopen('php://stdin', 'r');
}
return self::$stdin;
}

public static function outputStream() {
if (!isset(self::$stdout)) {
self::$stdout = fopen('php://stdout', 'w+');

}
return self::$stdout;
}

public static function errorStream() {
if (!isset(self::$stderr)) {
self::$stderr = fopen('php://stderr', 'w+');
}
return self::$stderr;
}

public static function prompt($text) {
$stdin = self::inputStream();
$stdout = self::outputStream();


$text .= "\n";
if (PEAR_OS != 'Windows') {
$text = Console_Color::convert("%g" . $text . "%n");
}

fwrite($stdout, $text);

//[Yn]
$pattern = '/\[(.*)\]/';
$matches = array();
if (preg_match($pattern, $text, $matches) == 0) {
return null;
}

$choices = str_split(strtolower($matches[1]));
while (($choice = fread($stdin,1)) !== FALSE) {
if (in_array($choice, $choices)) {
return $choice;
} else {
fwrite($stdout, $text);
}
}

return null;
}
}


To use it, you simply extend it

main.php

<?php
require_once 'CommandLineApplication.php';

class AccountingApplication extends CommandLineApplication {

public function getConsoleOptions() {
$longOptions = array(
'help==',
'invoice',
'path=',
'type=',
'id=',
'start=',
'end=',
'valfirm=',
'client='
);

$shortOptions = array();

return array($shortOptions, $longOptions);
}

public function defaults() {
$defaults = array();
$defaults['--type'] = 'recipient';
$defaults['--start']= '*';
$defaults['--end'] = '*';
$defaults['--valfirm'] = '*';

return $defaults;
}

public function className($name) {
return 'Accounting_Command_' . $name;
}

public function resolveCommand($name) {
return dirname(__FILE__) . "/commands/" . $name . ".php";
}
}

class AccountingException extends Exception {}

AccountingApplication::main(new AccountingApplication());


and then, as needed, implement invididual commands

commands/invoice.php

<?php
define('DATE_CALC_FORMAT', "%Y-%m-%d");

require_once 'Console/Table.php';
require_once 'Console/Color.php';
require_once 'Date/Calc.php';


class Accounting_Command_Invoice {
public function handle($options, AccountingApplication &$application) {
$start = $application->getOption('start');
$end = $application->getOption('end');

if (empty($start)) {
$start = strtotime(Date_Calc::beginOfPrevMonth());
}

if (empty($end)) {
$end = strtotime(Date_Calc::beginOfMonth());
}

$cl_id = $application->getOption('client');
$valfirm_id = $application->getOption('valfirm');


if ($valfirm_id == '*') {
$valfirm = new ValuationFirmOrganisation();
$valfirm->setName('All valuation firms');
} else {
$valfirm = new ValuationFirmOrganisation();
}

$client = new ClientOrganisation($cl_id);

$invoices = new RecipientInvoice();
printf("Generating\n\tfrom\t%s\n\tto\t%s\n\tfor\t%s\n\tby\t%s\n\n", date("Y-m-d H:i:s", $start), date("Y-m-d H:i:s", $end), $client->getName(), $valfirm->getName());
$paths = $invoices->generateInvoices($start, $end, $valfirm->getID(), $client->getID());

if (empty($paths)) {
print "No files generated\n";
} else {
print "Generated:\n";
foreach ($paths as $path) {
print "\t" . $path . "\n";
}
}
}
}


Let's break this down into it's individual parts.

You run it as follows
php main.php --start=2007-01-12 --client=8 --invoice

First, this instantiates a new AccountingApplication
AccountingApplication::main(new AccountingApplication());

This then parses the arguments and dispatches them.

public static function main(CommandLineApplication $application) {
$options = $application->parseArguments();

$application->dispatch($options);
}


Argument parsing is done by Console_GetOpt. In your child class, you've defined getConsoleOptions() and defaults(). defaults() are the default arguments, and getConsoleOptions() is the available commands.

This assumes console_getopt will throw an exception - if you are using PEAR::isError(), you'll want to check the return value.


public function parseArguments() {
global $args;

$con = new Console_Getopt;
$args = $con->readPHPArgv();

list($shortOptions, $longOptions) = $this->getConsoleOptions();

try {
$lines = $con->getopt($args, $shortOptions, $longOptions);
} catch (Exception $e) {
die($e->getMessage());
}


$defaults = $this->defaults();

$found = array();

$options = $lines[0];

foreach ($defaults as $key => $value) {
foreach ($options as $option) {
if ($option[0] == $key) {
$found[$key] = true;
}
}
}

foreach ($defaults as $key => $value) {
if (empty($found[$key])) {
array_unshift($options, array($key, $value));
}
}

$this->setOptions($options);

return $this->getOptions();
}


Next, we dispatch. To do this, we iterate over the command line options.

If something has been entered, first we try to resolve the command name to a file - pretty much an __autoload(). This is resolveCommand().

If we can't match anything - there are no special handlers - we just set it as an option for the application.

public function dispatch($options) {

foreach ($options as $option) {
$name = str_replace('--','',$option[0]);

$file = $this->resolveCommand($name);
if (file_exists($file)) {
$class_name = $this->className($name);
require_once $file;
$class = new $class_name();
$class->handle($option, $this);
} else {
$this->setOption($name, $option[1]);
}
}
}


Finally, we've dispatched to the individual command. It's a good idea to enforce the 'command that does whatever you want' to be put at the end - ie, php main.php --option=value --action. This ensures that all options have been set before the guts of the application starts to work.

In this case, we've made an invoice generator. ValuationFirmOrganisation and ClientOrganisation are just models, more or less; and RecipientInvoices is a generator for the actual invoice files. Previously what is done in Accounting_Command_Invoice would have been tied to processing form information from a webpage, and executing the below code.


<?php
define('DATE_CALC_FORMAT', "%Y-%m-%d");

require_once 'Console/Table.php';
require_once 'Console/Color.php';
require_once 'Date/Calc.php';


class Accounting_Command_Invoice {
public function handle($options, AccountingApplication &$application) {
$start = $application->getOption('start');
$end = $application->getOption('end');

if (empty($start)) {
$start = strtotime(Date_Calc::beginOfPrevMonth());
}

if (empty($end)) {
$end = strtotime(Date_Calc::beginOfMonth());
}

$cl_id = $application->getOption('client');
$valfirm_id = $application->getOption('valfirm');


if ($valfirm_id == '*') {
$valfirm = new ValuationFirmOrganisation();
$valfirm->setName('All valuation firms');
} else {
$valfirm = new ValuationFirmOrganisation();
}

$client = new ClientOrganisation($cl_id);

$invoices = new RecipientInvoice();
printf("Generating\n\tfrom\t%s\n\tto\t%s\n\tfor\t%s\n\tby\t%s\n\n", date("Y-m-d H:i:s", $start), date("Y-m-d H:i:s", $end), $client->getName(), $valfirm->getName());
$paths = $invoices->generateInvoices($start, $end, $valfirm->getID(), $client->getID());

if (empty($paths)) {
print "No files generated\n";
} else {
print "Generated:\n";
foreach ($paths as $path) {
print "\t" . $path . "\n";
}
}
}
}


So what's next? Say you need to process user input. You can use the handy helper method CommandLineApplication::prompt(). You pass in a string, and in square brackets, you put in the 1 character options.

$result = CommandLineApplication::prompt("Do you want to continue? [yn]")

When it finds a match, it returns it into $result for you.

What else? Got an error? Output it to stderr.


$stderr = CommandLineApplication::errorStream();

fwrite($stderr, "An error occured!");


Got a long wait? This is where Console_ProgressBar really helps you (from the pear examples for this package).

require_once 'Console/ProgressBar.php';

print "This will display a very simple bar:\n";
$bar = new Console_ProgressBar('%bar%', '=', ' ', 76, 3);
for ($i = 0; $i <= 3; $i++) {
$bar->update($i);
sleep(1);
}
print "\n";


Got tabular information to display? Console_Table rocks here.

$stdout = CommandLineApplication::outputStream();

$data = array();
foreach ($clients as $id) {
$client = new ClientOrganisation($id);
$data[] = array(count($client->users), $client->getName());
}


$table = new Console_Table();
fwrite($stdout, $table->fromArray(array('Total users', 'Client'), $data));


All in all, you've now got a pretty handy little kit for building quick command line applications.