coudenysj Jachim Coudenys on web technology

21Nov/120

Using multiple databases in phpunit/dbunit with composer

I'm using multiple databases in most of my projects, so having access to multiple databases in my test suite is a must.

phpunit/dbunit is excellent, but you are stuck with one database. The guys at Etsy created very good extensions to fix this problem (MultipleDatabase), but it took me a while to figure out how to use it.

Because PHPUnit is now available via Composer, you can fetch all dependencies with a single command.

The composer.json file to include all dependencies looks like this:

Install the dependencies by (installing and) running composer:

Now you can create a "parent" class to register the databases (mine is called DatabaseTest). Make sure you create a getDatabaseConfigs method (which is required and should return an array of PHPUnit_Extensions_MultipleDatabase_Database). For the fixtures, I use Xml Datasets, which look like this.

I've added a getConnection method, so I can use the same assertions as the normal dbunit testcase (see Database Assertions API):

The magic about to happen is quite cool. PHPUnit will read these database configs and use them to make sure all databases and tables are in a known state before every test and does this in following order:

  1. Connect to all databases
  2. TRUNCATE all tables supplied in the database fixture file
  3. Insert all rows supplied in the fixture file
  4. Execute the test
  5. TRUNCATE all tables supplied in the database fixture file

Now you'll be able to run tests for code making changes in your database without affecting other tests.

Some improvements:

  • Add composer support to Hamcrest (once Hamcrest PHP has moved to GitHub).
  • Add composer support to the Etsy extensions (I've submitted a PR, but it won't work until Hamcrest PHP is on GitHub).
  • See if the getConnection() method can be done in a better way.
30Mar/104

Custom URL’s in Zend Framework

I am searching for a url solution in Zend Framework for quite a while now, and I hope the solution I came up with helps other people trying to do the same thing.

More important, I hope more experienced (Zend Framework) developers can point out problems or suggest better solutions for this problem.

The CMS of my company makes it possible for users to create a tree structure of products/sections/chapters/etc... with an unlimited depth. This makes the url they 'generate' very variable.

I've searched the internet for solutions and for quite a while I used the solution of Jani. Because this completely overrides the Router object, I continued my search via the Zend MVC mailing list and had an extensive look at the Zend_Controller_Router manual page.

During the search Ivo Jansch tweeted about ZF URL's and pointed me towards chaining, but the solution for my specific problem lies in adding Routes to the route stack.

I wanted to override the default route so that a language is required in the url. To accomplisch this I added an new function in the Bootstrap.php file:

protected function _initLanguageRoute()
{
    $this->bootstrap('frontController');
    $fc = $this->frontController;
    $router = $fc->getRouter();
    $route = new Zend_Controller_Router_Route(
        ':language/:controller/:action/*',
        array(
            'module' => 'default',
            'language' => 'nl',
            'controller' => 'index',
            'action' => 'index'
        ),
        array(
            'language' => '[a-z]{2}'
        ),
        $translator
    );
    $router->addRoute('default', $route);
}

To add a variable route (calculated by the CMS), I extended the Zend_Controller_Router_Route_Abstract class so the route could also be used by the Url View helper.

The database I use below, has calculated colums already, but a custom mapping can be calculated in the class as well. As long as the request object is pointed to some controller and action.

class Coudenysj_Controller_Router_Route_Mapping extends Zend_Controller_Router_Route_Abstract
{

    private $_db;


    public function __construct($db)
    {
        $this->_db = $db;
    }

    /**
     * A method to publish the way the route operates.
     *
     * @see Zend/Controller/Router/Rewrite.php:392
     *
     * @return int
     */
    public function getVersion()
    {
        return 1;
    }

    /**
     * The required functions (required by Interface).
     *
     * @param Zend_Config $config The config object with defaults.
     *
     * @return void
     */
    public static function getInstance(Zend_Config $config)
    {
        return;
    }

    /**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param string $path Path used to match against this routing map
     *
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($path)
    {
        $path = trim($path, '/');

        return $this->_db->fetchRow(
            'SELECT lang, controller, action, id
            FROM mapping
            WHERE url = ?;',
            array($path)
        );
    }

    /**
     * Assembles a URL path defined by this route
     *
     * @param array   $data    An array of variable and value pairs used as parameters
     * @param boolean $reset   Not used (required by interface)
     * @param boolean $encode  Not used (required by interface)
     * @param boolean $partial Not used (required by interface)
     *
     * @return string|false Route path with user submitted parameters
     */
    public function assemble($data = array(), $reset = false, $encode = false, $partial = false)
    {
        if (!isset($data['language'])) {
            if (Zend_Registry::isRegistered('Zend_Locale')) {
                $locale = Zend_Registry::get('Zend_Locale');
                $data['language'] = $locale->getLanguage();
            } else {
                return false;
            }
        }
        if (   !isset($data['controller'])
            || !isset($data['action'])
            || !isset($data['id'])
        ) {
            return false;
        }

        $result = $this->_db->fetchRow(
            'SELECT url
            FROM mapping
            WHERE lang = ? AND controller = ? AND action = ? AND id = ?;',
            array(
                $data['language'],
                $data['controller'],
                $data['action'],
                $data['id'],
            )
        );
        return $result['url'];
    }
}

Now I can add a new 'cms' route to the Router object in the Bootstrap.php file:

protected function _initMappingRoute()
{
    $db = new Zend_Db::factory(...);
    $this->bootstrap('frontController');
    $fc = $this->frontController;
    $router = $fc->getRouter();
    $router->addRoute(
        'cms', new Coudenysj_Controller_Router_Route_Mapping($db)
    );
}

Remember that the route stack is processed backwards, so the 'cms' route is the first one to try and route the url.

This route can now also be used in the view:

echo $this->url(
    array(
        'id' => $id,
        'controller' => $controller,
        'action' => $action
    ),
    'cms' // the name of our route
);

This solution works for me (for now) and suggestions to improve it are very welcome!