Within Magento, a merchant has the ability to configure multiple root categories within the admin and add subsequent subcategories beneath these.
If we take a look at these records in the database, we can see that there are actually 5 rows instead of 4 that we would expect.
The first entity ID seems to be the one we are missing from the screenshot of our admin area. Magento creates a parent ‘God’ category that holds the category structure within it.
It is crated upon installing Magento along with the Default Category
root category and can be seen within the data install script of the Mage_Catalog
module.
<?php $installer = $this; // Create Root Catalog Node Mage::getModel('catalog/category') ->load(1) ->setId(1) ->setStoreId(0) ->setPath(1) ->setLevel(0) ->setPosition(0) ->setChildrenCount(0) ->setName('Root Catalog') ->setInitialSetupFlag(true) ->save(); /* @var $category Mage_Catalog_Model_Category */ $category = Mage::getModel('catalog/category'); $category->setStoreId(0) ->setName('Default Category') ->setDisplayMode('PRODUCTS') ->setAttributeSetId($category->getDefaultAttributeSetId()) ->setIsActive(1) ->setPath('1') ->setInitialSetupFlag(true) ->save();
So all root categories that get created will have this ‘God’ category as their parent. Any subcategories that get created will have their root category as their parent.
A complete list of the columns found within the catalog_category_entity table can be seen below:
As the catalog_category
is an EAV entity, there are of course other tables, such as catalog_category_entity_text
, catalog_category_entity_varchar
etc.
Whenever he save category information to the database, Magento sets the path and parent ID for us.
Below is the _beforeSave()
method of the Mage_Catalog_Model_Resource_Category
class.
protected function _beforeSave(Varien_Object $object) { parent::_beforeSave($object); if (!$object->getChildrenCount()) { $object->setChildrenCount(0); } if ($object->getLevel() === null) { $object->setLevel(1); } if (!$object->getId()) { $object->setPosition($this->_getMaxPosition($object->getPath()) + 1); $path = explode('/', $object->getPath()); $level = count($path); $object->setLevel($level); if ($level) { $object->setParentId($path[$level - 1]); } $object->setPath($object->getPath() . '/'); $toUpdateChild = explode('/',$object->getPath()); $this->_getWriteAdapter()->update( $this->getEntityTable(), array('children_count' => new Zend_Db_Expr('children_count+1')), array('entity_id IN(?)' => $toUpdateChild) ); } return $this; }
Here we set the category’s position, level, parent ID and initially set the path. Magento appends the category ID to the path however in the _afterSave()
method.
protected function _afterSave(Varien_Object $object) { /** * Add identifier for new category */ if (substr($object->getPath(), -1) == '/') { $object->setPath($object->getPath() . $object->getId()); $this->_savePath($object); } $this->_saveCategoryProducts($object); return parent::_afterSave($object); }
Let’s take a look at some of the code used to render the top menu. Firstly, we have a theme topmenu.phtml
template file.
<?php $_menu = $this->getHtml('level-top') ?> <?php if($_menu): ?> <nav id="nav"> <ol class="nav-primary"> <?php echo $_menu ?> </ol> </nav> <?php endif ?>
And a corresponding Topmenu.php
block.
public function getHtml($outermostClass = '', $childrenWrapClass = '') { Mage::dispatchEvent('page_block_html_topmenu_gethtml_before', array( 'menu' => $this->_menu, 'block' => $this )); $this->_menu->setOutermostClass($outermostClass); $this->_menu->setChildrenWrapClass($childrenWrapClass); if ($renderer = $this->getChild('catalog.topnav.renderer')) { $renderer->setMenuTree($this->_menu)->setChildrenWrapClass($childrenWrapClass); $html = $renderer->toHtml(); } else { $html = $this->_getHtml($this->_menu, $childrenWrapClass); } Mage::dispatchEvent('page_block_html_topmenu_gethtml_after', array( 'menu' => $this->_menu, 'html' => $html )); return $html; }
The page_block_html_topmenu_gethtml_before
event has an _addCategoriesToMenu()
observer that gets executed after the event gets dispatched.
protected function _addCategoriesToMenu($categories, $parentCategoryNode, $menuBlock, $addTags = false) { $categoryModel = Mage::getModel('catalog/category'); foreach ($categories as $category) { if (!$category->getIsActive()) { continue; } $nodeId = 'category-node-' . $category->getId(); $categoryModel->setId($category->getId()); if ($addTags) { $menuBlock->addModelTags($categoryModel); } $tree = $parentCategoryNode->getTree(); $categoryData = array( 'name' => $category->getName(), 'id' => $nodeId, 'url' => Mage::helper('catalog/category')->getCategoryUrl($category), 'is_active' => $this->_isActiveMenuCategory($category) ); $categoryNode = new Varien_Data_Tree_Node($categoryData, 'id', $tree, $parentCategoryNode); $parentCategoryNode->addChild($categoryNode); $flatHelper = Mage::helper('catalog/category_flat'); if ($flatHelper->isEnabled() && $flatHelper->isBuilt(true)) { $subcategories = (array)$category->getChildrenNodes(); } else { $subcategories = $category->getChildren(); } $this->_addCategoriesToMenu($subcategories, $categoryNode, $menuBlock, $addTags); } }
The adds the parent category node information to our category menu.
We also have a renderer.phtml
file that returns the category tree children categories.
<?php $html = ''; $children = $menuTree->getChildren(); $parentLevel = $menuTree->getLevel(); $childLevel = is_null($parentLevel) ? 0 : $parentLevel + 1; $counter = 1; $childrenCount = $children->count(); $parentPositionClass = $menuTree->getPositionClass(); $itemPositionClassPrefix = $parentPositionClass ? $parentPositionClass . '-' : 'nav-'; foreach ($children as $child) { $child->setLevel($childLevel); $child->setIsFirst($counter == 1); $child->setIsLast($counter == $childrenCount); $child->setPositionClass($itemPositionClassPrefix . $counter); $outermostClassCode = 'level'. $childLevel; $_hasChildren = ($child->hasChildren()) ? 'has-children' : ''; $html .= '<li '. $this->_getRenderedMenuItemAttributes($child) .'>'; $html .= '<a href="'. $child->getUrl() .'" class="'. $outermostClassCode .' '. $_hasChildren .'">'. $this->escapeHtml($this->__($child->getName())) .'</a>'; if (!empty($childrenWrapClass)) { $html .= '<div class="'. $childrenWrapClass .'">'; } $nextChildLevel = $childLevel + 1; if (!empty($_hasChildren)) { $html .= '<ul class="level'. $childLevel .'">'; $html .= '<li class="level'. $nextChildLevel .' view-all">'; $html .= '<;a class="level'. $nextChildLevel .'" href="'. $child->getUrl() .'">'; $html .= $this->__('View All') . ' ' . $this->escapeHtml($this->__($child->getName())); $html .= '</a>'; $html .= '</li>'; $html .= $this->render($child, $childrenWrapClass); $html .= '</ul>'; } if (!empty($childrenWrapClass)) { $html .= '</div>'; } $html .= '</li>'; $counter++; } return $html;
The category tree itself can be loaded using the Mage::getResourceModel('catalog/category_tree')
as seen in the Mage_Catalog_Model_Category
class.
public function getTreeModel() { return Mage::getResourceModel('catalog/category_tree'); } /** * Enter description here... * * @return Mage_Catalog_Model_Resource_Eav_Mysql4_Category_Tree */ public function getTreeModelInstance() { if (is_null($this->_treeModel)) { $this->_treeModel = Mage::getResourceSingleton('catalog/category_tree'); } return $this->_treeModel; }
protected function _getTree() { if (!$this->_tree) { $this->_tree = Mage::getResourceModel('catalog/category_tree') ->load(); } return $this->_tree; }
public function load($parentNode=null, $recursionLevel = 0) { if (!$this->_loaded) { $startLevel = 1; $parentPath = ''; if ($parentNode instanceof Varien_Data_Tree_Node) { $parentPath = $parentNode->getData($this->_pathField); $startLevel = $parentNode->getData($this->_levelField); } else if (is_numeric($parentNode)) { $select = $this->_conn->select() ->from($this->_table, array($this->_pathField, $this->_levelField)) ->where("{$this->_idField} = ?", $parentNode); $parent = $this->_conn->fetchRow($select); $startLevel = $parent[$this->_levelField]; $parentPath = $parent[$this->_pathField]; $parentNode = null; } else if (is_string($parentNode)) { $parentPath = $parentNode; $startLevel = count(explode($parentPath))-1; $parentNode = null; } $select = clone $this->_select; $select->order($this->_table . '.' . $this->_orderField . ' ASC'); if ($parentPath) { $pathField = $this->_conn->quoteIdentifier(array($this->_table, $this->_pathField)); $select->where("{$pathField} LIKE ?", "{$parentPath}/%"); } if ($recursionLevel != 0) { $levelField = $this->_conn->quoteIdentifier(array($this->_table, $this->_levelField)); $select->where("{$levelField} <= ?", $startLevel + $recursionLevel); } $arrNodes = $this->_conn->fetchAll($select); $childrenItems = array(); foreach ($arrNodes as $nodeInfo) { $pathToParent = explode('/', $nodeInfo[$this->_pathField]); array_pop($pathToParent); $pathToParent = implode('/', $pathToParent); $childrenItems[$pathToParent][] = $nodeInfo; } $this->addChildNodes($childrenItems, $parentPath, $parentNode); $this->_loaded = true; } return $this; }
public function addChildNodes($children, $path, $parentNode, $level = 0) { if (isset($children[$path])) { foreach ($children[$path] as $child) { $nodeId = isset($child[$this->_idField])?$child[$this->_idField]:false; if ($parentNode && $nodeId && $node = $parentNode->getChildren()->searchById($nodeId)) { $node->addData($child); } else { $node = new Varien_Data_Tree_Node($child, $this->_idField, $this, $parentNode); } //$node->setLevel(count(explode('/', $node->getData($this->_pathField)))-1); $node->setLevel($node->getData($this->_levelField)); $node->setPathId($node->getData($this->_pathField)); $this->addNode($node, $parentNode); if ($path) { $childrenPath = explode('/', $path); } else { $childrenPath = array(); } $childrenPath[] = $node->getId(); $childrenPath = implode('/', $childrenPath); $this->addChildNodes($children, $childrenPath, $node, $level+1); } } }
Note: This article is based on Magento Community/Open Source version 1.9.