Zend_Db_Table Enhancements
The Zend Framework's Zend_Db_Table class offers plenty of features to make working with tables a breeze in PHP. You can easily insert, update, and delete rows, along with build complex select queries with Zend_Db_Table_Select.
I have subclassed Zend_Db_Table_Abstract to add my own commonly-used features, like preInsert() and preUpdate() methods, and automated support for tables using the Modified Preorder Tree Traversal algorithm.
Modified Preorder Tree Traversal
To use the modified preorder tree traversal algorithm in your table, you will initially have to do just a little bit of work but once it is set up everything should be automated for you.
First, you will need to create your table in MySQL and add two columns for the "left" and "right" values. Let's create a comments table as an example. Since "left" and "right" are reserved words in SQL, let's name these columns "lt" and "rt", but you can name them whatever you choose. You will also need to add a "parent_id" column, which references this same table's "id" column.
CREATE TABLE `comments` ( `id` bigint(20) unsigned NOT NULL auto_increment, `parent_id` bigint(20) unsigned default NULL, `name` varchar(255) NOT NULL, `subject` varchar(255) NOT NULL, `comment` text NOT NULL, `created` timestamp NOT NULL default CURRENT_TIMESTAMP, `modified` datetime default NULL, `lt` bigint(20) unsigned NOT NULL, `rt` bigint(20) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `parent_id` (`parent_id`), KEY `lt` (`lt`,`rt`), KEY `rt` (`rt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Next, you will need to create your table class by extending Virgen_Db_Table, and declare the traversal properties.
<?php
class Model_Comments extends Virgen_Db_Table
{
protected $_name = 'comments';
protected $_traversal = array(
'left' => 'lt',
'right' => 'rt',
'column' => 'id',
'refColumn' => 'parent_id'
);
}
That's it! Now when you insert a new record, the "lt" and "rt" columns will be updated as necessary to reflect the new preorder tree.
If you already have data in your table or want to rebuild the entire tree, you can use the rebuildTreeTraversal() method. Please note on large tables, this may take some time to complete.
<?php $comments = new Model_Comments(); $comments->rebuildTreeTraversal();
Fetching Descendents of a Given Node
Once your tree is built, you can fetch all descendents of a node with fetchAllDescendents(). The first argument is the node to fetch the descendents of. The node can be either an instance of Zend_Db_Table_Row_Abstract or the string/numeric value of the columns id (based on $_traversal['column']). You can optionally pass in a select object to use as the second argument, which will be used when selecting the descendents.
<?php
$node = $comments->find(17)->current();
$descendents = $comments->fetchAllDescendents($node);
// Identical to:
$descendents = $comments->fetchAllDescendents(17);
// With optional select object
$select = $comments->select()->where('name = ?', 'jennifer')->limit(5);
$descendends = $comments->fetchAllDescendents($node, $select);
Fetching Ancestors of a Given Node
You can also fetch the ancestors just as easily with fetchAllAncestors(). All ancestors from the immediate parent up to the root of the tree will be returned.
<?php $ancestors = $comments->fetchAllAncestors($node);
Fetching Nodes as a Tree
You can fetch nodes as a tree by calling $table->fetchTree(). Its functionality is similar to fetchDescendents, except that it returns a modified rowset in that each row contains a tree_depth value.
<?php
$tree = $comments->fetchTree();
foreach ($tree as $node) {
echo str_repeat(' ', $node->tree_depth * 4) . $node->id . PHP_EOL;
}
Class: Virgen_Db_Table
Here's the complete Virgen_Db_Table class:
<?php
/**
* Enhancements to Zend_Db_Table
* @author Hector Virgen
*
*/
require_once 'Zend/Db/Table/Abstract.php';
class Virgen_Db_Table extends Zend_Db_Table_Abstract
{
/**
* Traversal tree information for
* Modified Preorder Tree Traversal Model
*
* http://www.sitepoint.com/print/hierarchical-data-database
*
* Values:
* 'left' => column name for left value
* 'right' => column name for right value
* 'column' => column name for identifying row (primary key assumed)
* 'refColumn' => column name for parent id (if not set, will look in reference map for own table match)
* 'order' => order by for rebuilding tree (e.g. "`name` ASC, `age` DESC")
*
* @var array $_traversal
*/
protected $_traversal = array();
/**
* Automatically is set to true once traversal info is set and verified
*
* @var boolean $_isTraversable
*/
protected $_isTraversable = false;
/**
* Modified to initialize traversal
*
*/
public function __construct($config = array())
{
parent::__construct($config);
$this->_initTraversal();
}
/**
* Returns columns names
*
* @return array columns
*/
public function getColumns()
{
return $this->info(Zend_Db_Table_Abstract::COLS);
}
/**
* Returns metadata value for index or entire array
*
* @param index $key
* @return value | array
*/
public function getMetadata($key = null)
{
if (null === $key) return $this->_metadata;
if (!array_key_exists($key, $this->_metadata)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Key '{$key}' not found in metadata");
}
return $this->_metadata[$key];
}
/**
* Returns the table name and schema separated by a dot for use in sql queries
*
* @return string schema.name || name
*/
public function getName()
{
return $this->_schema ? $this->_schema . '.' . $this->_name : $this->_name;
}
/**
* Is Duplicate - Checks for a duplicate value in the database
*
* @param string $column - column name
* @param string $value - value to search for
* @return boolean
*/
public function isDuplicate($column, $match)
{
$select = $this->select()->limit(1);
if (is_string($match) OR is_numeric($match)) {
$select->where("{$column} = ?", $match);
} else if (is_array($match)) {
$select->where("{$column} IN (?)", $match);
} else {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Match value must be a string, numeric, or array");
}
return (null !== $this->fetchRow($select)) ? true : false;
}
/**
* Fetches duplicate entries based on column name
*
* @param string $column - column name
* @param string $match - optional match value
* @return Zend_Db_Table_Rowset
*/
public function fetchDuplicates($column, $match = null)
{
$select = $this->select()
->from(
$this->getName(),
array(
'value' => $column,
'duplicates' => new Zend_Db_Expr('COUNT(*)')
)
)
->group($column)
->having('duplicates > ?', 1)
;
if (is_string($match) OR is_numeric($match)) {
$select->where("{$column} = ?", $match);
} else if (is_array($match)) {
$select->where("{$column} IN (?)", $match);
}
return $this->fetchAll($select);
}
/**
* Is Valid - Checks if a field is valid based on its validator
*
* @param string $field
* @param string|int $value
* @return boolean
*/
public function isValid($field, $value)
{
if (!array_key_exists($field, $this->_validators)) return true;
foreach($this->_validators[$field] as $validator) {
if (!array_key_exists('name', $validator)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Validators must contain a name.");
}
$name = $validator['name'];
$arguments = array_key_exists('arguments', $validator) ? $validator['arguments'] : array();
if (!Zend_Validate::is($value, $name, $arguments)) {
return false;
}
}
return true;
}
/**
* Counts the number of rows for a given select statement.
* Accepts instances of Zend_Db_Table_Select, Zend_Db_Select,
* an array of WHERE clauses, or null to return a total
* count of all rows in the table.
*
* @param Zend_Db_Table_Select|string|array $select
* @return int theCount
*/
public function count($select = null)
{
// Count using instance of Zend_Db_Table_Select
if ($select instanceof Zend_Db_Table_Select) {
$_select = clone $select;
$result = $this->_countSelect($_select);
// Count using array or count all
} else if(null === $select OR is_string($select) OR is_array($select)) {
$result = $this->_countWhere($select);
// Invalid parameter
} else {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception('Invalid parameter passed to count() method');
}
return $result;
}
/**
* Counts the number of rows using an instance of
* Zend_Db_Table_Select.
*
* @param Zend_Db_Table_Select $select
* @return double theCount
*/
protected function _countSelect(Zend_Db_Table_Select $select)
{
$s = clone $select;
// Remove any existing limits, offsets, and orders
$s->reset('order');
$s->reset('limitcount');
$s->reset('limitoffset');
$_select = $this->getAdapter()
->select()
->from(
array('c' => $s),
array('theCount' => 'COUNT(*)')
)
;
$row = $this->getAdapter()->fetchRow($_select);
return (double) $row['theCount'];
}
/**
* Counts the number of rows using an array or string
* of where clauses, or null to count all rows in the
* table.
*
* @param array|string $where
* @return double theCount
*/
protected function _countWhere($where = null)
{
$select = $this->select();
if (is_array($where)) {
foreach ($where as $key => $value) {
if (is_int($key)) {
$select->where($value);
} else {
$select->where($key, $value);
}
}
} else if (is_string($where)) {
$select->where($where);
}
return (double) $this->_countSelect($select);
}
/**
* Returns the number of rows from the last SQL_CALC_FOUND_ROWS query
*
* @return double - found rows
*/
public function getCalcFoundRows()
{
$sql = "SELECT FOUND_ROWS() AS theCount";
$stmt = $this->_db->query($sql);
$row = $stmt->fetch();
return (double) $row['theCount'];
}
/**
* Pre-insert hook allows for data validation / filtering on a per-class basis
*
* @param array $data
* @return array
*/
public function preInsert($data)
{
return $data;
}
/**
* Pre-update hook allows for data validation / filtering on a per-class basis
*
* @param array $data
* @return array
*/
public function preUpdate($data)
{
return $data;
}
/**
* Override insert method to include pre-insert hook
*
* @param mixed $data
* @return primary key
*/
public function insert(array $data)
{
$data = $this->preInsert($data);
return $this->_isTraversable ? $this->_insertTraversable($data) : parent::insert($data);
}
/**
* Override update method to include pre-update hook
*
* @param mixed $data
* @param mixed $where
* @return int
*/
public function update(array $data, $where)
{
$data = $this->preUpdate($data);
return parent::update($data, $where);
}
/**
* Factory method to return instances of reference tables
*
* @param string $name
* @param array $options for constructor
* @return Virgen_Db_Table $instance
*/
public function getReferenceInstance($ruleKey, array $options = array())
{
if (!array_key_exists($ruleKey, $this->_referenceMap)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Reference key {$ruleKey} not found in " . __CLASS__);
}
$className = $this->_referenceMap[$ruleKey]['refTableClass'];
// Check for self-references
if (!array_key_exists($className, self::$_referenceInstances)) {
self::$_referenceInstances[$className] = ($className == __CLASS__) ?
$this:
new $className($options);
}
return self::$_referenceInstances[$className];
}
/**
* Factory method to return instances of dependent tables
*
* @param string $name - class name of dependent table
* @param array $options - options to pass to constructor
* @return Virgen_Db_Table $instance
*/
public function getDependentInstance($className, array $options = array())
{
if (!in_array($className, $this->_dependentTables)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Dependent table {$className} not found in " . __CLASS__);
}
if (!array_key_exists($className, self::$_dependentInstances)) {
self::$_dependentInstances[$className] = ($className == __CLASS__) ?
$this:
new $className($options);
}
return self::$_dependentInstances[$className];
}
/**
* Returns all reference instances
*
* @return array - reference instances
*/
public function getReferenceInstances()
{
return self::$_dependentInstances;
}
/**
* Returns all dependent instances
*
* @return array - dependent instances
*/
public function getDependentInstances()
{
return self::$_dependentInstances;
}
/**
* Public function to rebuild tree traversal. The recursive function
* _rebuildTreeTraversal() must be called without arguments.
*
* @return $this - Fluent interface
*/
public function rebuildTreeTraversal()
{
$this->_rebuildTreeTraversal();
return $this;
}
/**
* Recursively rebuilds the modified preorder tree traversal
* data based on a parent id column
*
* @param int $parentId
* @param int $leftValue
* @return int new right value
*/
protected function _rebuildTreeTraversal($parentId = null, $leftValue = 0)
{
$this->_verifyTraversable();
$select = $this->select();
if ($parentId > 0) {
$select->where("{$this->_traversal['refColumn']} = ?", $parentId);
} else {
$select->where("{$this->_traversal['refColumn']} IS NULL OR {$this->_traversal['refColumn']} = 0");
}
if (array_key_exists('order', $this->_traversal)) {
$select->order($this->_traversal['order']);
}
$rightValue = $leftValue + 1;
$rowset = $this->fetchAll($select);
foreach ($rowset as $row) {
$rightValue = $this->_rebuildTreeTraversal($row->{$this->_traversal['column']}, $rightValue);
}
if ($parentId > 0) {
$node = $this->fetchRow($this->select()->where("{$this->_traversal['column']} = ?", $parentId));
if (null !== $node) {
$node->{$this->_traversal['left']} = $leftValue;
$node->{$this->_traversal['right']} = $rightValue;
$node->save();
}
}
return $rightValue + 1;
}
/**
* Calculates left and right values for new row and inserts it.
* Also adjusts all rows to make room for the new row.
*
* @param array $data
* @return int $id
*/
protected function _insertTraversable($data)
{
$this->_verifyTraversable();
// Disable traversable flag to prevent automatic traversable manipulation during updates.
$isTraversable = $this->_isTraversable;
$this->_isTraversable = false;
if (array_key_exists($this->_traversal['refColumn'], $data) && $data[$this->_traversal['refColumn']] > 0) {
// Find parent row
$parent_id = $data[$this->_traversal['refColumn']];
$parent = $this->find($parent_id)->current();
if (null === $parent) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Traversable error: Parent id {$parent_id} not found");
}
$lt = (double) $parent->{$this->_traversal['left']};
$rt = (double) $parent->{$this->_traversal['right']};
// Make room for the new node
parent::update(
array(
$this->_traversal['left'] => new Zend_Db_Expr($this->getAdapter()->quoteInto("{$this->_traversal['left']} + ?", 2)),
),
array(
$this->getAdapter()->quoteInto("{$this->_traversal['left'] > ?", $lt)
)
);
parent::update(
array(
$this->_traversal['right'] => new Zend_Db_Expr($this->getAdapter()->quoteInto("{$this->_traversal['right']} + ?", 2)),
),
array(
$this->getAdapter()->quoteInto("{$this->_traversal['right']} > ?", $lt)
)
);
$data[$this->_traversal['left']] = $lt + 1;
$data[$this->_traversal['right']] = $lt + 2;
} else {
$maxRt = (double) $this->fetchRow($this->select()->from($this, array('theMax' => "MAX({$this->_traversal['right']})")))->theMax;
$data[$this->_traversal['left']] = $maxRt + 1;
$data[$this->_traversal['right']] = $maxRt + 2;
}
// Do insert
$id = $this->insert($data);
// Reset isTraversable flag to previous value.
$this->_isTraversable = $isTraversable;
return $id;
}
/**
* Fetches all descendents of a given node
*
* @param Zend_Db_Table_Row_Abstract|string $row - Row object or value of row id
* @param Zend_Db_Select $select - optional custom select object
* @return Zend_Db_Table_Rowset|null
*/
public function fetchAllDescendents($row, Zend_Db_Select $select = null)
{
$this->_verifyTraversable();
if ($row instanceof Zend_Db_Table_Row_Abstract) {
$_row = $row;
} else if (is_string($row) OR is_numeric($row)) {
$_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
if (null === $_row) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
}
} else {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
}
$left = $_row->{$this->_traversal['left']};
$right = $_row->{$this->_traversal['right']};
if (null === $select) {
$select = $this->select();
}
$select->where("{$this->_traversal['left']} > ?", (double) $left)
->where("{$this->_traversal['left']} < ?", (double) $right)
;
$orderPart = $select->getPart('order');
if (empty($orderPart)) $select->order($this->_traversal['left']);
return $this->fetchAll($select);
}
/**
* Fetches all descendents of a given node and returns them as a tree
*
* @param Zend_Db_Table_Row_Abstract|string|int $rows- Row object or value of row id or array of rows
* @param Zend_Db_Select $select - optional select object
* @return Zend_Db_Table_Rowset|null
*/
public function fetchTree($row = null, Zend_Db_Select $select = null)
{
$this->_verifyTraversable();
if (null === $select) {
$select = $this->select();
}
$select->setIntegrityCheck(false)
->from(array('node' => $this->getName()))
->join(array('parent' => $this->getName()),
null,
array(
'tree_depth' => new Zend_Db_Expr("COUNT(parent.{$this->_traversal['refColumn']})")
)
)
->group("node.{$this->_traversal['column']}")
;
if (null !== $row) {
if ($row instanceof Zend_Db_Table_Row_Abstract) {
$_row = $row;
} else if (is_string($row) OR is_numeric($row)) {
$_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
if (null === $_row) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
}
} else {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
}
$left = (double) $_row->{$this->_traversal['left']};
$right = (double) $_row->{$this->_traversal['right']};
if ($left > 0 AND $right > 0) {
$select->where("node.{$this->_traversal['left']} >= {$left} AND node.{$this->_traversal['left']} < {$right}");
} else {
// Traversal information is bad, throw an exception
$id = $_row->{$this->_traversal['column']};
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Left/right values for row '{$this->_traversal['column']}' = '{$id}' in table '{$this->_name}' must be greater than zero to fetch tree.");
}
}
$select->where("node.{$this->_traversal['left']} BETWEEN parent.{$this->_traversal['left']} AND parent.{$this->_traversal['right']}");
$orderPart = $select->getPart('order');
if (empty($orderPart)) {
$select->order("node.{$this->_traversal['left']}");
}
return $this->fetchAll($select);
}
/**
* Fetches all ancestors of a given node
*
* @param Zend_Db_Table_Row_Abstract|string $row - Row object or value of row id
* @param Zend_Db_Select $select - optional custom select object
* @return Zend_Db_Table_Rowset|null
*/
public function fetchAllAncestors($row, Zend_Db_Select $select = null)
{
$this->_verifyTraversable();
if ($row instanceof Zend_Db_Table_Row_Abstract) {
$_row = $row;
} else if (is_string($row) OR is_numeric($row)) {
$_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
if (null === $_row) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
}
} else {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
}
$left = $_row->{$this->_traversal['left']};
$right = $_row->{$this->_traversal['left']};
if (null === $select) {
$select = $this->select();
}
$select->where("{$this->_traversal['left']} < ?", (double) $left)
->where("{$this->_traversal['right']} > ?", (double) $right)
;
$orderPart = $select->getPart('order');
if (empty($orderPart)) {
$select->order($this->_traversal['left']);
}
return $this->fetchAll($select);
}
/**
* Prepares the traversal information
*
*/
protected function _initTraversal()
{
if (empty($this->_traversal)) return;
$columns = $this->getColumns();
// Verify 'left' value and column
if (!isset($this->_traversal['left'])) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("'left' value must be specified for tree traversal");
}
if (!in_array($this->_traversal['left'], $columns)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Column '" . $this->_traversal['left'] . "' not found in table for tree traversal");
}
// Verify 'right' value and column
if (!isset($this->_traversal['right'])) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("'right' value must be specified for tree traversal");
}
if (!in_array($this->_traversal['right'], $columns)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Column '" . $this->_traversal['right'] . "' not found in table for tree traversal");
}
// Check for identifying column
if (!isset($this->_traversal['column'])) {
if (!isset($this->_primary)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Unable to determine primary key for tree traversal");
}
if (count($this->_primary) > 1) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Cannot use compound primary key as identifying column for tree traversal, please specify the column manually");
}
$this->_traversal['column'] = current((array) $this->_primary);
}
// Check for reference column
if (!isset($this->_traversal['refColumn'])) {
if (!array_key_exists('Parent', $this->_referenceMap)) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Unable to determine reference column for traversal, and did not find reference rule 'Parent' in reference map");
}
$refColumn = $this->_referenceMap['Parent']['refColumns'];
if (!is_string($refColumn) AND count($refColumn) > 1) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Cannot use compound primary key as reference column for tree traversal, please specify the reference column manually");
}
$this->_traversal['refColumn'] = $refColumn;
}
$this->_isTraversable = true;
}
/**
* Verifies that the current table is a traversable
*
* @throws Zend_Db_Exception - Table is not traversable
*/
protected function _verifyTraversable()
{
if (!$this->_isTraversable) {
require_once 'Zend/Db/Table/Exception.php';
throw new Zend_Db_Table_Exception("Table {$this->_name} is not traversable");
}
}
}

I am using your class to build a hierarchial menu system, it works very nicely for fetching the categories out, I now need to expand the functionality with a left join, to retrieve pages for each category from a second table each time a category is iterated, I have spent a little while messing around and cant get it to work, do you have any pointers I can pick up on.
Thanks again
$select = $myTraversableTable->select()
->setIntegrityCheck(false)
->from($myTraversableTable)
->join($myJoinTable, [condition], [fields]);
$descendents = $myTraversableTable->fetchAllDescendents(null, $select);
I hope this helps. :)
Could you email me on nick AT nickpack DOT com so that I have a contact email address for you?
Thanks for this extension !!!
But I have one problem...
I have this tree (numbers in parentheses is the tree depth)
Cat 1(0)
_Cat 1-1(1)
__Cat 1-1-1(2)
__Cat 1-1-2(2)
__Cat 1-1-3(2)
__Cat 1-1-4(2)
when I insert 1 more node on parent Cat 1-1 i got this change
Cat 1(0)
_Cat 1-1(1)
___Cat 1-1-1(3)
___Cat 1-1-5(3)
__Cat 1-1-2(2)
__Cat 1-1-3(2)
__Cat 1-1-4(2)
My new node and the first node on Cat 1-1 (depth 2) comes to depth (3)
Yes, im using rebuildTreeTranversal to fix the tree after insert/update/delete any node
u need to see my table structure and data to make tests ?
Now, the script works perfectly !!Thank !!
U need only to correct the line 467, u forgot to close bracets on $this->_traversal['left']
$this->getAdapter()->quoteInto("{$this->_traversal['left'] > ?", $lt)
when im inserting i have a function overrind insert to manage left and right values, but i dont have one to manage updates when change node father... in this case, i need to call rebuildTreeTranversal ??? its cost a lot of querys... we dont have other method to update the tree like _insertTraversable ?
thanks
I noticed that you listed the constant, null, on the left when making this comparison:
if (null === $this->_connection)
I've seen comparisons done this way in other code as well. I usually do it the opposite way because I like how it reads. Is there reason (performance?) that you do it that way?
if (null = $foo)
then PHP will complain because null cannot be assigned a value.
If I had the variable on the left, then PHP will think I'm assigning null to $foo. The code is semantically correct and I could be pulling my hair out not knowing why my code isn't running like it should ;)
if ($foo = null) {
// this block never executes
}
So it's just an easy way to prevent one type of common typo.
This is an incredible piece of code, and I thank you for it!
I have a question, and this may be just my misunderstanding of how some internals work. When I use the Zend_Db_Profiler_Firebug, I see a DESCRIBE being done twice on the table. Have I possibly set up my row and table models incorrectly?
Thank you very much for this awesome class. I hope this can be added to Zend Framework component soon.
Just a minor syntax error on line 467. It's missing a closing curly bracket.
I have an 'active' column in my table. If a node is marked inactive (active = 0) I don't want to get that node or child nodes when calling fetchTree(). Passing in a select object with 'node.active = 1' only removes the the parent node. The children are still returned.
@Edward
In order to fetch the tree, the select must join on itself. Try this:
$select = $table->select(); $select->where('node.active = ?', 1) ->where('parent.active = ?', 1); $tree = $table->fetchTree(null, $select);Thanks Hector!
I should have mentioned that I tried that as well, it behaves the same way. I may have to put logic in to filter after the results have been retrieved. I loop over the result to build a nested array for Zend_Navigation anyway.
@Edward
I was just doing some testing and it turns out you can omit a node and its descendants by using its left/right values:
$table = new MyTable(); $node = $table->find(123); $select = $table->select()->where("node.lt NOT BETWEEN {$node->lt} AND {$node->rt}"); $tree = $table->fetchTree(null, $select);I hope this helps!
Thanks Hector!
Using your solution I could query all the inactive records in my table and loop over them to dynamically build the the where clause. However, filtering in the loop that builds the nested array might be more performant.
What are your thoughts?
How about moving nodes/branches? Is it possible to do with this code or that needs extra function?
@Aurimas
I haven't added code to handle updates, but if you think of moving a node as two separate processes (deleting from one place and inserting in another) then it should work. If for whatever reason it doesn't you can always rebuild the tree.