Tuesday 16 March 2010

Using MongoHq in Zend Framework based applications

MongoHq logoAs the name slightly foreshadows MongoHq is a currently bit pricey cloud-based hosting solution for MongoDb databases provided by CommonThread. Since they went live a few weeks ago I signed up for the small plan and started to successfully re-thinker with it in an exploratory Zend Framework based application.

Therefore the following post will show how to bootstrap such an instance into a Zend Framework based application and how to use it from there in some simple scenarios like storing data coming from a Zend_Form into a designated collection and vice versa fetching it from there.

Bootstrapping a MongoHq enabled connection

To establish and make the MongoDb connection application-wide available the almighty Zend_Application component came to the rescue again. After reading Matthew Weier O'Phinney's enlightening blog post about creating re-usable Zend_Application resource plugins and deciding to use MongoDb in some more exploratory projects, I figured it would be best to create such a plugin and ditch the also possible resource method approach.

The next code listing shows a possible implementation of the MongoDb resource plugin initializing a Mongo instance for the given APPLICATION_ENV (i.e. production) mode.

For the other application environment modes (development | testing | staging) it's currently assumed that no database authentication is enabled, which is also the default when using MongoDb, so you might need to adapt the plugin to your differing needs; and since I'm currently only rolling on the small plan the support for multiple databases is also not accounted for.

library/Recordshelf/Resource/MongoDb.php
<?php

class Recordshelf_Resource_MongoDb
extends Zend_Application_Resource_ResourceAbstract
{
/**
* Definable Mongo options.
*
* @var array
*/
protected $_options = array(
'hostname' => '127.0.0.1',
'port' => '27017',
'username' => null,
'password' => null,
'databasename' => null,
'connect' => true
);
/**
* Initalizes a Mongo instance.
*
* @return Mongo
* @throws Zend_Exception
*/
public function init()
{
$options = $this->getOptions();

if (null !== $options['username'] &&
null !== $options['password'] &&
null !== $options['databasename'] &&
'production' === APPLICATION_ENV) {
// Database Dns with MongoHq credentials
$mongoDns = sprintf('mongodb://%s:%s@%s:%s/%s',
$options['username'],
$options['password'],
$options['hostname'],
$options['port'],
$options['databasename']
);
} elseif ('production' !== APPLICATION_ENV) {
$mongoDns = sprintf('mongodb://%s:%s/%s',
$options['hostname'],
$options['port'],
$options['databasename']
);
} else {
$exceptionMessage = sprintf(
'Recource %s is not configured correctly',
__CLASS__
);
throw new Zend_Exception($exceptionMessage);
}
try {
return new Mongo($mongoDns, array('connect' => $options['connect']));
} catch (MongoConnectionException $e) {
throw new Zend_Exception($e->getMessage());
}
}
}
With the MongoDb resource plugin in the place to be, it's time to make it known to the boostrapping mechanism which is done by registering the resource plugin in the application.ini.

Further the MongoHq credentials, which are available in the MongoHq > My Database section, and the main database name are added to the configuration file which will be used to set the definable resource plugin ($_)options and to connect to the hosted database.

application/configs/application.ini

[production]
pluginPaths.Recordshelf_Resource = "Recordshelf/Resource"
resources.mongodb.username = __MONGOHQ_USERNAME__
resources.mongodb.password = __MONGOHQ_PASSWORD__
resources.mongodb.hostname = __MONGOHQ_HOSTNAME__
resources.mongodb.port = __MONGOHQ_PORT__
resources.mongodb.databasename = __MONGOHQ_DATABASENAME__

...

Cloudifying documents into collections

Having the MongoHq enabled connection in the bootstrapping mechanism it can now be picked up from there and used in any Zend Framework application context.

The example action method (i.e. proposeAction) assumes data (i.e. a tech talk proposal to revive the example domain from my last blog post) coming from a Zend_Form which will be stored in a collection named proposals, a table in old relational database think.

The next code listings states the action method innards to do so by injecting the valid form values into a model class which provides accessors and mutators for the domain model's properties and can transform them into a proposal document aka an array structure.

application/controllers/ProposalController.php
<?php

class ProposalController extends Zend_Controller_Action
{
public function indexAction()
{
$this->view->form = new Recordshelf_Form_Proposal();
}
public function thanksAction()
{
}
public function proposeAction()
{
$this->_helper->viewRenderer->setNoRender();
$form = new Recordshelf_Form_Proposal();

$request = $this->getRequest();

if ($this->getRequest()->isPost()) {
if ($form->isValid($request->getPost())) {
$model = new Recordshelf_Model_Proposal($form->getValues());
$mapper = new Recordshelf_Model_ProposalMapper();
if ($mapper->insert($model)) {
return $this->_helper->redirector('thanks');
}
$this->view->form = $form;
return $this->render('index');
} else {
$this->view->form = $form;
return $this->render('index');
}
}
}
}
Next the model/data mappper is initialized, which triggers the picking of the MongoHq enabled Mongo connection instance and the auto-determination of the collection name to use based on the mapper's class name. Subsequently the populated model instance is passed into the mappper's insert method which is pulling the document (array structure) and doing the actual insert into the proposals collection.

To give you an idea of the actual document structure it's shown in the next listing, followed by the model/data mapper implementation.
Array
(
[state] => new
[created] => MongoDate Object
(
[sec] => 1268774242
[usec] => 360831
)

[submitee] => Array
(
[title] => Mr
[firstname] => John
[familyname] => Doe
[email] => john.doe@gmail.com
[twitter] => johndoe
)

[title] => How to get a real name
[description] => Some descriptive text...
[topictags] => Array
(
[0] => John
[1] => Doe
[2] => Anonymous
)

)


application/models/ProposalMapper.php
<?php

class Recordshelf_Model_ProposalMapper
{
private $_mongo;
private $_collection;
private $_databaseName;
private $_collectionName;

public function __construct()
{
$frontController = Zend_Controller_Front::getInstance();
$this->_mongo = $frontController->getParam('bootstrap')
->getResource('mongoDb');
$config = $frontController->getParam('bootstrap')
->getResource('config');

$this->_databaseName = $config->resources->mongodb->get('databasename');

$replaceableClassNameparts = array(
'recordshelf_model_',
'mapper'
);
$this->_collectionName = str_replace($replaceableClassNameparts, '',
strtolower(__CLASS__) . 's');

$this->_collection = $this->_mongo->selectCollection(
$this->_databaseName,
$this->_collectionName
);
}
/**
* Inserts a proposal document/model into the proposals collection.
*
* @param Recordshelf_Model_Proposal $proposal The proposal document/model.
* @return MongoId
* @throws Zend_Exception
*/
public function insert(Recordshelf_Model_Proposal $proposal)
{
$proposalDocument = $proposal->getValues();
try {
if ($this->_collection->insert($proposalDocument, true)) {
return $proposalDocument['_id'];
}
} catch (MongoCursorException $mce) {
throw new Zend_Exception($mce->getMessage());
}
}
}

Querying and retrieving the cloudified data

As what comes in must come out, the next interaction with the Document Database Management System (DocDBMS) is about retrieving some afore-stored talk proposal documents from the collection so they can be rendered to the application's user. This isn't really MongoHq specific anymore, like most of the previous model parts, and is just here to round up this blog post and use some more of that MongoDb goodness. Looks like I have to look for an anonymous self-help group that stuff is highly addictive.

Anyway the next listing shows the action method fetching all stored documents available in the proposals collection. To save some CO2 on this blog post all documents are fetched, which ends up in the most trivial query but as you can figure the example domain provides a bunch of query examples like only proposals for a given topic tag, specific talk title or a given proposal state which can be easily created via passed-through Http request parameters.

application/controllers/ProposalController.php
<?php

class ProposalController extends Zend_Controller_Action
{
...

public function listAction()
{
$mapper = new Recordshelf_Model_ProposalMapper();
$proposals = $mapper->fetchAll();
// For iterating the Recordshelf_Model_Proposal's in the view
$this->view->proposals = $proposals;
}
}
The last code listing shows the above used fetchAll method of the data mapper class returning an array of stored proposal documents mapped to their domain model (i.e. Recordshelf_Model_Proposal) in the application.

application/models/ProposalMapper.php
<?php

class Recordshelf_Model_ProposalMapper
{
...

/**
* Fetches all stored talk proposals.
*
* @return array
*/
public function fetchAll()
{
$cursor = $this->_collection->find();
$proposals = array();

foreach ($cursor as $documents) {
$proposal = new Recordshelf_Model_Proposal();
foreach ($documents as $property => $value) {
if ('submitee' === $property) {
$proposal->submitee = new Recordshelf_Model_Submitee($value);
} else {
$proposal->$property = $value;
}
}
$proposals[] = $proposal;
}
return $proposals;
}
}

3 comments:

Anonymous said...

Thank you for this, I'm about to start a new project using ZF and Mongo :)

Unknown said...

Very nice, I think I will use your resource plugin. I have a similar setup with a mapper. I only let a "fetchAll" method return a collection object (implementing iterator interface) which contains the cursor and construct the object when traversing the object.

Raphael Stolt said...

Hi Onno,

For the model part used in this post there is for sure some room for improvements. It kind of grew while writing this post, but I will take a look at your suggested approach. Thanks for sharing.