After working through this problem on my latest application, I've come to the conclusion that 1: I do, in fact, really like collection classes. and 2: Collection classes really really need to a)Iterate and b)return the SQL that created them
To me it really does make the most sense when you start actually doing stuff with your models that are written in this style.
Code: Select all
$users = new UserList(array('role'=>'Administrator'));
foreach ($users as $user) {
echo $user->getUsername();
}
echo $person->getSQL();
For instance, you would normally load the collections in your controllers, and pass the desired collection to the view for rendering. The view can easily iterate over them and display stuff. Or, if you want to debug stuff, you can get the SQL printed out in the view.
In order to make collections act like this there's just no way around having a class act as a wrapper for the database result set. And rather than doing this wrapper stuff in each and every collection class...well, we make a base class out of it. So basically, if I want that style of code, I have to have some sort of result handler.
Zend_db however, did make the finder method of the collections much cleaner than I had been doing. I no longer have to explicitly declare each and every field in the table. I only need to declare fields that I can find on from other table (joins).
I was also able to make the model constructors optionally take an array of data for hydrating the objects. This saved a database call per row in the result set. For very large database results, my database server thanks me.
After today, here is my new collection class style, with flexible finding. I haven't implemented the sort, limit, and groupBy support yet, but it should be easy to add back into my find() method.
Code: Select all
class PersonList extends ZendDbResultIterator
{
/**
* @param array $fields
*/
public function __construct($fields=null)
{
parent::__construct();
if (is_array($fields)) {
$this->find($fields);
}
}
/**
* @param array $fields
* @param string $sort
* @param string $limit
* @param string $groupBy
*/
public function find($fields=null,$sort='lastname',$limit=null,$groupBy=null)
{
$this->select->from('people');
if (count($fields)) {
foreach ($fields as $key=>$value) {
$this->select->where("$key=?",$value);
}
}
$this->populateList();
}
/**
* Hydrates all the objects from a database result set
*
* @return array An array of Role objects
*/
protected function loadResult($key)
{
return new Person($this->result[$key]);
}
}
Now, in order to find from other tables, you certainly have to do the joins. And without wanting to declare all the normal fields we needed some way to know what the normal fields were. Zend_db was able to provide. Which means now, I only have to add in code for the extra findable fields as I'm developing the application
Code: Select all
class UserList extends ZendDbResultIterator
{
private $columns;
/**
* @param array $fields
*/
public function __construct($fields=null)
{
parent::__construct();
$this->columns = $this->zend_db->describeTable('users');
if (is_array($fields)) {
$this->find($fields);
}
}
/**
* @param array $fields
* @param string $sort
* @param string $limit
* @param string $groupBy
*/
public function find($fields=null,$sort='username',$limit=null,$groupBy=null)
{
$this->select->from(array('u'=>'users'));
if (count($fields)) {
foreach ($fields as $key=>$value) {
if (array_key_exists($key,$this->columns)) {
$this->select->where("u.$key=?",$value);
}
}
}
// Finding on fields from other tables required joining those tables.
// You can add fields from other tables to $options by adding the join SQL
// to $this->joins here
$joins = array();
if (isset($fields['firstname'])) {
$joins['p'] = array('table'=>'people','condition'=>'u.id=p.user_id');
$this->select->where('p.firstname=?',$fields['firstname']);
}
if (isset($fields['lastname'])) {
$joins['p'] = array('table'=>'people','condition'=>'u.id=p.user_id');
$this->select->where('p.lastname=?',$fields['lastname']);
}
if (isset($fields['email'])) {
$joins['p'] = array('table'=>'people','condition'=>'u.id=p.user_id');
$this->select->where('p.email=?',$fields['email']);
}
if (isset($fields['role'])) {
$joins['ur'] = array('table'=>'user_roles','condition'=>'u.id=ur.user_id');
$joins['r'] = array('table'=>'roles','condition'=>'ur.role_id=r.id');
$this->select->where('r.name=?',$fields['role']);
}
foreach ($joins as $key=>$join) {
$this->select->joinLeft(array($key=>$join['table']),$join['condition']);
}
$this->populateList();
}
/**
* Hydrates all the objects from a database result set
*
* @return array An array of objects
*/
protected function loadResult($key)
{
return new User($this->result[$key]);
}
}
And now, instead of my old, raw PDOResultIterator, I needed that base class to handle Zend_db results. I was able to keep all my old SPL implementations, and just swap out the populate() method. Meaning the complexity of the class dropped significantly.
Code: Select all
abstract class ZendDbResultIterator implements ArrayAccess,SeekableIterator,Countable
{
protected $zend_db;
protected $select;
protected $result = array();
private $valid = false;
private $cacheEnabled = true;
private $cache = array();
private $key;
abstract public function find($fields=null,$sort='',$limit=null,$groupBy=null);
abstract protected function loadResult($key);
public function __construct()
{
$this->zend_db = Database::getConnection();
$this->select = new Zend_Db_Select($this->zend_db);
}
/**
* Runs the query and stores the results
*/
protected function populateList()
{
$this->result = array();
$this->result = $this->zend_db->fetchAll($this->select);
}
/**
* @return string
*/
public function getSQL()
{
return $this->select->__toString();
}
// Array Access section
/**
* @param int $offset
* @return boolean
*/
public function offsetExists($offset) {
return array_key_exists($offset,$this->result);
}
/**
* Unimplemented stub requried for interface compliance
* @ignore
*/
public function offsetSet($offset,$value) { } // Read-only for now
/**
* Unimplemented stub requried for interface compliance
* @ignore
*/
public function offsetUnset($offset) { } // Read-only for now
/**
* @param int $offset
* @return mixed
*/
public function offsetGet($offset)
{
if ($this->offsetExists($offset)) {
return $this->loadResult($offset);
}
else {
throw new OutOfBoundsException('Invalid seek position');
}
}
// SPLIterator Section
/**
* Reset the pionter to the first element
*/
public function rewind() {
$this->key = 0;
}
/**
* Advance to the next element
*/
public function next() {
$this->key++;
}
/**
* Return the index of the current element
* @return int
*/
public function key() {
return $this->key;
}
/**
* @return boolean
*/
public function valid() {
return array_key_exists($this->key,$this->result);
}
/**
* @return mixed
*/
public function current()
{
return $this->loadResult($this->key);
}
/**
* @param int $index
*/
public function seek($index)
{
if (isset($this->result[$index])) {
$this->key = $index;
}
else {
throw new OutOfBoundsException('Invalid seek position');
}
}
/**
* @return Iterator
*/
public function getIterator()
{
return $this;
}
// Countable Section
/**
* @return int
*/
public function count()
{
return count($this->result);
}
}
Now, I'm off to go rest, but tomorrow...time to commit all this new stuff to my scaffolding!