Zend_Db_Table dynamic finders

The recently added Zend_Navigation component uses dynamic finders to find pages e.g. findOneByLabel(‘Home’) to return the first matching page with label Home (and that’s straight from the manual).
It would be nice if Zend_Db_Table could do this too but it can’t. This seems a little inconsistent to me. Why do it for some components and not others?

Anyway, adding it wasn’t that difficult. The first step is to have an (abstract?) class extend Zend_DB_Table and let all your Db-backed models extend that. I’ll call mine App_Db_Table.

<?php

abstract class App_Db_Table extends Zend_Db_Table_Abstract {

	/**
	 * Db Instance
	 */
	protected $_db;

	/**
	 * Call method used to implement the dynamic finders
	 *
	 * @param string
	 * @param array
	 * @return function || void
	 */
	public function __call($method, $args) {
		$watch = array('findBy','findAllBy');
		foreach ($watch as $found) {
			if (stristr($method, $found)) {
				$fields = str_replace($found, '', $method);
				return $this->{'_' . $found}($fields, $args);
			}
		}
		throw new Exception("Call to undefined method App_Db_Table::{$method}()");
	}

	/**
     * Initialize object
     *
     * Called from {@link __construct()} as final step of object instantiation.
     */
    public function init() {
    	$this->_db = Zend_Registry::get('db');
    }

	/**
	 * Find By
	 *
	 * This method only ever returns the first record it finds!
	 *
	 * @param 	string
	 * @param 	array
	 * @return  object|false
	 */
	protected function _findBy($columns, $args) {
		$stmt = $this->_buildQuery($columns, $args);
		return $this->fetchRow($stmt);
	}

	/**
	 * Find All By
	 *
	 * @param 	string
	 * @param 	array
	 * @return	 object|false
	 */
	protected function _findAllBy($columns, $args) {
		$stmt = $this->_buildQuery($columns, $args);
		return $this->fetchAll($stmt);
	}

	/**
	 * Builds the query for the findBy methods
	 *
	 * @param 	string
	 * @param 	array
	 * @return 	Zend_Db_Select
	 */
	protected function _buildQuery($columns, $args) {
		$fields = explode('And', $columns);
		$count = count($fields);

		$where = "{$this->_filterField($fields[0])} = ?";
		$where_args = $args[0];
		unset($args[0]);

		$select = $this->select();
		$select->where($where, $where_args);

		if ($count > 1) {
			array_shift($fields);
			foreach ($fields as $field) {
				$where = "{$this->_filterField($field)} = ?";
				$where_args = array_shift($args);
				$select->where($where, $where_args);
			}
		}
		return $select;
	}

	/**
	 * Converts a camelCased word into an underscored word
	 *
	 * @param 	string
	 * @return  string
	 */
    protected function _underscore($word) {
    	$word = preg_replace('/([A-Z]+)([A-Z])/', '\1_\2', $word);
		return strtolower(preg_replace('/([a-z])([A-Z])/', '\1_\2', $word));
    }

    /**
     * Converts field name to lowercase and if camelcased, converts to underscored
     *
     * @param string
     * @return string
     */
    protected function _filterField($item) {
    	$item = $this->_underscore($item);
    	return strtolower($item);
    }

}

 

Usage

I’ll use a couple of unit tests to drive the class. I’ll assume a User class extends App_Db_Table. The table schema for my test is:

CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 32 ) NOT NULL ,
`email` VARCHAR( 64 ) NOT NULL ,
`city` VARCHAR( 32 ) NOT NULL ,
`state` VARCHAR( 32 ) NOT NULL ,
`favourite_colour` VARCHAR( 16 ) NOT NULL
)

And the relevant section of the test class is as shown:

...
/**
 * Add  a user to the users table
 *
 * @param   array   $attributes
 */
private function _createUser($attributes = array()) {
    $details = array(
        'username'  => 'john',
        'email'     => 'john@example.com'
        'city'      => 'Colchester',
        'state'     => 'Essex',
        'favourite_colour' => 'red'
    );
    $params = array_merge($details, $attributes);
    $user = new User();
    $user->createRow($params)->save();
}

/**
 * Test findBy Dynamic finder with one parameter
 */
public function testFindByWithOneParameter() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByEmail('john@example.com');
	$this->assertEquals(1, count($found));

	$found = $finder->findByEmail('smith@example.com');
	$this->assertFalse($found);
}

/**
 * Test findBy Dynamic finder with underscored column names
 */
public function testFindByWithUnderscoredColumnNames() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByFavouriteColour('red');
	$this->assertEquals(1, count($found));
}

/**
 * Test findBy dynamic finder with more than one parameter
 */
public function testFindByWithMultipleParameters() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByStateAndCity('Essex', 'Colchester');
	$this->assertEquals('Essex', $found->state);

	$found = $finder->findByStateAndCity('Essex', 'London');
	$this->assertFalse($found);

	$found = $finder->findByStateAndCity('London', 'Colchester');
	$this->assertFalse($found);
}

/**
 * Test findAllBy Dynamic finder with one parameter
 */
public function testFindAllByWithOneParameter() {
	$this->_createUser();
	$this->_createUser(array(
        'username'  => 'smith',
        'email'     => 'smith@example.com'
    ));

    $finder = new User();

	$found = $finder->findAllByState('Essex');
	$this->assertEquals(2, count($found));

	$found = $finder->findAllByUsername('john');
	$this->assertEquals(1, count($found));

	$found = $finder->findAllByEmail('smith');
	$this->assertEquals(0, count($found));
}
...

No related posts.

This entry was posted in Zend Framework. Bookmark the permalink.

3 Responses to Zend_Db_Table dynamic finders

  1. baphled says:

    Nice write up, am hoping this functionality will be in Jara :D

  2. Jérôme says:

    Create job, thank you.

  3. tommy says:

    Just wanted to say I really liked the site. You have really put a lot of time into your posts and it is just great!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>