Magento Category Structure

Within Magento, a merchant has the ability to configure multiple root categories within the admin and add subsequent subcategories beneath these.

Magento Category Structure

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.

Magento Category Structure

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:

Magento Category Structure

  • entity_id – The unique ID of the entity
  • entity_type_id – The ID of the entity_type
  • attribute_set_id – The ID of the attribute set
  • parent_id – The ID of the parent category
  • created_at – The date the category was created
  • updated_at – The date the category was updated
  • path – The path of the categories used with category IDs separated by a “/” e.g. 1/2/3
  • position The position of the category within its level in the tree structure
  • level – The hierarchy level that the category belongs to
  • children_count – The number of children categories that the category contains

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.