Down & Dirty with MongoDB Part 3: PHP ToDo

Earlier this summer, we kicked off a series on MongoDB where the goal was to write a simple todo application using native MongoDB drivers and three of our favorite scripting languages.

This article is part 3 in that series, in which we write said application in PHP.

The Interface

In case you missed it you can look back at the introductory article for the complete details, but real quick I’ll review interface here:

  • todo – list all incomplete tasks sorted by priority then chronologically
  • todo next – list all incomplete tasks that are high priority
  • todo done – list all complete tasks chronologically
  • todo high “pay bills” – add high priority task called “pay bills”
  • todo low “get milk” – add low priority task called “get milk”
  • todo finish “pay bills” – complete task called “pay bills”
  • todo dont “get milk” – delete task called “get milk”
  • todo help – list all commands and their usage

Looks simple enough, let’s dive in!

The Driver

10gen distributes the PHP extension as a PECL package, or you can install from source. For more details, refer to the PHP.net Guide

We will go ahead and just use PECL, which is as easy as running one command.

pecl install mongo

You will also need to edit your php.ini file to add the following line, and probably reload your server.

extension=mongo.so

Go ahead and start a file called ToDo.php and stub out the first few lines. Since this is a command line application, it’s a bit different from a standard PHP script. We need to add a shebang and we’ll go ahead and change our error reporting levels to keep things quiet. Changing your error reporting is optional, if you aren’t sure of your coding skills, just leave the error reporting at full blast, you can turn it down when we finish the app.

#!/usr/bin/env php
< ?php

error_reporting( E_ERROR | E_PARSE );

The ToDo Class

PHP5 has great support for object-oriented programming, so we will employ a single object of a class we’ll write called ToDo. The class will take in the required command-line arguments, connect to the MongoDB collection that stores our todos, and execute the appropriate commands on them.

To facilitate these needs, our object will need to take some arguments from the user and store a reference to the MongoDB collection when it is instantiated:

  class ToDo {

    protected $argv = array();
    protected $mongo_connection = null;
    protected $mongo_db = null;

    public function __construct ( $argv ) {
      $this->argv = $argv;
      try {
        $this->mongo_connection = $mongo = new Mongo();
        $this->mongo_db = $this->mongo_connection->todo;
      }
      catch ( Exception $e ) {
        throw new Exception ( "Oops! Error connecting to MongoDB: " . $e->getMessage() );
     }
   }

The __construct function is the PHP5 way of adding a constructor to a class. This is called when you instantiate an object, like this for instance:

$my_todo = new ToDo( $argv );

When a new ToDo object is instantiated, our class will store the arguments passed in to it in an protected member called $argv and set up a connection to MongoDB in a member called $mongo_connection. Mongo is a class provided by the Mongo extension we installed earlier.

For convenience we will set up an instance of MongoDB in the member called $mongo_db. We do this since we will only be using one database.

Our ToDo class expects some arguments passed in to the constructor, so we need to provide them when we instantiate it. These arguments will be read in from the command-line when the program is executed, and PHP exposes these to our program via the $argv global variable.

However, $argv only exists in certain execution contexts of PHP, so we need to make sure we are executing from the command line. To ascertain this, we look at the constant PHP_SAPI, then we instantiate a ToDo object with the $argv array.

  if( 'cli' == PHP_SAPI ) {
    try {
      $app = new ToDo( $argv );
    }
    catch( Exception $e ) {
      print $e->getMessage() . "\n";
    }
  }

At this point, the ToDo.php should look like this:

#!/usr/bin/env php
<?php
   error_reporting( E_ERROR | E_PARSE );
   class ToDo {
     protected $argv = array();
     protected $mongo_connection = null;
     protected $mongo_db = null;
     public function __construct ( $argv ) {
       $this->argv = $argv;
      try {
        $this->mongo_connection = $mongo = new Mongo();
        $this->mongo_db = $this->mongo_connection->todo;
      }
      catch ( Exception $e ) {
        throw new Exception ( "Oops! Error connecting to MongoDB: " . $e->getMessage() );
      }
    }
  }

  if( 'cli' == PHP_SAPI ) {
    try {
      $app = new ToDo( $argv );
    }
    catch( Exception $e ) {
      print $e->getMessage() . "\n";
    }
  }

You can execute this program from the command-line, but nothing visible will happen. We still need to implement the bulk of the program, which parses the user’s command-line arguments and acts appropriately.

A Little Help

The first thing we’ll implement is also the simplest, a method that prints usage help for the program. It looks like this:

protected function help ( $error = null ) {
  if( ! is_null( $error ) ) {
    print "$error\n";
    print "\n";
  }

  $out = < <<EOF usage: {$this->argv[0]} <method>

== Methods ==

<none>             list all incomplete tasks sorted by priority then chronologically
help               show this help
next               list all incomplete tasks that are high priority
done               list all complete tasks chronologically
high <argument>    add high priority task called </argument><argument>
low </argument><argument>     add low priority task called </argument><argument>
finish </argument><argument>  complete task called </argument><argument>
dont </argument><argument>    delete unfinished task called </argument><argument>

EOF;
  print $out;
}

Run, Baby, Run

Now our application needs to be able to actually do it’s job, so lets create a method called run which will handle the program’s flow. In this method we examine the commands the user has given and call our other methods to get that work done. The default action is to list all incomplete tasks, and the catch-all is to execute the help method we implemented above. Here is what run looks like:

public function run () {
	try {
		if ( 1 >= count( $this->argv ) ) {
			$this->show(
				array( "complete" => false ),
				array( "level" => 1, "added" => 1 )
			);
		}
		else {
			$argument = strtolower( $this->argv[1] );
			if ( "help" == $argument )
				$this->help();
			else if ( "next" == $argument )
				$this->show(
					array( "level" => "high", "complete" => false ),
					array( "level" => 1, "added" => 1 )
				);
			else if ( "done" == $argument )
				$this->show(
					array( "complete" => array( '$ne' => false ) ),
					array( 'completed' => 1 )
				);
			else if ( "high" == $argument )
				$this->add( "high", $this->argv[2] );
			else if ( "low" == $argument )
				$this->add( "low", $this->argv[2] );
			else if ( "finish" == $argument )
				$this->complete( true, $this->argv[2] );
			else if ( "dont" == $argument )
				$this->complete( false, $this->argv[2] );
			else
				throw new Exception( "Bad Method" );
		}
	}
	catch ( Exception $e ) {
		$this->help( $e->getMessage() );
	}
}

$this->argv is an Array we were given when the ToDo constructor was run. It should hold all of the options and arguments that the user provided when they ran the application from the command line. If there is only one or fewer arguments we use a default action. Otherwise we move into the switch statement and choose a method there.

One item worth noting is that we assume that $this->argv[0] is the name of the program itself, which is how PHP (and most other languages) handle command line arguments.

Using Mongo

So far we’ve only dealt with setting up our class and parsing user arguments, we’ve hardly event glanced at MongoDB! Time to change that! I’ll work through each of the methods referenced in run

First, the show method:

protected function show ( $params = null, $sort = null ) {
  $cursor = $this->mongo_db->todos->find( $params );
  if( ! is_null( $sort ) )
    $cursor->sort( $sort );
  foreach ( $cursor as $row )
    print $row['task'] . "\n";
}

This method takes an array of selection parameters and an array of sort options (both optional) and passes them directly to the MongoDB cursor. The cursor will then let us extract the matching documents one at a time, sorted and returned as arrays. This style of interface is pretty easy to work with, it’s one of the great benefits of using a document store like MongoDB.

Now let’s find a way to store those item’s in MongoDB in the first place. This requires us to implement the add method.

protected function add ( $level, $item ) {
  if ( 2 >= count( $this->argv ) )
    throw new Exception( "missing argument" );
  $this->mongo_db->todos->insert( array(
    "task" => $item,
    "level" => $level,
    "complete" => false,
    "added" => time()
   ) );
}

Our add method takes two arguments. $level is the importance of the item, either “high” or “low”. $item is the text of the item itself. Once you have these two facts you can quickly pass them on to MongoDB’s insert method.

The insert method in MongoDB takes an associative array and persists it to the datastore as a document. You can store just about anything you like this way, as long as it can be serialized.

The final method we need is a bit more versatile, the complete method. This method can either finish a task or remove it from the system.

protected function complete ( $finish, $item ) {
  if ( 2 >= count( $this->argv ) )
    throw new Exception( "missing argument" );
  $document = $this->mongo_db->todos->findOne( array( "complete" => false, "task" => $item ) );
  if ( is_null( $document ) )
    print "No Matching ToDo Found\n";
  else {
    if ( $finish ) {
      $document['complete'] = time();
      $this->mongo_db->todos->save( $document );
    }
    else {
      $this->mongo_db->todos->remove( $document );
    }
  }
}

This method uses a variant of find (which we used in show) called findOne. Think of this as the MongoDB version of LIMIT 0,1. After we’ve tried to find a task matching the requested title, we then either abort (if not found) or complete (if found).

When completing we will use one of two MongoDB methods, depending on whether we are finishing or skipping the task. The save method will be used to finish a task. This method takes a MongoDB document that was previously retrieved and saves it back into the database. Any changes you make to the object (like setting the completed time, for instance) is persisted to the database. This is done through a unique (but not necessarily sequential) ID given to each document in the datastore.

To delete a task from our list we need to use remove. This method takes an existing document object (with a document id) and deletes it from the database.

Finishing Up

So there you have it, we’ve completed all of our methods. Now all we have to do is call run and we should have a working todo program!

if( 'cli' == PHP_SAPI ) {
  try {
    $app = new ToDo( $argv );
    $app->run();
  }
  catch( Exception $e ) {
    print $e->getMessage() . "\n";
  }
}

You can view/download the program in its entirety here. One thing to note is how little datastore code is needed to add CRUD to an application when using MongoDB as a datastore.

Hopefully this article has helped you with PHP or MongoDB, or both. Stay tuned for our next article in the series when we write the exact same application in Python!

John Hobbs is a prolific software developer from the midwest who dabbles in anything he can get his hands on. Find more about John on his website, GitHub account, or Twitter account.

 

If you liked this article, please help spread the news on the following sites:

  • Bump It
  • Blend It
  • Bookmark on Delicious
  • Stumble It
  • Float This
  • Reddit This
  • Share on FriendFeed
  • Clip to Evernote