It’s been a busy few months. I’ve been busy at Totsy.com a startup that is taking the best of Lithium, MongoDB, and my time. If you want the lowdown of what I’m talking about check out the slides from Mitch Pirtle CTO at Totsy.

To the business at hand – Simple ACL with Lithium and MongoDB. Let’s get down to it!

The heart of this ACL functionality rests with the Li3 Router. With the Router you can connect a URL to a specific Class::action. This is handy when you want to have /login route to app\controllers\UsersController::login(). Of course, your routes can become more complicated and robust with a little regex goodness. Take a look at a default route for example:

    Router::connect('/{:controller}/{:action}/{:args}');

When going to the url http://yourapp.com/users/login/23423  then Li3 will map to the users controller, login action (or method) and pass the 23423 as the argument or input to that method. That’s all we really need to hookup a route and in turn give us the ability to setup ACL.

If there are no routes hooked up in your app/config/routes.php then requests will go nowhere. Without some default routes hooked up you’ll app will be sitting dead without anywhere to go. This is the perfect time to setup an ACL. How do we get this all going?

The next step is to call upon MongoDB. With MongoDB we can embed an array with the route and parameter into a mongo document object. I would embed this array with the user document that has the information used for login. This way, you can pull the entire document with one query from Mongo. Too simple to be true? Lets take a step by step approach:

1) Hook up your code in routes.php to loop through the document pulled from your user document:

    if (isset($session['acls'])) {
        foreach ($session['acls'] as $acl) {
             Router::connect($acl['route'], $acl['connection']);
        }
    }

Note that in my app I put the necessary information in the users session after they successfully login.

2) Embed the acls array as part of your user document schema. For example:

{
    "_id" : ObjectId("4c8d53e0ce64e5f449992400"),
    "created_date" : "Sun Sep 12 2010 18:27:44 GMT-0400 (EDT)",
    "email" : "test@lcs.com",
    "firstname" : "Test",
    "lastname" : "User",
    "password" : "b119670d125b0bd4271b25ce39fa166bc3bf79a0",
    "acls" : [
        {
            "route" : "/orders",
            "connection":"Orders::index"
        },
        {
           "route" : "/posts/view/{:item:[a-z0-9\-]+}",
           "connection":"Events::view"
        }
    ]
}

3) Do a happy dance because you’re done!!!

With a little admin work you can put together groups and more robust configurations for ACL.  At the end of the day all you really need to do is loop through an array of routes and connections and you’ll have the exact ACL you need to keep everyone in order.

Posted in PHP.

Mission: Find all the items in a users shopping cart that expired 3 minutes ago.

If you want to cheat just scroll to the last code block. ;) Now for the setup.

This is a common query for any ecommerce website that wants to keep things “fresh” or run some analytics. Private sales sites are fond of this because an item in a shopping cart means that a customer is holding up inventory. That of course is a bad thing. This is just one of MANY reason why searching by date range is important.

So how do we handle this with Lithium our RAD PHP 5.3+ framework and MongoDB? To answer this lets use the mission above as our basis and spit out some code. Since you didn’t skip ahead lets build up our example just a little.

Let’s start in a BaseModel that will hold our core code. We will first assume that we want to query for some standard time values. To get things started we can setup a human readable protected array:

	protected $_dates = array(
		'now' => 0,
		'-1min' => -60,
		'-3min' => -180,
		'-5min' => -300,
		'15min' => 900
	);
That times above are second that we will either add or subtract from the php method time(). Our calculation will basically be time() + $_dates['key']. The result of that calculation needs to be converted into a MongoDate object. This is important for several reasons. One good reason is that its easier for us to query using dates if Mongo knows we are looking for date. We may also save ourselves some trouble if more native MongoDB temporal calculations are added to the core.

Using the MongoDate object you can do things like greater than ($gt), less than ($lt) etc, etc and well etc. So how do we create this object? We need to instantiate the date with the Mongo Driver PHP MongoDate class. Since we are using static methods in our Model we’ll use the following code to create and return our MongoDate based on the name-key we pass the method. We can place this method in the BaseModel as well.
	public static function dates($name) {
	     return new MongoDate(time() + static::_object()->_dates[$name]);
	}
That done, we can now save and query for data. The code below demonstrates how we can add some created and expire MongoDates to a cart document. The really important part to glean here is that we are calling our static method to create the MongoDate. This method will live in our CartModel which can extend the BaseModel.
	public static function addDates($product, array $options = array()) {
		$product->expires = static::dates('15min');
		$product->created = static::dates('now');
		return static::_object()->save($product);
	}

Note: The MongoDate object will look something like “Thu Aug 05 2010 01:32:41 GMT-0400 (EDT)”. This throws a few folks off thinking its just a string.

Now for our mission. Don’t worry I’ll repeat it here to save you from looking: Find all the items in a users shopping cart that expired 3 minutes ago. How would we do that in the CartsController? All we need to do is call a static method in the CartModel that does the search and let it know what range we are looking for:

    $cart = Cart::search(array('time' => '-3min'));

We’re going to let our model do the heavy lifting to get the data.

	public static function search($params = null) {
		$time = (!empty($params['time'])) ? $params['time'] : 'now';
		$user = Session::read('userLogin');
		return static::all(array(
			'conditions' => array(
				'session' => Session::key(),
				'expires' => array('$gte' => static::dates($time)),
				'user' => $user['_id'])),
			'order' => array('expires' => 'ASC')
		));
	}

There is a bit going on here so lets break it down. First we are checking to make sure that the time is there and set it if it’s not. Then for the sake of finding a specific users cart item we doing some ID grabbing from the PHP Session. Next is the query part: Give me all the user cart items that are greater than X date.

Notice in the code above that we are calling static::dates($time) to fetch our MongoDate. This will let MongoDB in turn know that we are searching for a date and query for it appropriately. This helps to cut out all the epoch timestamp manipulation in our code to properly search for values.

Whew! Now you are an expert and have enough ammunition to use MongoDates to the full.

If you were like me you started throwing all kinds of files into MongoDB with GridFS. When you took a look at the db.fs.files collection you saw something like this for a document:

	
{
	"_id" : ObjectId("4c40affcce64e5e275c60100"),
	"filename" : "My First File.jpg",
	"uploadDate" : "Fri Jul 16 2010 15:16:12 GMT-0400 (EDT)",
	"length" : 55162,
	"chunkSize" : 262144,
	"md5" : "46aa378be7f6f1f3660efd7de5c1cbb6"
}

Did you see the MD5 hash? It’s there for a reason you know.

Since my PHP/MongoDB application has an administrative backend multiple people are loading up files. There is always a possibility that they will upload the same file. Of course this would be a very inefficient use of storage especially when the file is a video or picture.  That’s where the MD5 field in fs.files comes in handy.

In PHP you can use the md5_file() method to get the MD5 hash before you save the file to MongoDB. Running a findOne query using the md5 of your tmp file will let you know if  a document for that file already exists. If it does exist, then you’ll get back the fs.files document of the preloaded file. Then you can use the _id as a reference and don’t bother saving the file. Can you imagine all the money you save in storage fees on Amazon S3?

This is a very common and reliable way of doing things since byte for byte you know the files are the same. The sample script below is a snapshot of code in a Lithium application (Lithium is a new PHP 5.3+ framework). I’m basically running a findOne({“md5″ : “$md5″}) query:

	
protected function write() {
		$success = false;
		$grid = File::getGridFS();
		$this->fileName = $this->request->data['Filedata']['name'];
		$md5 = md5_file($this->request->data['Filedata']['tmp_name']);
		$file = File::first(array('conditions' => array('md5' => $md5)));
		if ($file) {
			$success = true;
			$this->id = (string) $file->_id;
		} else {
			$this->id = (string) $grid->storeUpload('Filedata', $this->fileName);
			if ($this->id) {
				$success = true;
			}
		}
		return $success;
}