Friday, 22 April 2011

Enforcing target descriptions within build files with a Git hook

When automating mundane tasks of a project or development environment with a build tool like Phing or Ant, the driving build file will naturally accumulate several targets and tasks over time. To ease the build file acceptance within a team and at a later stage also the contribution rate by team members, it's crucial that all build targets have a description attribute to provide at least a rough outline of the build features at hand. When these attributes are in place the (potential) build file user will get such an outline by executing the build tool's list command (phing -l or ant -p). To get a better picture of the problem at hand imagine a project poorly covered with tests and your personal attitude towards extending it or just take a peek at the screenshot below showing a very poorly documented build file.

A poorly documented build file in Phing's list view

To overcome this accumulation of some sort of technical debt (i.e. poorly documented targets) there are various options at hand. The first one, not covered in this blog post, would be to add a pursuant test which verifies the existence of a description for every target/task of the build file under test. As it's very uncommon, at least from what I've heard, to have your build files covered by tests; the next thinkable approach would be to use a Git pre-commit hook to guard your repository/ies against the creeping in of such poorly documented build files.

The next listing shows such a Git hook (also available via GitHub) scribbled away in PHP, which detects any build file(s) following a common build file naming schema (i.e. build.xml|build.xml.dist|personal-build.xml|…) , prior to the actual commit. For every target element in the detected build file(s) it's then verified that it has a description attribute and that it's actual content is long enough to carry some meaning. If one of those two requirements aren't met, the commit is rejected while revealing the build file smells to the committer, so she can fix it, as shown in the outro screenshot. Happy build file sniffing.

#!/usr/bin/php
<?php
define('DEPENDENT_EXTENSION', 'SimpleXML');

if (!extension_loaded(DEPENDENT_EXTENSION)) {
    $consoleMessage = sprintf(
        "Skipping build file checks as the '%s' extension isn't available.", 
        DEPENDENT_EXTENSION
    );
    echo $consoleMessage . PHP_EOL;
    exit(0);
}

define('MIN_TARGET_DESCRIPTION_LENGTH', 10);
define('TARGET_DESCRIPTION_ATTRIBUTE', 'description');
define('TARGET_NAME_ATTRIBUTE', 'name');
define('CHECK_DESCRIPTION_LENGTH', true);

$possibleBuildFileNames = array(
    'build.xml.dist',
    'build.xml-dist',
    'build-dist.xml',
    'build.xml',
    'personal-build.xml'
);

$violations = getAllBuildFileViolationsOfCommit($possibleBuildFileNames);
fireBackPossibleViolationsAndExitAccordingly($violations);

function getAllBuildFileViolationsOfCommit(array $possibleBuildFileNames)
{
    $filesOfCommit = array();
    $gitCommand = 'git diff --cached --name-only';
    
    exec($gitCommand, $filesOfCommit, $commandReturnCode);
    
    $allViolations = array();
    foreach ($filesOfCommit as $file) {
      if (in_array(basename($file), $possibleBuildFileNames)) {
          $violations = checkBuildFileForViolations($file);
          if (count($violations) > 0) {
            $allViolations[$file] = $violations;
          }
      }
    }

    return $allViolations;
}

/**
 *  @param  array $allViolations
 *  @return void
 */
function fireBackPossibleViolationsAndExitAccordingly(array $allViolations)
{
    if (count($allViolations) > 0) {
        foreach ($allViolations as $buildFile => $violations) {

            $buildFileConsoleMessageHeader = sprintf("Build file '%s':", $buildFile);
            echo $buildFileConsoleMessageHeader . PHP_EOL;

            foreach ($violations as $violationMessage) {
                $buildFileConsoleMessageLine = sprintf(" + %s", $violationMessage);
                echo $buildFileConsoleMessageLine . PHP_EOL;
            }
        }
        if (count($allViolations) > 1) {
            $rejectCommitConsoleMessage = sprintf(
                "Therefore rejecting the commit of build files [ %s ].", 
                implode(', ', array_keys($allViolations))
            );
        } else {
            $rejectCommitConsoleMessage = sprintf(
                "Therefore rejecting the commit of build file [ %s ].", 
                implode(', ', array_keys($allViolations))
            );
        }

        echo $rejectCommitConsoleMessage . PHP_EOL;
        exit(1);
    }
    exit(0);
}
/**
 *  @param  string $buildfile
 *  @return array
 */
function checkBuildFileForViolations($buildFile) {
    if (!file_exists($buildFile)) {
        return array();
    }

    $buildfileXml = file_get_contents($buildFile);
    $buildXml = new SimpleXMLElement($buildfileXml);
    $allBuildTargets = $buildXml->xpath("//target");
    $violations = array();

    if (count($allBuildTargets) > 0) {

        $targetsWithNoDescription = $targetsWithTooShortDescription = array();

        foreach ($allBuildTargets as $buildTarget) {

            $actualTragetAttributes = $buildTarget->attributes();
            $allUsedTragetAttributes = array();
            $actualTargetName = null;

            foreach ($actualTragetAttributes as $attribute => $value) {
                $allUsedTragetAttributes[] = $attribute;

                if ($attribute === TARGET_NAME_ATTRIBUTE) {
                    $actualTargetName = $value;
                }

                if (CHECK_DESCRIPTION_LENGTH === true && 
                    $attribute === TARGET_DESCRIPTION_ATTRIBUTE && 
                    strlen($value) < MIN_TARGET_DESCRIPTION_LENGTH) 
                {
                    $targetsWithTooShortDescription[] = $actualTargetName;
                }
            }   

            if (!in_array(TARGET_DESCRIPTION_ATTRIBUTE, $allUsedTragetAttributes)) {
                $targetsWithNoDescription[] = $actualTargetName;
            }
        }
        if (count($targetsWithNoDescription) > 0) {
            if (count($targetsWithNoDescription) > 1) {
                $violations[] = sprintf(
                    "Build targets [ %s ] don't have mandatory descriptions.", 
                    implode(', ', $targetsWithNoDescription)
                );
            } else {
                $violations[] = sprintf(
                    "Build target [ %s ] doesn't have a mandatory description.", 
                    implode(', ', $targetsWithNoDescription)
                );
            }
        }

        if (count($targetsWithTooShortDescription) > 0) {
            if (count($targetsWithTooShortDescription) > 1) {
                $violations[] = sprintf(
                    "Build targets [ %s ] don't have an adequate target description length.", 
                    implode(', ', $targetsWithTooShortDescription),
                    MIN_TARGET_DESCRIPTION_LENGTH
                );
            } else {
                $violations[] = sprintf(
                    "Build target [ %s ] doesn't have an adequate target description length.", 
                    implode(', ', $targetsWithTooShortDescription),
                    MIN_TARGET_DESCRIPTION_LENGTH
                );
            }
        }
    }
    return $violations;
}
Non-Descriptive Phing build files rejected by a Git hook

Saturday, 20 November 2010

Measuring & displaying Phing build times with buildhawk

Recently I installed a Ruby gem called buildhawk which allows to measure and display the build times of Rake driven builds. As I like the idea behind this tool a lot but mostly use Phing for build orchestration, it was time to explore the possibility to interconnect them both. In this blog post I'll show an implementation of an apposite Phing Logger gathering the buildhawk compatible build times via git note(s) and how to put the interplay between those two tools to work.

Logging on

As mentioned above the build time of each build is stored as a git note and associated to the repository's HEAD, reflecting the current state of the system under build (SUB), which assumes that the SUB is versioned via Git. The next shown Phing Logger (i.e. BuildhawkLogger) grabs the overall build time by hooking into the buildFinished method of the extended DefaultLogger class, transforms it into a buildhawk specific format and finally adds it as a git note.
<?php

require_once 'phing/listener/DefaultLogger.php';

/**
 *  Writes a build event to the console and store the build time as a git notes in the   
 *  project's repository HEAD.
 *
 *  @author    Raphael Stolt <raphael.stolt@gmail.com>
 *  @see       BuildEvent
 *  @link      https://github.com/xaviershay/buildhawk Buildhawk on GitHub
 *  @package   phing.listener
 */
class BuildhawkLogger extends DefaultLogger {
    
    /**
     *  @var string
     */
    private $_gitNotesCommandResponse = null;

    /**
     *  Behaves like the original DefaultLogger, plus adds the total build time 
     *  as a git note to current repository HEAD.
     *
     *  @param  BuildEvent $event
     *  @see    BuildEvent::getException()
     *  @see    DefaultLogger::buildFinished
     *  @link   http://www.kernel.org/pub/software/scm/git/docs/git-notes.html
     */
    public function buildFinished(BuildEvent $event) {
        parent::buildFinished($event);
        if ($this->_isProjectGitDriven($event)) {
            $error = $event->getException();
            if ($error === null) {
                $buildtimeForBuildhawk = $this->_formatBuildhawkTime(
                    Phing::currentTimeMillis() - $this->startTime
                );
                if (!$this->_addBuildTimeAsGitNote($buildtimeForBuildhawk)) {
                    $message = sprintf(
                        "Failed to add git note due to '%s'",
                        $this->_gitNotesCommandResponse
                    );
                    $this->printMessage($message, $this->err, Project::MSG_ERR);
                }
            }
        }
    }
    
    /**
     * Checks (rudimentary) if the project is Git driven
     *
     *  @param  BuildEvent $event
     *  @return boolean
     */
    private function _isProjectGitDriven(BuildEvent $event)
    {
        $project = $event->getProject();
        $projectRelativeGitDir = sprintf(
            '%s/.git', $project->getBasedir()->getPath()
        );
        return file_exists($projectRelativeGitDir) && is_dir($projectRelativeGitDir);
    }
    
    /**
     *  Formats a time micro integer to buildhawk readable format.
     *
     *  @param  integer The time stamp
     */
    private function _formatBuildhawkTime($micros) {
        return sprintf("%0.3f", $micros);
    }
    
    /**
     *  Adds the build time as a git note to the current repository HEAD
     *
     *  @param  string  $buildTime The build time of the build
     *  @return mixed   True on sucess otherwise the command failure response
     */
    private function _addBuildTimeAsGitNote($buildTime) {
        $gitNotesCommand = sprintf(
            "git notes --ref=buildtime add -f -m '%s' HEAD 2>&1",
            $buildTime
        );
        $gitNotesCommandResponse = exec($gitNotesCommand, $output, $return);
        if ($return !== 0) {
            $this->_gitNotesCommandResponse = $gitNotesCommandResponse;
            return false;
        }
        return true;
    }
}

Putting the Logger to work

As the buildhawk logger is available via GitHub you can easily grab it by issuing sudo curl -s http://gist.github.com/raw/707868/BuildhawkLogger.php -o $PHING_HOME/listener/BuildhawkLogger.php. The next step, making the build times loggable, is achieved by using the -logger command line argument of the Phing Cli and specifying the buildhawk logger name or the path to it. In case you want the buildhawk logger to be used per default (it behaves like the default logger if the SUB isn't Git driven/managed) you can also add it to the Phing shell script.

The next console command issued in the directory of the SUB shows a Phing call utilizing the BuildhawkLogger, assumed it has been installed at $PHING_HOME/listener/BuildhawkLogger.php and not been made the default logger.
phing -logger phing.listener.BuildhawkLogger

Looking at them Phing build times

Now it's time to switch to buildhawk and let it finally perform it's designated task, rendering an with the commit SHAs, commit messages, and build times fed Erb template into an informative, viewable HTML page. To install it you simply have to run sudo gem install buildhawk and you're good to go.

The next console command shows the buildhawk call issued in the SUB's directory to produce it's build time report page.
buildhawk --title 'Examplr' > examplr-build-times.html
The outro screenshot below gives you a peek at a rendered build time report.Buildhawk report for a Phing driven build

Thursday, 3 June 2010

Growling PHPUnit's test status

PHPUnit Growl TestListenerTwo years ago I blogged about a Xinc (R.I.P?) plugin that growls each build status for any via Xinc continuously integrated project. Since I'm using PHPUnit more and more lately, especially in continuous testing sessions (sprints without hitting the continuous integration server), my dependence on a fast and more visual feedback loop rose. In this post I'll provide an easy solution that meets these requirements by utilizing PHPUnit's test listener feature.

What's the motivation, yo?

While doing story or feature sprints embedded in a continuous testing approach I first used a combination of stakeout.rb and PHPUnit's --colors option to radiate the tests status, but soon wasn't that satisfied with the chosen route as it happened that the console window got superimposed with other opened windows (e.g. API Browser, TextMate etc.) especially on my 13,3" MacBook.

To overcome this misery I decided to utilize PHPUnit's ability to write custom test listeners and to implement one that radiates the test status in a more prominent and sticky spot via Growl.

Implementing the Growl test listener

Similar to the ticket listener plugin mechanism I blogged about earlier PHPUnit also provides one for test listeners. This extension mechanism allows to bend the test result formatting and output to the given needs and scenarios a developer might face and therefore is a perfect match.

To customize the test feedback and visualization the test listener has to implement the provided PHPUnit_Framework_Testlistener interface. A few keystrokes later I ended up with the next shown implementation, which is also available via a GitHub gist, supporting the previous stated requirements.
<?php

class PHPUnit_Extensions_TestListener_GrowlTestListener 
    implements PHPUnit_Framework_Testlistener
{
    const TEST_RESULT_COLOR_RED = 'red';
    const TEST_RESULT_COLOR_YELLOW = 'yellow';
    const TEST_RESULT_COLOR_GREEN = 'green';
    
    private $_errors = array();
    private $_failures = array();
    private $_incompletes = array();
    private $_skips = array();
    private $_tests = array();
    private $_suites = array();
    private $_endedSuites = 0;
    private $_assertionCount = 0;
    private $_startTime = 0;

    private $_successPicturePath = null;
    private $_incompletePicturePath = null;
    private $_failurePicturePath = null;

    /**
     * @param string $successPicturePath
     * @param string $incompletePicturePath
     * @param string $failurePicturePath
     */
    public function __construct($successPicturePath, $incompletePicturePath, 
        $failurePicturePath)
    {
        $this->_successPicturePath = $successPicturePath;
        $this->_incompletePicturePath = $incompletePicturePath;
        $this->_failurePicturePath = $failurePicturePath;
    }

    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        $this->_errors[] = $test->getName();
    }
    
    public function addFailure(PHPUnit_Framework_Test $test, 
        PHPUnit_Framework_AssertionFailedError $e, $time) 
    {     
        $this->_failures[] = $test->getName();
    }
    
    public function addIncompleteTest(PHPUnit_Framework_Test $test, 
        Exception $e, $time)
    {
        $this->_incompletes[] = $test->getName();
    }
    
    public function addSkippedTest(PHPUnit_Framework_Test $test, 
        Exception $e, $time) 
    {
        $this->_skips[] = $test->getName();
    }
    
    public function startTest(PHPUnit_Framework_Test $test)
    {
    
    }
    
    public function endTest(PHPUnit_Framework_Test $test, $time) 
    { 
        $this->_tests[] = array('name' => $test->getName(), 
            'assertions' => $test->getNumAssertions()
        );
        $this->_assertionCount+= $test->getNumAssertions();
    }
    
    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        if (count($this->_suites) === 0) {
            PHP_Timer::start();
        }
        $this->_suites[] = $suite->getName();
    }
    
    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        $this->_endedSuites++;
        
        if (count($this->_suites) <= $this->_endedSuites)
        {
            $testTime = PHP_Timer::secondsToTimeString(
                PHP_Timer::stop());

            if ($this->_isGreenTestResult()) {
                $resultColor = self::TEST_RESULT_COLOR_GREEN;
            }
            if ($this->_isRedTestResult()) {
                $resultColor = self::TEST_RESULT_COLOR_RED;
            }
            if ($this->_isYellowTestResult()) {
                $resultColor = self::TEST_RESULT_COLOR_YELLOW;
            }

            $suiteCount = count($this->_suites);
            $testCount = count($this->_tests);
            $failureCount = count($this->_failures);
            $errorCount = count($this->_errors);
            $incompleteCount = count($this->_incompletes);
            $skipCount = count($this->_skips);

            $resultMessage = '';

            if ($suiteCount > 1) {
                $resultMessage.= "Suites: {$suiteCount}, ";
            }
            $resultMessage.= "Tests: {$testCount}, ";
            $resultMessage.= "Assertions: {$this->_assertionCount}";

            if ($failureCount > 0) {
                $resultMessage.= ", Failures: {$failureCount}";
            } 

            if ($errorCount > 0) {
                $resultMessage.= ", Errors: {$errorCount}";
            }

            if ($incompleteCount > 0) {
                $resultMessage.= ", Incompletes: {$incompleteCount}";
            }

            if ($skipCount > 0) {
                $resultMessage.= ", Skips: {$skipCount}";
            }
            $resultMessage.= " in {$testTime}.";
            $this->_growlnotify($resultColor, $resultMessage);
        }
    }

    /**
     * @param string $resultColor
     * @param string $message
     * @param string $sender The name of the application that sends the notification
     * @throws RuntimeException When growlnotify is not available
     */
    private function _growlnotify($resultColor, $message = null, $sender = 'PHPUnit')
    {
        if ($this->_isGrowlnotifyAvailable() === false) {
            throw new RuntimeException('The growlnotify tool is not available');
        }
        $notificationImage = $this->_getNotificationImageByResultColor(
            $resultColor);
        $command = "growlnotify -w -s -m '{$message}' "
                 . "-n '{$sender}' "
                 . "-p 2 --image {$notificationImage}";
        exec($command, $response, $return);
    }

    /**
     * @return boolean
     */
    private function _isGrowlnotifyAvailable()
    {
        exec('growlnotify -v', $reponse, $status);
        return ($status === 0);
    }

    /**
     * @param string $color 
     * @return string
     */
    private function _getNotificationImageByResultColor($color)
    {
        switch ($color) {
            case self::TEST_RESULT_COLOR_RED:
                return $this->_failurePicturePath;
                break;
            case self::TEST_RESULT_COLOR_GREEN:
                return $this->_successPicturePath;
                break;
            default:
                return $this->_incompletePicturePath;
        }
    }

    /**
     * @return boolean
     */
    private function _isGreenTestResult()
    {
        return count($this->_errors) === 0 && 
               count($this->_failures) === 0 &&
               count($this->_incompletes) === 0 &&
               count($this->_skips) === 0;
    }

    /**
     * @return boolean
     */
    private function _isRedTestResult()
    {
        return count($this->_errors) > 0 ||
               count($this->_failures) > 0;
    }

    /**
     * @return boolean
     */
    private function _isYellowTestResult()
    {
        return count($this->_errors) === 0 &&
               count($this->_failures) === 0 &&
               (count($this->_incompletes) > 0 ||
                count($this->_skips) > 0);
    }
}

Hooking the Growl test listener into the PHPUnit ecosystem

To make use of the just outlined test listener it's necessary to add an entry to PHPUnit's XML configuration file telling PHPUnit which test listener class to utilize and where it's located in the file system. In a next step the images for the three possible Growl notifications have to be added to the local file system, and as the Growl test listener constructor takes these as arguments they have also to be injected in the PHPUnit XML configuration file (i.e. phpunit-offline.xml). Take a peek yourself how this is done in the next listing.
<phpunit backupGlobals="false"
         backupStaticAttributes="true"
         bootstrap="bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="true"
         stopOnFailure="true"
         syntaxCheck="true"
         testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader">
  <testsuites> 
    <testsuite name="Zend_Service_GitHub Offline Testsuite">
      <directory>Zend/Service/GitHub</directory>
      <directory>Zend/Service</directory>
    </testsuite>
  </testsuites>
  <groups>
    <include>
      <group>offline</group>
    </include>
  </groups>
  <listeners>
    <listener class="PHPUnit_Extensions_TestListener_GrowlTestListener" 
              file="/Users/stolt/Work/GrowlTestListener.php">
     <arguments>
       <string>$HOME/Pictures/pass.png</string>
       <string>$HOME/Pictures/pending.png</string>
       <string>$HOME/Pictures/fail.png</string>
     </arguments>
    </listener>
  </listeners>
</phpunit>

Putting the Growl test listener to work

Attention shameless plug! As an example application for a continuous testing session I chose a Zend Framework Service component I'm currently working on. To set up the continuously testing workflow, stakeout.rb is still my #1 choice, but in a recent blog post Andy Stanberry shows another tool dubbed Kicker which seems to be coequal. The following console snippet shows in a concrete scenario how to utilize stakeout.rb to watch for any changes on the Zend_Service_GitHub component or it's backing tests which immediately trigger the test suite execution if one is detected.
stakeout.rb 'phpunit --configuration phpunit-offline.xml' **/*.{php} ../Zend/**/*.php ../Zend/Service/**/*.php
In the classic TDD cycle we start with a failing test. Creating the test, adding the assertions that the system under test (SUT) has to fulfill and saving the according test class automatically triggers the test suite execution which ends up in the next shown Growl notification.

Growl notice for failed tests

Nest a très important client call comes in and since we are clever, a quick TextMate shortcut marks the currently worked on test as incomplete. This step might be a bit controversy as it's also suggested to leave the last worked on test broken, but I got to show you the pending/incomplete Growl notification ;D

Growl notice for incomplete tests

After finishing the 'interruptive' client call aka context switch we can continue to work on the feature of the SUT until it fulfills the expected behavior which will be radiated via the next shown Growl notification. Happy Growl flavored testing!

Growl notice for successful tests

* As you might notice in the shown Growl notification images there's a test suite count of 9 while we are only operating on a single one, this seems to be a possible PHPUnit bug, or just a misconfiguration of my testing environment.

In case you got a solution for this problem feel free to add an illuminating comment.

Saturday, 15 May 2010

Installing the PHP redis extension on Mac OS X

Recently I took a look at Redis, a popular and advanced key-value store. Peeking at the supported languages section of the project's website you'll notice a lot of client libraries available for PHP. Two out of them caught my particular attention: Rediska due to it's impressive Zend Framework integration and phpredis as it's a native PHP extension written in C and therefore supposed to be blazingly faster than vanilla PHP client libraries. The following blog post will show how to install and configure the aforementioned, native PHP extension on a Mac OS X system.

The next steps assume that you've installed redis on your machine. In case you are using MacPorts and haven't installed the key-value store yet, all it takes are the following two commands and you're good to go. In case you prefer Homebrew for managing your package/software installations, there's also a Formula for redis available that allows you to install it via brew install redis.

sudo port install redis
sudo launchctl load -w /Library/LaunchDaemons/org.macports.redis.plist
The very first step for building the native PHP redis extension is to get the source code by cloning the GitHub repository of the extension without it's history revisions.
mkdir phpredis-build
cd phpredis-build
git clone --depth 1 git://github.com/nicolasff/phpredis.git
cd phpredis
The next task is to compile the extension with the following batch of commands.
phpize
./configure
make
sudo make install
The next to last step is to alternate your php.ini, use php --ini | grep 'Loaded' to get the location of it on your system, so that the redis module/extension is available to your PHP ecosystem. Therefor simply add extension=redis.so in the Dynamic Extensions section of your php.ini. Afterwards you can verify that the redis module is loaded and available via one of the following commands.
php -m | grep redis
php -i | grep 'Redis Support'
To make the extension also available to the running Apache PHP module you'll need to restart the Apache server. Looking at phpinfo()'s output in a browser you should see the entry shown in the next image.

Enabled redis extension

For testing the communication between the just installed redis extension and the running Redis server, I further created a simple test script called redis-glue-test.php you can fetch from GitHub and run via the next commands.
curl -s http://gist.github.com/raw/402018/redis-glue-test.php -o redis-glue-test.php
php redis-glue-test.php
When you see the following shown console output you're good to go. Happy Redising!

Output of redis-glue-test.php

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;
}
}