月度归档:2013年03月

Magento创建一个Block实例_读取任意目录的子目录

创建一个读取目录的块来展示块创建过程。块创建跟建立一个模块很类似。
这里报名为Vfeelit,模块名为Catcus,块类名为Vfeelit_Catcus_Block_Menu:

app/etc/modules/Vfeelit_Helloworld.xml
app/code/local/Vfeelit/Catcus/etc/config.xml
app/code/local/Vfeelit/Catcus/block/Menu.php

app/design/frontend/default/default/layout/catcus.xml
app/design/frontend/default/default/template/catcus/menu.phtml

app/etc/modules/Vfeelit_Helloworld.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Vfeelit_Catcus>
            <active>true</active>
            <codePool>local</codePool>
        </Vfeelit_Catcus>
    </modules>
</config>

app/code/local/Vfeelit/Catcus/etc/config.xml

<?xml version="1.0"?>

<config>
    <modules>
        <Vfeelit_Catcus>
            <version>1.0.0.0</version>
        </Vfeelit_Catcus>
    </modules>
     <frontend>   
        <layout>
            <updates>
                <catcus>
                    <file>catcus.xml</file>
                </catcus>
            </updates>
        </layout>
    </frontend>
    <global>
        <blocks>
            <catcus>
                <class>Vfeelit_Catcus_Block</class>
            </catcus>
        </blocks>
    </global>
</config>

注意这个配置文件,必须给出布局对应的文件,否则找不到。还有前台必须给出块的类。

app/code/local/Vfeelit/Catcus/block/Menu.php

<?php
class Vfeelit_Catcus_Block_Menu extends Mage_Core_Block_Template
{
    /**
     * Top menu data tree
     *
     * @var Varien_Data_Tree_Node
     */
    protected $_menu;

    /**
     * Init top menu tree structure
     */
    public function _construct()
    {
        $this->_menu = new Varien_Data_Tree_Node(array(), 'root', new Varien_Data_Tree());
    }
    
    public function addCategoriesToMenu($categories, $parentCategoryNode)
    {
    	foreach ($categories as $category) {
    		if (!$category->getIsActive()) {
    			continue;
    		}
    
    		$nodeId = 'category-node-' . $category->getId();

    		$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);
    
    		if (Mage::helper('catalog/category_flat')->isEnabled()) {
    			$subcategories = (array)$category->getChildrenNodes();
    		} else {
    			$subcategories = $category->getChildren();
    		}
    
    		$this->addCategoriesToMenu($subcategories, $categoryNode);
    	}
    }

    protected function _isActiveMenuCategory($category)
    {
    	$catalogLayer = Mage::getSingleton('catalog/layer');
    	if (!$catalogLayer) {
    		return false;
    	}
    
    	$currentCategory = $catalogLayer->getCurrentCategory();
    	if (!$currentCategory) {
    		return false;
    	}
    
    	$categoryPathIds = explode(',', $currentCategory->getPathInStore());
    	return in_array($category->getId(), $categoryPathIds);
    }
    
    /**
     * Get top menu html
     *
     * @param string $outermostClass
     * @param string $childrenWrapClass
     * @return string
     */
    public function getHtml($cid ='', $recursionLevel = 0, $outermostClass = '', $childrenWrapClass = '')
    {


     	$cid = (int)$cid;

     	
     	$recursionLevel = (int)$recursionLevel;
    	 
    	$category = Mage::getModel('catalog/category');
     	if (!$category->checkId($cid)) {
     		return '';
     	}    	
    	$categories = $category->getCategories($cid, $recursionLevel);
    	
    	$this->addCategoriesToMenu($categories,$this->_menu);

        $this->_menu->setOutermostClass($outermostClass);//level-top  $this->_menu->_data['OutermostClass'] = 'level-top'
        $this->_menu->setChildrenWrapClass($childrenWrapClass); //$this->_menu->_data['ChildrenWrapClass'] = ''

        $html = $this->_getHtml($this->_menu, $childrenWrapClass);

        return $html;
    }

    /**
     * Recursively generates top menu html from data that is specified in $menuTree
     *
     * @param Varien_Data_Tree_Node $menuTree
     * @param string $childrenWrapClass
     * @return string
     */
    public function _getHtml(Varien_Data_Tree_Node $menuTree, $childrenWrapClass)
    {
        $html = '';

        $children = $menuTree->getChildren(); // $this->_menu   Varien_Data_Tree_Node_Collection
        $parentLevel = $menuTree->getLevel(); //root -> null  $this->_menu->_data['level'] = null
        $childLevel = is_null($parentLevel) ? 0 : $parentLevel + 1;

        $counter = 1;
        $childrenCount = $children->count(); //nodes total

        $parentPositionClass = $menuTree->getPositionClass();  //no set
        $itemPositionClassPrefix = $parentPositionClass ? $parentPositionClass . '-' : 'nav-'; //can get 'nav-'

        foreach ($children as $child) {

            $child->setLevel($childLevel);
            $child->setIsFirst($counter == 1);
            $child->setIsLast($counter == $childrenCount);
            $child->setPositionClass($itemPositionClassPrefix . $counter);

            $outermostClassCode = '';
            $outermostClass = $menuTree->getOutermostClass();

            if ($childLevel == 0 && $outermostClass) {
                $outermostClassCode = ' class="' . $outermostClass . '" ';
                $child->setClass($outermostClass);
            }

            $html .= '<li ' . $this->_getRenderedMenuItemAttributes($child) . '>';
            $html .= '<a href="' . $child->getUrl() . '" ' . $outermostClassCode . '><span>'
                . $this->escapeHtml($child->getName()) . '</span></a>';

            if ($child->hasChildren()) {
                if (!empty($childrenWrapClass)) {
                    $html .= '<div class="' . $childrenWrapClass . '">';
                }
                $html .= '<ul class="level' . $childLevel . '">';
                $html .= $this->_getHtml($child, $childrenWrapClass);
                $html .= '</ul>';

                if (!empty($childrenWrapClass)) {
                    $html .= '</div>';
                }
            }
            $html .= '</li>';

            $counter++;
        }

        return $html;
    }

    /**
     * Generates string with all attributes that should be present in menu item element
     *
     * @param Varien_Data_Tree_Node $item
     * @return string
     */
    protected function _getRenderedMenuItemAttributes(Varien_Data_Tree_Node $item)
    {
        $html = '';
        $attributes = $this->_getMenuItemAttributes($item);

        foreach ($attributes as $attributeName => $attributeValue) {
            $html .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"';
        }

        return $html;
    }

    /**
     * Returns array of menu item's attributes
     *
     * @param Varien_Data_Tree_Node $item
     * @return array
     */
    protected function _getMenuItemAttributes(Varien_Data_Tree_Node $item)
    {
        $menuItemClasses = $this->_getMenuItemClasses($item);
        $attributes = array(
            'class' => implode(' ', $menuItemClasses)
        );

        return $attributes;
    }

    /**
     * Returns array of menu item's classes
     *
     * @param Varien_Data_Tree_Node $item
     * @return array
     */
    protected function _getMenuItemClasses(Varien_Data_Tree_Node $item)
    {
        $classes = array();

        $classes[] = 'level' . $item->getLevel();
        $classes[] = $item->getPositionClass();

        if ($item->getIsFirst()) {
            $classes[] = 'first';
        }

        if ($item->getIsActive()) {
            $classes[] = 'active';
        }

        if ($item->getIsLast()) {
            $classes[] = 'last';
        }

        if ($item->getClass()) {
            $classes[] = $item->getClass();
        }

        if ($item->hasChildren()) {
            $classes[] = 'parent';
        }

        return $classes;
    }
}

用这个块类来输出任意目录的子目录。

app/design/frontend/default/default/layout/catcus.xml

<layout version="0.1.0">
    <default>
	<reference name="top.container">
            <block type="catcus/menu" name="catcus.menu" template="catcus/menu.phtml"/>
        </reference>	
    </default>

</layout>

把这个块放在top.container中输出。

app/design/frontend/default/default/template/catcus/menu.phtml

<?php
echo $this->getHtml(3);

直接输出内容。

建立一个自定义的块基本就设置这些文件。块的类是应该继承自Mage_Core_Block_Template的。模块的配置文件中必须指定块的类前缀和使用的布局文件。

永久链接:http://blog.ifeeline.com/531.html
原创文章,转载务必保持出处。

Magento目录读取代码细节

目录实体使用catalog_category_entity来存储,虽然目录的具体数据使用EAV模型存储,但是目录的层次结构还是用catalog_category_entity保存的:
Magento目录实体

entity_id就是目录id,entity_type_id是类型id(每种eav模型都有一个类型标识),attribute_set_id是属性集id,parent_id是父目录id,path记录了目录路径,position是位置值,level是层次,children_count记录当前目录有多少个子孙。

注意,第一个记录,它是所有root目录的父目录,像1/3这样的目录才是所谓的root目录。

接下来看看目录树是如何构建起来的:

在frontend\default\default\template\page/html/topmenu.phtml中调用了Mage_Page_Block_Html_Topmenu的getHmtl(‘top-level’)获取整个网站目录:

    public function getHtml($outermostClass = '', $childrenWrapClass = '')
    {
        Mage::dispatchEvent('page_block_html_topmenu_gethtml_before', array(
            'menu' => $this->_menu
        ));
//level-top  $this->_menu->_data['OutermostClass'] = 'level-top'
        $this->_menu->setOutermostClass($outermostClass);
//$this->_menu->_data['ChildrenWrapClass'] = ''
$this->_menu->setChildrenWrapClass($childrenWrapClass); 

        $html = $this->_getHtml($this->_menu, $childrenWrapClass);

        Mage::dispatchEvent('page_block_html_topmenu_gethtml_after', array(
            'menu' => $this->_menu,
            'html' => $html
        ));

        return $html;
} 

代码通过触发page_block_html_topmenu_gethtml_before事件进入获取目录:

<page_block_html_topmenu_gethtml_before>
<observers>
<catalog_add_topmenu_items>
<class>catalog/observer</class>
<method>addCatalogToTopmenuItems</method>
</catalog_add_topmenu_items>
</observers>
</page_block_html_topmenu_gethtml_before>

这里执行了catalog/observer类的addCatalogToTopmenuItems方法:

//Mage_Catalog_Model_Observer
    public function addCatalogToTopmenuItems(Varien_Event_Observer $observer)
    {
        $this->_addCategoriesToMenu(Mage::helper('catalog/category')->getStoreCategories(), $observer->getMenu());
}
// catalog/category 助手类的 getStoreCategories()
    public function getStoreCategories($sorted=false, $asCollection=false, $toLoad=true)
{
		//获取商店根目录Id(每个store_group都必须分配一个目录ID)
        $parent     = Mage::app()->getStore()->getRootCategoryId(); 

$category = Mage::getModel('catalog/category');
//获取层次,0表示没有限制,默认为0
        $recursionLevel  = max(0, (int) Mage::app()->getStore()->getConfig('catalog/navigation/max_depth'));
        //$category->getCategories(cid, 0, false, false, true);
        $storeCategories = $category->getCategories($parent, $recursionLevel, $sorted, $asCollection, $toLoad);

        return $storeCategories;
    }
// Mage_Catalog_Model_Category   cid, 0, false, false, true
    public function getCategories($parent, $recursionLevel = 0, $sorted=false, $asCollection=false, $toLoad=true)
    {
        $categories = $this->getResource()
            ->getCategories($parent, $recursionLevel, $sorted, $asCollection, $toLoad);
        return $categories;
    }
// Mage_Catalog_Model_Resource_Category  cid, 0, false, false, true
public function getCategories($parent, $recursionLevel = 0, $sorted = false, $asCollection = false, $toLoad = true)
    {
        $tree = Mage::getResourceModel('catalog/category_tree');
        /* @var $tree Mage_Catalog_Model_Resource_Category_Tree */
        $nodes = $tree->loadNode($parent)   //node
            ->loadChildren($recursionLevel) //node
            ->getChildren();

        $tree->addCollectionData(null, $sorted, $parent, $toLoad, true);

        if ($asCollection) {
            return $tree->getCollection();
        }
        return $nodes;
}
// Mage_Catalog_Model_Resource_Category_Tree
    public function loadNode($nodeId)
    {
        $select = clone $this->_select;
        if (is_numeric($nodeId)) {
            $condField = $this->_conn->quoteIdentifier(array($this->_table, $this->_idField));
        } else {
            $condField = $this->_conn->quoteIdentifier(array($this->_table, $this->_pathField));
        }

        $select->where("{$condField} = ?", $nodeId);
// SELECT `catalog_category_entity`.* FROM `catalog_category_entity` WHERE (`catalog_category_entity`.`entity_id` = '3')
// print_r($this->_conn->fetchRow($select));
//         Array
//         (
//         		[entity_id] => 3
//         		[entity_type_id] => 9
//         		[attribute_set_id] => 12
//         		[parent_id] => 1
//         		[created_at] => 2007-08-22 15:54:41
//         		[updated_at] => 2007-12-05 04:38:59
//         		[path] => 1/3
//         		[position] => 3
//         		[level] => 1
//         		[children_count] => 26
//         )
// $this->_idField = entity_id
        $node = new Varien_Data_Tree_Node($this->_conn->fetchRow($select), $this->_idField, $this);
        $this->addNode($node);
/*****  Mage_Catalog_Model_Resource_Category_Tree
    public function addNode($node, $parent=null)
    {
        $this->_nodes->add($node); // Varien_Data_Tree_Node_Collection
/*-----> Varien_Data_Tree_Node_Collection
    public function add(Varien_Data_Tree_Node $node)
    {
        $node->setParent($this->_container);//可能是node 也可能是tree

        // Set the Tree for the node
        if ($this->_container->getTree() instanceof Varien_Data_Tree) {
            $node->setTree($this->_container->getTree()); //一定是tree
        }

        $this->_nodes[$node->getId()] = $node;

        return $node;
    }
<------*/
        $node->setParent($parent); //如果指定了父节点 _parent= null
		//父节点不为空
        if (!is_null($parent) && ($parent instanceof Varien_Data_Tree_Node) ) {
            $parent->addChild($node);
        }
        return $node;
    }
****/
        return $node;
    }

    public function loadChildren($recursionLevel=0)
    {
        $this->_tree->load($this, $recursionLevel);
        return $this;
}

// Mage_Catalog_Model_Resource_Category_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);
//                 echo '$parentPath -- $startLevel'.$parentPath ."--". $startLevel; // 	1/3		1
            } 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);
            }
//echo $select->__toString();
//             SELECT `catalog_category_entity`.* FROM `catalog_category_entity` WHERE (`catalog_category_entity`.`path` LIKE '1/3/%') ORDER BY `catalog_category_entity`.`position` ASC
            $arrNodes = $this->_conn->fetchAll($select);

            $childrenItems = array();

            foreach ($arrNodes as $nodeInfo) {
//             	echo print_r($nodeInfo);
// echo $nodeInfo[$this->_pathField]."----";
                $pathToParent = explode('/', $nodeInfo[$this->_pathField]);
                array_pop($pathToParent); // 1/10/18/30   ->  1/10/18   
                $pathToParent = implode('/', $pathToParent);
                $childrenItems[$pathToParent][] = $nodeInfo;
            }

            $this->addChildNodes($childrenItems, $parentPath, $parentNode);

            $this->_loaded = true;
        }

        return $this;
    }

//Varien_Data_Tree_Node
    		public function getChildren()
   		{
        		return $this->_childNodes;
    }

//Mage_Catalog_Model_Observer
    /**
     * Recursively adds categories to top menu
     *
     * @param Varien_Data_Tree_Node_Collection|array $categories
     * @param Varien_Data_Tree_Node $parentCategoryNode
     */
    protected function _addCategoriesToMenu($categories, $parentCategoryNode)
    {
        foreach ($categories as $category) {
            if (!$category->getIsActive()) {
                continue;
            }

            $nodeId = 'category-node-' . $category->getId();

            $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);

            if (Mage::helper('catalog/category_flat')->isEnabled()) {
                $subcategories = (array)$category->getChildrenNodes();
            } else {
                $subcategories = $category->getChildren();
            }

            $this->_addCategoriesToMenu($subcategories, $categoryNode);
        }
    }

通过上面的代码分析,假设如下目录结构:
Magento目录树

如果指定了3为root,那么将从数据库中获取:

$childrenItems[1/3][1] = 4
$childrenItems[1/3][2] = 5

$childrenItems[1/3/4][1] = 6
$childrenItems[1/3/4][2] = 7
$childrenItems[1/3/4][3] = 8

$childrenItems[1/3/5][1] = 9
$childrenItems[1/3/5][2] = 10

$childrenItems[1/3/4/6][1] = 11
$childrenItems[1/3/4/6][2] = 12

然后根据这个关系,建立节点之间的相互关系。

Magento目录对象关系模型

每个节点的子节点都存放在自己的一个Node_Collection中,同时都会引用同一个Tree(这个Tree是负责加载节点和建立对应的父子关系),这个Tree中有一个Node_Collection,它保存了符合条件的所有的Node。 从某个节点进入,然后进入到它的子节点又获取到了节点,然后节点再进入子节点获取节点…. 这是一个递归过程。这里它把数据库里面的信息映射到了如上的模型。

如下:

// Mage_Catalog_Model_Resource_Category  cid, 0, false, false, true
public function getCategories($parent, $recursionLevel = 0, $sorted = false, $asCollection = false, $toLoad = true)
    {
        $tree = Mage::getResourceModel('catalog/category_tree');
        /* @var $tree Mage_Catalog_Model_Resource_Category_Tree */
        $nodes = $tree->loadNode($parent)   //node
            ->loadChildren($recursionLevel) //node
            ->getChildren();
	}

看这段代码,loadNode()把父节点读入,然后获取这个父节点的子节点返回。

对应这个模型,getCategories(3,0)返回了4和5(一个Node_Collection), 4的子节点是6和7和8,5的子节点是9和10,依次类推。

从上面可以提取2行通用代码:

$category = Mage::getModel('catalog/category');
$categories = $category->getCategories($id, $recursionLevel);

只要指定$id,就可以获取这个目录下的所有子目录(映射到上面的模型中),注意看$recursionLevel,它表示深入的层次,0表示无限,如果指定1表示只获取子节点,如果是2,将获取子节点的子节点(如果有)。

以上得到的其实是一个对象关系,要获取输出,还是需要来一次递归。

//Mage_Catalog_Model_Observer
    protected function _addCategoriesToMenu($categories, $parentCategoryNode)
    {
        foreach ($categories as $category) {
            if (!$category->getIsActive()) {
                continue;
            }

            $nodeId = 'category-node-' . $category->getId();
            $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);

            if (Mage::helper('catalog/category_flat')->isEnabled()) {
                $subcategories = (array)$category->getChildrenNodes();
            } else {
                $subcategories = $category->getChildren();
            }

            $this->_addCategoriesToMenu($subcategories, $categoryNode);
        }
}

这里根据获取的模型,再递归一次,这个时候node的值是name id url is_active,并且把节点挂到了$parentCategoryNode下面。

这样,getHtml()就利用这个建立节点关系,返回菜单。

//Mage_Page_Block_Html_Topmenu
    public function _construct()
    {
        $this->_menu = new Varien_Data_Tree_Node(array(), 'root', new Varien_Data_Tree());
}

//Varien_Data_Tree_Node
    public function __construct($data, $idFeild, $tree, $parent = null)
    {
        $this->setTree($tree);  // _tree = Varien_Data_Tree
        $this->setParent($parent);  //_parent = $parent = null
        $this->setIdField($idFeild);  //_idField = root
        $this->setData($data);
        $this->_childNodes = new Varien_Data_Tree_Node_Collection($this);
    }

//Varien_Data_Tree
    public function __construct()
    {
        $this->_nodes = new Varien_Data_Tree_Node_Collection($this);
} 

//Varien_Data_Tree_Node_Collection
    public function __construct($container) 
    {
        $this->_nodes = array();
        $this->_container = $container;
} 

实际的数据加载是tree对象进行的。首先需要获取一个tree对象,然后把父节点从数据库加载进来(会把tree节点引用传递给它),接着调用tree的方法把这个node的全部子节点加载进来,然后根据path建立依赖关系,之后返回第一次生成的node的直接子节点。

永久链接:http://blog.ifeeline.com/523.html
原创文章,转载务必保留出处。

Magento页面构建:page_html_header分析

<div class="header-container">
    <div class="header">
        <?php if ($this->getIsHomePage()):?>
        <h1 class="logo"><strong><?php echo $this->getLogoAlt() ?></strong><a href="<?php echo $this->getUrl('') ?>" title="<?php echo $this->getLogoAlt() ?>" class="logo"><img src="<?php echo $this->getLogoSrc() ?>" alt="<?php echo $this->getLogoAlt() ?>" /></a></h1>
        <?php else:?>
        <a href="<?php echo $this->getUrl('') ?>" title="<?php echo $this->getLogoAlt() ?>" class="logo"><strong><?php echo $this->getLogoAlt() ?></strong><img src="<?php echo $this->getLogoSrc() ?>" alt="<?php echo $this->getLogoAlt() ?>" /></a>
        <?php endif?>
        <div class="quick-access">
            <?php echo $this->getChildHtml('topSearch') ?>
            <p class="welcome-msg"><?php echo $this->getWelcome() ?> <?php echo $this->getAdditionalHtml() ?></p>
            <?php echo $this->getChildHtml('topLinks') ?>
            <?php echo $this->getChildHtml('store_language') ?>
        </div>
        <?php echo $this->getChildHtml('topContainer'); ?>
    </div>
</div>
<?php echo $this->getChildHtml('topMenu') ?>

这个模板上涉及到的方法:

class Mage_Page_Block_Html_Header extends Mage_Core_Block_Template
{
    public function _construct()
    {
        $this->setTemplate('page/html/header.phtml');
    }

    /**
     * Check if current url is url for home page
     *
     * @return true
     */
    public function getIsHomePage()
{
		//getUrl('')返回首页连接
        return $this->getUrl('') == $this->getUrl('*/*/*', array('_current'=>true, '_use_rewrite'=>true));
    }

    public function setLogo($logo_src, $logo_alt)
{
		//PHP的奇葩,_data['logo_src']= $logo_src
        $this->setLogoSrc($logo_src);
        $this->setLogoAlt($logo_alt);
        return $this;
    }

    public function getLogoSrc()
    {
        if (empty($this->_data['logo_src'])) {
            $this->_data['logo_src'] = Mage::getStoreConfig('design/header/logo_src');
        }
		//获取配置名,然后到皮肤文件夹中寻找
        return $this->getSkinUrl($this->_data['logo_src']);
    }

    public function getLogoAlt()
    {
        if (empty($this->_data['logo_alt'])) {
            $this->_data['logo_alt'] = Mage::getStoreConfig('design/header/logo_alt');
        }
        return $this->_data['logo_alt'];
    }

    public function getWelcome()
    {
        if (empty($this->_data['welcome'])) {
			//如果已经登录
            if (Mage::isInstalled() && Mage::getSingleton('customer/session')->isLoggedIn()) {
                $this->_data['welcome'] = $this->__('Welcome, %s!', $this->escapeHtml(Mage::getSingleton('customer/session')->getCustomer()->getName()));
            } else {
                $this->_data['welcome'] = Mage::getStoreConfig('design/header/welcome');
            }
        }

        return $this->_data['welcome'];
    }
}

这里对应了后台的三个设置:(System->Configuration->Design->Header)
Magento头部设置

可以给定Logo图片,图片的Alt属性,已经显示的Welcome文本。

getUrl(”)是从父类继承过来的,它可以获取首页连接。

1 子块topSearch

<reference name="header">
<block type="core/template" name="top.search" as="topSearch" template="catalogsearch/form.mini.phtml"/>
</reference>

这个搜索块的类型是core/template,那不就是指Mage_Core_Block_Template吗,而这个类一般作为块的父类,但是不一定都去继承它。这个toSearch模板事实是使用到Mage_Catalogsearch_Helper_Data助手类,但是模板是必须对应一个块的,所有就有了这个core/template作为基础类型。

2 子块topLinks

<default>
   <reference name="top.links">
      <action method="addLink" translate="label title" module="customer">
         <label>My Account</label>
         <url helper="customer/getAccountUrl"/>
         <title>My Account</title>
         <prepare/>
         <urlParams/>
         <position>10</position>
      </action>
   </reference>
</default>

<customer_logged_in>
   <reference name="top.links">
      <action method="addLink" translate="label title" module="customer">
         <label>Log Out</label>
         <url helper="customer/getLogoutUrl"/>
         <title>Log Out</title>
         <prepare/>
         <urlParams/>
         <position>100</position>
      </action>
   </reference>
</customer_logged_in>

<customer_logged_out>
   <reference name="top.links">
      <action method="addLink" translate="label title" module="customer">
         <label>Log In</label>
         <url helper="customer/getLoginUrl"/>
         <title>Log In</title>
         <prepare/>
         <urlParams/>
         <position>100</position>
      </action>
   </reference>
   <remove name="reorder"/>
</customer_logged_out>

<default>
   <reference name="top.links">
      <block type="checkout/links" name="checkout_cart_link">
         <action method="addCartLink"/>
         <action method="addCheckoutLink"/>
      </block>
   </reference>
</default>

<default>
   <reference name="top.links">
      <block type="wishlist/links" name="wishlist_link"/>
      <action method="addLinkBlock">
         <blockName>wishlist_link</blockName>
      </action>
   </reference>
</default>

以上是从包布局中找出来的针对top.links的操作。可以看到,在default句柄中,执行了四个方法。还有customer_logged_in 和 customer_logged_out就让人困惑了。可以说,这两个句柄完全是为了配合客户是否已经登录对布局做不同的修改而添加的,如果客户已经登录,句柄customer_logged_in总是会被添加,如果没有登录customer_logged_out就总是被添加。

根据这个状态,应用不同的布局。

这个块的类型是page/template_links,对应的类是Mage_Page_Templete_Links,看看构造函数:

    protected function _construct()
    {
        $this->setTemplate('page/template/links.phtml');
} 

它使用了page/template/links.phtml作为模板文件:

<?php $_links = $this->getLinks(); ?>
<?php if(count($_links)>0): ?>
<ul class="links"<?php if($this->getName()): ?> id="<?php echo $this->getName() ?>"<?php endif;?>>
    <?php foreach($_links as $_link): ?>
        <?php if ($_link instanceof Mage_Core_Block_Abstract):?>
            <?php echo $_link->toHtml() ?>
        <?php else: ?>
            <li<?php if($_link->getIsFirst()||$_link->getIsLast()): ?> class="<?php if($_link->getIsFirst()): ?>first<?php endif; ?><?php if($_link->getIsLast()): ?> last<?php endif; ?>"<?php endif; ?> <?php echo $_link->getLiParams() ?>><?php echo $_link->getBeforeText() ?><a href="<?php echo $_link->getUrl() ?>" title="<?php echo $_link->getTitle() ?>" <?php echo $_link->getAParams() ?>><?php echo $_link->getLabel() ?></a><?php echo $_link->getAfterText() ?></li>
        <?php endif;?>
    <?php endforeach; ?>
</ul>
<?php endif; ?>

这段代码很简单,获取所有连接,然后循环。对于每个_link,都可以是Mage_Core_Block_Abstract类型的Block。 仔细看下,有些是执行了addLink有些是执行了addLinkBlock,如果是addLinkBlock就是添加了一个Mage_Core_Block_Abstract类型的Block,那么输出时只要直接调用toHtml()就可以。

不过这里的addLink可比较有学问了:

    public function addLink($label, $url='', $title='', $prepare=false, $urlParams=array(),
        $position=null, $liParams=null, $aParams=null, $beforeText='', $afterText='')
    {
        if (is_null($label) || false===$label) {
            return $this;
        }
        $link = new Varien_Object(array(
            'label'         => $label,
            'url'           => ($prepare ? $this->getUrl($url, (is_array($urlParams) ? $urlParams : array())) : $url),
            'title'         => $title,
            'li_params'     => $this->_prepareParams($liParams),
            'a_params'      => $this->_prepareParams($aParams),
            'before_text'   => $beforeText,
            'after_text'    => $afterText,
        ));

        $this->_links[$this->_getNewPosition($position)] = $link;
        if (intval($position) > 0) {
             ksort($this->_links);
        }

        return $this;
    }

仔细对照如下代码就能明白这里的参数的作用:

<li<?php if($_link->getIsFirst()||$_link->getIsLast()): ?> class="<?php if($_link->getIsFirst()): ?>first<?php endif; ?><?php if($_link->getIsLast()): ?> last<?php endif; ?>"<?php endif; ?> <?php echo $_link->getLiParams() ?>><?php echo $_link->getBeforeText() ?><a href="<?php echo $_link->getUrl() ?>" title="<?php echo $_link->getTitle() ?>" <?php echo $_link->getAParams() ?>><?php echo $_link->getLabel() ?></a><?php echo $_link->getAfterText() ?></li>

LiParams表示在li标签中添加的属性。BeforeText表示在a标签之前输出的内容。Url资源就是a标签的链接了。Title对应a标签的title属性。AParams对应a标签的属性。Label就是a标签的描文本(必须)。AfterText就是a标签之后要输出的内容。
3 store_language

4 catalog.topnav

<?php $_menu = $this->getHtml('level-top')?>
<?php if($_menu): ?>
<div class="nav-container">
    <ul id="nav">
        <?php echo $_menu ?>
    </ul>
</div>
<?php endif ?>

这个目录的输出看起来非常简单,但是如果看看它的实现就比较复杂了。

5 top.container
这个块作为header的最后一个内容,实际上它是头部的一个容器,我们可以把内容放到这里面。
作为一个例子:

    <default>
		<reference name="top.container">
            <block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
        </reference>	
    </default>

那么头部最后将输出注册表单。

永久链接:http://blog.ifeeline.com/518.html
原创文章,转载务必保留出处。

Magento插件Layoutviewer详解

http://blog.ifeeline.com/369.html中,原文作者介绍了一个查看包布局 和 页面布局 和 句柄的插件:configviewer。虽然实现比较简单,但是比较有研究价值。

首先这个插件通过Magento中的事件触发而工作的。如果不理解这个,请参考http://blog.ifeeline.com/465.html

先看这个插件的配置文件

<?xml version="1.0"?>
<config>	
	<modules>
	<Vfeelit_Layoutviewer>
		<version>0.1.0</version>
	</Vfeelit_Layoutviewer></modules>

	<global>
		<models>
			<vfeelit_layoutviewer>
				<class>Vfeelit_Layoutviewer_Model</class>
			</vfeelit_layoutviewer>
			
			<core>
				<rewrite>
					<layout_update>Vfeelit_Layoutviewer_Model_Layout_Update</layout_update>
				</rewrite>
			</core>
			
		</models>
	
		<events>
			<controller_action_postdispatch>
				<observers>
					<vfeelit_layoutviewer_model_observer>
						<type>singleton</type>						
						<class>Vfeelit_Layoutviewer_Model_Observer</class>
						<method>checkForLayoutDisplayRequest</method>
					</vfeelit_layoutviewer_model_observer>
				</observers>
			</controller_action_postdispatch>
		</events>
	</global>	
</config>

首先是core里面的rewrite标签,这里提供了一个覆盖核心类的方法。比如,Mage:getModel(‘core/layout_update’),获取的是Mage_Core_Model_Layout_Update,通过rewrite,那么就会得到里面对应值(Vfeelit_Layoutviewer_Model_Layout_Update)。

相当于是使用Vfeelit_Layoutviewer_Model_Layout_Update替换了系统默认的Mage_Core_Model_Layout_Update。那么看看自定义的类:

	class Vfeelit_Layoutviewer_Model_Layout_Update extends Mage_Core_Model_Layout_Update {		
		public function getPackageLayout() {
			$this->fetchFileLayoutUpdates();
			return $this->_packageLayout;
		}		
	}

这个就是自定义的类的全部内容。注意,它是继承Mage_Core_Model_Layout_Update类的(必须),然后提供了一个自定义的方法:getPackageLayout()。当然,也可以覆盖继承过来的方法。这个就是Magento替换核心类的方案。

这里先跳到events标签,里面的controller_action_postdispatch是事件名,看起来是在控制器的分发之后触发的:

// Mage_Core_Controller_Varien_Action
    public function postDispatch()
    {
		//……
        Mage::dispatchEvent('controller_action_postdispatch', array('controller_action'=>$this));
    }

正如我们所见,的确如此。 控制器的分发过程可以参考http://blog.ifeeline.com/481.html。这个事件触发执行的代码基本可以认为是临近最后要执行的代码了,就等着把输出缓冲送回客户端了(执行完代码就输出),这个时候要执行Vfeelit_Layoutviewer_Model_Observer类对象的checkForLayoutDisplayRequest方法:

//Vfeelit_Layoutviewer_Model_Observer
		private function init() {
$this->setLayout(Mage::app()->getFrontController()->getAction()->getLayout());
			$this->setUpdate($this->getLayout()->getUpdate());
		}
		
		//entry point
		public function checkForLayoutDisplayRequest($observer) {			
			$this->init();
			$is_set = array_key_exists(self::FLAG_SHOW_LAYOUT, $_GET);
			if(		$is_set && 'package' == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputPackageLayout();
			}
			else if($is_set && 'page'    == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputPageLayout();			
			}
			else if($is_set && 'handles' == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputHandles();
			}
		}
 

首先执行的是init方法,它从控制器中获取布局对象,这个就是Mage_Core_Model_Layout。然后这个对象保存在_data[‘layout’]中。接下来从布局对象中调用getUpdate()方法获取layout_update对象,注意,layout_update已经通过rewrite指令修改为了Vfeelit_Layoutviewer_Model_Layout_Update类型,所以这个方法就会获取一个这个类型的对象引用保存到_data[‘update’]中。 然后根据条件调用outputPackageLayout 和 outputPageLayout 和 outputHandles方法:

接下来继续看具体方法:

	class Vfeelit_Layoutviewer_Model_Observer extends Varien_Object{
		const FLAG_SHOW_LAYOUT 			= 'showLayout';
		const FLAG_SHOW_LAYOUT_FORMAT 	= 'showLayoutFormat';		
		const HTTP_HEADER_TEXT			= 'Content-Type: text/plain';
		const HTTP_HEADER_HTML			= 'Content-Type: text/html';
		const HTTP_HEADER_XML			= 'Content-Type: text/xml';
		
		private $request;
		
		private function init() {
$this->setLayout(Mage::app()->getFrontController()->getAction()->getLayout());
			$this->setUpdate($this->getLayout()->getUpdate());
		}
		
		//entry point
		public function checkForLayoutDisplayRequest($observer) {			
			$this->init();
			$is_set = array_key_exists(self::FLAG_SHOW_LAYOUT, $_GET);
			if(		$is_set && 'package' == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputPackageLayout();
			}
			else if($is_set && 'page'    == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputPageLayout();			
			}
			else if($is_set && 'handles' == $_GET[self::FLAG_SHOW_LAYOUT]) {
				$this->outputHandles();
			}
		}

		private function outputHandles() {
			$update = $this->getUpdate();
			$handles = $update->getHandles();
			echo '<h1>','Handles For This Request','</h1>'."\n";
			echo '<ol>' . "\n";
			foreach($handles as $handle) {
				echo '<li>',$handle,'</li>';
			}
			echo '</ol>' . "\n";			
			die();
		}
		
		private function outputHeaders() {
			$is_set = array_key_exists(self::FLAG_SHOW_LAYOUT_FORMAT,$_GET);			
			$header		= self::HTTP_HEADER_XML;
			if($is_set && 'text' == $_GET[self::FLAG_SHOW_LAYOUT_FORMAT]) {
				$header = self::HTTP_HEADER_TEXT;
			}
			header($header);
		}
		
		private function outputPageLayout() {
			$layout = $this->getLayout();		//获取layout对象
			$this->outputHeaders();		
			die($layout->getNode()->asXML());	//直接从layout获取页面布局
		}
		
		private function outputPackageLayout() {
			$update = $this->getUpdate();
			$this->outputHeaders();
			die($update->getPackageLayout()->asXML()); //从layout_update对象中获取包布局
		}
	}

看outputPageLayout方法,调用update的getPackageLayout()方法,上面已经说过,系统的Layout_Update对象已经换成了Vfeelit_Layoutviewer_Model_Layout_Update,其实它仅仅是添加了getPackageLayout方法而已。它里面调用了继承过来的fetchFileLayoutUpdates()方法,它把所有的布局XML读取进来,然后放入到_packageLayout中。细节可以参考:http://blog.ifeeline.com/488.html

考虑到事件的触发是在postdispatch时,看起来,这个layout_update已经读取了相关的文件了,但是那必须是在控制器方法调用了loadLayout()为假设的,如果没有调用,那意味着Layout_update还没有读取布局xml文件。当要读取包布局时就是空白了。为了无论如何至少能把包布局呈现出来了,这里重写了Layout_Update类并实现了getPackageLayout方法,不管如何都读取包布局(如果没有执行loadLayout方法,句柄和页面布局是无法呈现的),但是这里的考虑还是有一点欠缺,因为layout_update的内容可能还有来自数据库的配置(目录产品CMS都可能应用自定义的布局配置,写入数据库中),方法中直接读取文件配置,没有考虑这个。改进方法是,如果有Layout_Update对象,就使用它,如果没有才强制手动读取:

	class Vfeelit_Layoutviewer_Model_Layout_Update extends Mage_Core_Model_Layout_Update {		
		public function getPackageLayout() {
			if(empty($this->_packageLayout)){
				$this->fetchFileLayoutUpdates();
			}
			return $this->_packageLayout;
		}		
}

这个工具是在做页面布局时可以使得我们可以查看包布局和页面布局和句柄,非常实用。同时,通过研究这个工具的代码,可以知道如果重写系统的核心类和系统的事件机制,还有控制器分发过程和包布局和页面布局与句柄的关系(使用句柄从包布局中剥离页面布局)。

永久链接:http://blog.ifeeline.com/512.html
原创文章,转载务必保留出处。

Magento页面构建page_html_notices详解

Magento布局模板XML

class Mage_Page_Block_Html_Notices extends Mage_Core_Block_Template
{
    public function displayNoscriptNotice()
    {
        return Mage::getStoreConfig('web/browser_capabilities/javascript');
    }

    public function displayDemoNotice()
    {
        return Mage::getStoreConfig('design/head/demonotice');
    }

    public function getPrivacyPolicyLink()
    {
        return Mage::getUrl('privacy-policy-cookie-restriction-mode');
    }
}

displayNoscriptNotice获取的配置在后台System-> Configuration->Web-> Browser Capabilities Detection

Magento后台设置JS禁用时的行为

当浏览器不支持JS时将获取提示:

浏览器不支持JS时将获取提示

displayDemoNotice设置在后台System->Configuration->Design->HTML Head-> Display Demo Store Notice

Magento Demo Store提示

getPrivacyPolicyLink根据给定的路由获取链接,在Browser Capabilities Detection下面还有一个叫Redirect to CMS-page if Cookies are Disabled的设置,意思是当客户端禁用Cookies时是否重定向到CMS页面。

永久链接:http://blog.ifeeline.com/506.html
原创文章,转载务必保留出处。

Magento块输出page_html_head块详解

<meta http-equiv="Content-Type" content="<?php echo $this->getContentType() ?>" />
<title><?php echo $this->getTitle() ?></title>
<meta name="description" content="<?php echo htmlspecialchars($this->getDescription()) ?>" />
<meta name="keywords" content="<?php echo htmlspecialchars($this->getKeywords()) ?>" />
<meta name="robots" content="<?php echo htmlspecialchars($this->getRobots()) ?>" />
<link rel="icon" href="<?php echo $this->getFaviconFile(); ?>" type="image/x-icon" />
<link rel="shortcut icon" href="<?php echo $this->getFaviconFile(); ?>" type="image/x-icon" />
<!--[if lt IE 7]>
<script type="text/javascript">
//<![CDATA[
    var BLANK_URL = '<?php echo $this->helper('core/js')->getJsUrl('blank.html') ?>';
    var BLANK_IMG = '<?php echo $this->helper('core/js')->getJsUrl('spacer.gif') ?>';
//]]>
</script>
<![endif]-->
<?php echo $this->getCssJsHtml() ?>
<?php echo $this->getChildHtml() ?>
<?php echo $this->helper('core/js')->getTranslatorScript() ?>
<?php echo $this->getIncludes() ?>

毫无疑问,这里面的方法都是head这个块提供的方法,这里面的这些输出语句可以认为是占位符。这些占位符是由块负责,那么好像跟控制器没有什么关系,Magento中的块引入替换了一般控制器的应该做的很多工作。

这个块提供了一些列的方法,很多方法的返回值的默认值在System->Configuration->Design->HTML Head中设置:
magento_head_setup

这里的很多设置对应了以下的XML

<head translate="default_description" module="page">
<default_title>Magento Commerce</default_title>
<default_description>Default Description</default_description>
<default_keywords>Magento, Varien, E-commerce</default_keywords>
<default_robots>INDEX,FOLLOW</default_robots>
<default_media_type>text/html</default_media_type>
<default_charset>utf-8</default_charset>
<title_prefix/>
<title_suffix/>
<includes/>
<demonotice>0</demonotice>
</head>

除了media_type和charset 和 Favicon ICO外,其它一一对应。media_type和charset安装之后就已经定,也没有提供set方法。

getContentType是getMediaType和getCharset包装器。

getMediaType 		媒体类型,总是text/html
getCharset			字符编码,utf-8
getTitle			获取标题 对应setTitle
getDescription		获取Meta描述 对应setDescription
getKeywords			获取Meta关键字 对应setKeywords
getRobots			获取机器人抓取设置
getIncludes			获取要包含到head中的代码
getFaviconFile		获取上传的Favicon文件
getIncludes			获取自定义的要在head中包含的代码

Title、Description和Keywords会根据不同的页而被改变,所有head块提供了对应的set方法。注意,对于有set方法的get方法,都是先获取已经设置的值,如果无法获取才使用默认值得,对于要设置这些值,会在构建块树时调用这些方法设置,到了渲染阶段就能获取正确值。

另外,看到使用helper(‘core/js’)助手类,这个类提供了一些有用的方法:

getTranslatorScript			设置翻译脚本
getScript($script)			直接获取一个script块
includeScript($file)		相当于addJs()
includeSkinScript($file)	相当于addItem(‘skin_js’,$file)
getJsUrl($file)				
getJsSkinUrl($file)

根据这个块的定义,有很多action在构建块树时被执行:
Magento的head输出xml配置

所以分别看看addJs、addCss和addItem分别干了什么工作:

    public function addCss($name, $params = "")
    {
        $this->addItem('skin_css', $name, $params);
        return $this;
    }

    public function addJs($name, $params = "")
    {
        $this->addItem('js', $name, $params);
        return $this;
}
	// type -> js  	$name -> prototype/prototype.js
    public function addItem($type, $name, $params=null, $if=null, $cond=null)
    {
        if ($type==='skin_css' && empty($params)) {
            $params = 'media="all"';
        }
		//$this_data[‘items’][‘js/prototype/prototype.js’] = array(type=>js, name=>prototype/prototype.js,params=>,if=>,cond=>) 
        $this->_data['items'][$type.'/'.$name] = array(
            'type'   => $type,
            'name'   => $name,
            'params' => $params,
            'if'     => $if,
            'cond'   => $cond,
       );
        return $this;
    }

可以看到addJs和addCss都是addItem的包装器。所有调用addItem()函数的,都存放在_date[‘items’]数组中。 在块中定义的这些aciton,在块构建时候就已经被执行了,不是在渲染的时候才进行的,这个必须谨记。

实际输出CSS/JS的是getCssJsHtml()。不过不要被这个名字困扰认为它只输出Css和Js,在addItem时不一定只添加Css和Js的引用,也可以添加其它类型的,所以getCssJsHtml是获取所有添加到_data[‘items’]中的输出。

从包装函数可以看出,如果想要带条件输出JS或CSS,就非要使用addItem不可了。从getCssJsHtml函数可以看出,针对JS和CSS的处理,有四种类型:

js			js/*.js			指在根目录下的js文件夹		addJs默认
skin_js		skin/*/*.js		指skin里面的的js
js_css		js/*.css		指在根目录下的js文件夹内		
skin_css	skin/*/*.css	指skin里面的的css 			addCss默认

从上面分析可知,如果要添加skin里面的js,你需要使用类型为skin_js,如果要添加根目录js里面的css,你需要使用类型为js_css。

head块里面定义了一个块,这个块非常简单,从core/cookie中获取相关参数,本身使用JS操作cookie其实非常方便:

class Mage_Page_Block_Js_Cookie extends Mage_Core_Block_Template
{
    public function getCookie()
    {
        return Mage::getSingleton('core/cookie');
    }
    public function getDomain()
    {
        $domain = $this->getCookie()->getDomain();
        if (!empty($domain[0]) && ($domain[0] !== '.')) {
            $domain = '.'.$domain;
        }
        return $domain;
    }

    public function getPath()
    {
        return $this->getCookie()->getPath();
    }
}

这提供的几个方法都非常简单,对应的模板:

<script type="text/javascript">
//<![CDATA[
Mage.Cookies.path     = '<?php echo $this->getPath()?>';
Mage.Cookies.domain   = '<?php echo $this->getDomain()?>';
//]]>
</script>

注意,这里调用getChildHtml()方法是并没有指定名字给它,默认如果没有指定名字,它就是指获取所有的块的HTML。为了避免风险,一般应该指定名字。

基本上,这个就是head块的全部内容。

永久链接:http://blog.ifeeline.com/501.html
原创文章,转载务必保留出处。

Magento块输出page_html块详解

整个页面的输出就是一个HTML块。这个块是输出的起点,通常被命名为root,而且带有output属性,属性值一般都是toHtml(),它是启动输出的一个方法。可以使用template指出默认使用的模板,这个默认是针对这个HTML块的,它是一个框架,我们常见的全局布局。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<head>
<?php echo $this->getChildHtml('head') ?>
</head>
<body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
<?php echo $this->getChildHtml('after_body_start') ?>
<div class="wrapper">
    <?php echo $this->getChildHtml('global_notices') ?>
    <div class="page">
        <?php echo $this->getChildHtml('header') ?>
        <div class="main-container col3-layout">
            <div class="main">
                <?php echo $this->getChildHtml('breadcrumbs') ?>
                <div class="col-wrapper">
                    <div class="col-main">
                        <?php echo $this->getChildHtml('global_messages') ?>
                        <?php echo $this->getChildHtml('content') ?>
                    </div>
                    <div class="col-left sidebar"><?php echo $this->getChildHtml('left') ?></div>
                </div>
                <div class="col-right sidebar"><?php echo $this->getChildHtml('right') ?></div>
            </div>
        </div>
        <?php echo $this->getChildHtml('footer') ?>
        <?php echo $this->getChildHtml('before_body_end') ?>
    </div>
</div>
<?php echo $this->getAbsoluteFooter() ?>
</body>
</html>

这个块对应的类为Mage_Page_Block_Html。不去管那些继承过来的方法,这个类是Magento中比较简单的类之一。

首先看构造函数:

    public function __construct()
    {
        parent::__construct();
        $this->_urls = array(
            'base'      => Mage::getBaseUrl('web'),
            'baseSecure'=> Mage::getBaseUrl('web', true),
            'current'   => $this->getRequest()->getRequestUri()
        );

        $action = Mage::app()->getFrontController()->getAction();
        if ($action) {
            $this->addBodyClass($action->getFullActionName('-'));
        }

        $this->_beforeCacheUrl();
}
// base 			->  web/unsecure/base_url
// baseSecure 	->  web/secure/base_url
// current		->	URI

首先设置了_urls,它有三个参数,安全连接和非安全连接和当前请求的URI。然后把控制器的名字作为body类名。

紧接这提供了三个方法分别取回非安全连接 和 安全连接 和 当前的URI。

    public function getBaseUrl()
    {
        return $this->_urls['base'];
    }

    public function getBaseSecureUrl()
    {
        return $this->_urls['baseSecure'];
    }

    public function getCurrentUrl()
    {
        return $this->_urls['current'];
    }

然后提供了一个getPrintLogoUrl(),用来获取打印发票和装箱单的Logon。对应了getPrintLogoText。

然后是setHeaderTitle和getHeaderTitle,暂时看不出它们有什么用。

addBodyClass用来给body标签添加类,在构造函数中调用,一般是控制器名字,它可以为CSS控制页面预留钩子。

HTML块用到的如下两个方法:

    public function getLang()
    {
        if (!$this->hasData('lang')) {
            $this->setData('lang', substr(Mage::app()->getLocale()->getLocaleCode(), 0, 2));
        }
        return $this->getData('lang');
}

    public function getAbsoluteFooter()
    {
        return Mage::getStoreConfig('design/footer/absolute_footer');
    }

先看看getAbsoluteFooter方法,实际它是获取design/footer/absolute_footer的值

<design>
<footer translate="copyright" module="page">
<copyright>&copy; 2012 Magento Demo Store. All Rights Reserved.</copyright>
<absolute_footer/>
</footer>
</design>

而这个配置是在后台System->Configuration->Design->Footer,对应了这里的两个部分。absolute_footer就是保存了在页脚之后希望插入的标签,比如友情链接这些等。

getLang从Locale对象中获取本地代码的前两个字符,实际上就是国家二字代码。对比一下模板就知道它在哪里放置:

http://blog.ifeeline.com/498.html
原创文章,转载务必保持出处。

Magento页面构建源码分析

Magento块继承层次

大部分的块都是从Mage_Core_Block_Template继承的,部分块会加入自己的抽象类(如上所示),间接继承自Mage_Core_Block_Template。

对应一个输出块,对有一个output属性,它的值应该总是toHtml,它是视图渲染的进入点。一般每个页面只有一个块有output属性,有多个块具有output属性也是允许的,这样它会把多个块分别渲染后合并在一起。如果指定了template,那么块的调用的第一个方法将是:

    public function setTemplate($template)
    {
        $this->_template = $template;
        return $this;
    }

而对应没有给出template的块,一般会实现_construct函数,里面调用setTemplate函数:

    protected function _construct()
    {
        $this->setTemplate('page/html/head.phtml');
    }

在Mage_Core_Block_Template中实现的构造函数会执行_construct()函数。

对应给定了output=toHtml的块,接下来调用它的toHtml()方法:

//Mage_Core_Block_Abstract
    final public function toHtml()
    {
        Mage::dispatchEvent('core_block_abstract_to_html_before', array('block' => $this));
		// 模块状态
        if (Mage::getStoreConfig('advanced/modules_disable_output/' . $this->getModuleName())) {
            return '';
        }
		//缓存
        $html = $this->_loadCache();
        if ($html === false) {
            $translate = Mage::getSingleton('core/translate');
            /** @var $translate Mage_Core_Model_Translate */
            if ($this->hasData('translate_inline')) {
                $translate->setTranslateInline($this->getData('translate_inline'));
            }

            $this->_beforeToHtml();
            $html = $this->_toHtml();
            $this->_saveCache($html); //保存缓存

            if ($this->hasData('translate_inline')) {
                $translate->setTranslateInline(true);
            }
        }
        $html = $this->_afterToHtml($html);

        /**
         * Check framing options
         */
        if ($this->_frameOpenTag) {
            $html = '<'.$this->_frameOpenTag.'>'.$html.'<'.$this->_frameCloseTag.'>';
        }

        /**
         * Use single transport object instance for all blocks
         */
        if (self::$_transportObject === null) {
            self::$_transportObject = new Varien_Object;
        }
        self::$_transportObject->setHtml($html); //_data[‘html’]= $html
        Mage::dispatchEvent('core_block_abstract_to_html_after',
                array('block' => $this, 'transport' => self::$_transportObject));
        $html = self::$_transportObject->getHtml();

        return $html;
    }

看起来很复杂,实际上它是间接调用了_toHtml(),注意toHtml是final方法,它不允许被覆盖,所以,自定义的实现应该放在_toHtml()方法中:

// Mage_Core_Block_Template
    protected function _toHtml()
{
		//如果没有模板,直接返回了
        if (!$this->getTemplate()) {
            return '';
        }
        $html = $this->renderView();
        return $html;
}

    public function renderView()
{
		// Mage::getBaseDir('design')获得/…/app/design
		// $this->_viewDir = /…/app/design;
        $this->setScriptPath(Mage::getBaseDir('design'));
		//$this->getTemplateFile()获取模板文件,如果找不到会逐层返回
		//frontend\base\default\template\page/3columns.phtml
        $html = $this->fetchView($this->getTemplateFile());
        return $html;
    }

这个fetchView看起来复杂,其实就是把模板文件include进来,如此而已。

    public function fetchView($fileName)
    {
        // EXTR_SKIP protects from overriding
        // already defined variables
        extract ($this->_viewVars, EXTR_SKIP); //把视图变量释放进入
        //$do = $this->getDirectOutput(); //直接输出,不缓存,总是false
		try{
            $includeFilePath = realpath($this->_viewDir . DS . $fileName);
            if (strpos($includeFilePath, realpath($this->_viewDir)) === 0 || $this->_getAllowSymlinks()) {
                include $includeFilePath;
            }
		}
	}

从这个作为入口,层层深入(一般都是一个整体模板文件,比如1列2列3列布局等):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<head>
<?php echo $this->getChildHtml('head') ?>
</head>
<body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
<?php echo $this->getChildHtml('after_body_start') ?>
<div class="wrapper">
    <?php echo $this->getChildHtml('global_notices') ?>
    <div class="page">
        <?php echo $this->getChildHtml('header') ?>
        <div class="main-container col3-layout">
            <div class="main">
                <?php echo $this->getChildHtml('breadcrumbs') ?>
                <div class="col-wrapper">
                    <div class="col-main">
                        <?php echo $this->getChildHtml('global_messages') ?>
                        <?php echo $this->getChildHtml('content') ?>
                    </div>
                    <div class="col-left sidebar"><?php echo $this->getChildHtml('left') ?></div>
                </div>
                <div class="col-right sidebar"><?php echo $this->getChildHtml('right') ?></div>
            </div>
        </div>
        <?php echo $this->getChildHtml('footer') ?>
        <?php echo $this->getChildHtml('before_body_end') ?>
    </div>
</div>
<?php echo $this->getAbsoluteFooter() ?>
</body>
</html>

由于模板本身就是include进来的,那么$this当然就是指当前的块了。getChildHtml()自然也是当前块的方法,这个方法可以调用子块,继续进行下去。页面就是如此构建的。

    public function getChildHtml($name = '', $useCache = true, $sorted = false)
    {
        if ($name === '') {
        } else {
            return $this->_getChildHtml($name, $useCache);
        }
}
    protected function _getChildHtml($name, $useCache = true)
    {
        $child = $this->getChild($name);

        if (!$child) {
            $html = '';
        } else {
            $this->_beforeChildToHtml($name, $child);
            $html = $child->toHtml();
        }

        $this->_childrenHtmlCache[$name] = $html;
        return $html;
    }

看到了吧,getChildHtml()最终还是调用了块的toHtml()方法加载模板,而这个方法就是include模板文件。(当然了,务必记住,一个块是否能使用getChildHtml()把子块调进来,关键在于这个块已经定义了这个子块,否则不工作)

永久链接:http://blog.ifeeline.com/495.html
原创文章,转载务必保留出处。

Magento布局语法源码分析

在控制器方法中调用的loadLayout()方法中,最后一步是根据构建的XML内容创建布局块(generateLayoutBlocks方法),我们从源代码分析一下布局块的应该如何使用。

public function generateLayoutBlocks()
{
   	 $this->getLayout()->generateBlocks();
    	return $this;
}

// Mage_Core_Model_Layout
    public function generateBlocks($parent=null)
    {
        if (empty($parent)) {
            $parent = $this->getNode(); //$this->_xml
        }
        foreach ($parent as $node) {
            $attributes = $node->attributes(); //属性
	    //在生成xml时,已经对所有remove节点添加了ignore为true的属性
            if ((bool)$attributes->ignore) { 
                continue;
            }
	    //节点名 block reference action
            switch ($node->getName()) { 
                case 'block':
                    $this->_generateBlock($node, $parent); 
                    //$this->_blocks[$blockName] = new $className();  节点的name值只是一个_blocks数组中的下标
                    $this->generateBlocks($node);  // 递归
                    break;

                case 'reference':
                    $this->generateBlocks($node); // 递归
                    break;

                case 'action':  //通过递归,层层进入
                    $this->_generateAction($node, $parent);
                    break;
            }
        }
    }

从这个方法可以看到,如果节点名是block,就会创建这个块(_generateBlock,稍后探讨细节),然后递归(如果块还包含了块,那么继续创建这个块),如果是reference直接递归进入内层(所以内层要么是block,要么是action,因为refernce表示引用别的块,不需要做任何创建的动作)。如果是action,调用_generateAction。通过这个循环递归的过程,一个块树就建立起来了。

看看这个块树建立的逻辑:

    protected function _generateBlock($node, $parent)
    {
        //block节点 可以使用type 和 class 属性指定它的类型
        if (!empty($node['class'])) {	
            $className = (string)$node['class'];
        } else {
            $className = (string)$node['type'];
        }
        //节点name属性值
        $blockName = (string)$node['name']; 

        //$this->_blocks[$blockName] = new $className();  节点的name值只是一个_blocks数组中的下标 ***********
        $block = $this->addBlock($className, $blockName); 
        if (!$block) {
            return $this;
        }

        //用parent指定了父节点,否则采用函数传递进来的
        if (!empty($node['parent'])) {  
            $parentBlock = $this->getBlock((string)$node['parent']);
        } else {
            $parentName = $parent->getBlockName();
            
            if (!empty($parentName)) {
                $parentBlock = $this->getBlock($parentName);
            }
        }
        // 父节点不应该为空
        if (!empty($parentBlock)) { 
            //as属性记录了别名 一般和name属性一致,实际上节点插入时跟name的名字“绑定了”,如果使用上当要reference是要用name的名字,当在模板中调用getChildHtml()时,要指定as的名字(质疑这个实现)。
            $alias = isset($node['as']) ? (string)$node['as'] : '';  
            if (isset($node['before'])) {
                $sibling = (string)$node['before'];
                if ('-'===$sibling) {
                    $sibling = '';
                }
                //插入到父节点下,在$sibling前
                $parentBlock->insert($block, $sibling, false, $alias); 
            } elseif (isset($node['after'])) {
                $sibling = (string)$node['after'];
                if ('-'===$sibling) {
                    $sibling = '';
                }
		//插入到父节点下 在$sibling后
                $parentBlock->insert($block, $sibling, true, $alias);  
            } else {
                //否者就是附加
                $parentBlock->append($block, $alias); 
            }
        }
        if (!empty($node['template'])) { //如果有template,那么实际就是调用块的setTemplate()方法,为这个块分配模板
            $block->setTemplate((string)$node['template']);
        }
        //块的output属性保存了一个方法,它总是应该是toHtml,一般一个页面只有一个块有output属性,它是输出的起点。注意,这个块用_output数组保存,但是它之前已经添加到了块树中。
        if (!empty($node['output'])) {	
            $method = (string)$node['output'];
            $this->addOutputBlock($blockName, $method);
/*
    public function addOutputBlock($blockName, $method='toHtml')
   {	
	//它用了_output[块名]=array(块名,方法名)
        $this->_output[$blockName] = array($blockName, $method);
        return $this;
    }
*/
        }

        return $this;
    }

这里先看看block的语法:

name		一个在布局对象中_blocks数组的下标
as		别名,一般和name一致,refernce使用name,getChildHtml()时用as
parent		指出父块
before		在块之前的块
after		在块之后的块
template	块模板,实际调用块的setTemplate()方法
output		实际调用块的addOutputBlock方法,把output指定的值保存到一个数组,它是渲染的起点,一般都是toHtml

至于refernce,只有name属性,它必须和已经存在的块的name匹配,表示对已经存在的块的操作。

基本上,块树建立的逻辑就是如此,但是还有一个可以为action的节点,看看_generateAction方法:

    protected function _generateAction($node, $parent)
    {
        if (isset($node['ifconfig']) && ($configPath = (string)$node['ifconfig'])) {
            if (!Mage::getStoreConfigFlag($configPath)) {
                return $this;
            }
        }
	// action的method属性值
        $method = (string)$node['method'];
        // action可以使用block指出它的父块,如果没有指定,那就是直接包含它的块
        if (!empty($node['block'])) {
            $parentName = (string)$node['block'];
        } else {
            $parentName = $parent->getBlockName();
        }

        //通过名字,直接引用块树中的块
        if (!empty($parentName)) {
            $block = $this->getBlock($parentName);
        }
        if (!empty($block)) {

            $args = (array)$node->children();
            unset($args['@attributes']);
	    //循环子节点 处理子节点
            foreach ($args as $key => $arg) {
            	//每个子节点都应该是Mage_Core_Model_Layout_Element类型的实例
                if (($arg instanceof Mage_Core_Model_Layout_Element)) {
		    //子节点有包含helper属性,马上转换
                    if (isset($arg['helper'])) { 
                        //比如<url helper="customer/getLoginUrl" />
                        $helperName = explode('/', (string)$arg['helper']); 
                        $helperMethod = array_pop($helperName); //getLoginUrl
                        $helperName = implode('/', $helperName); //customer
                        $arg = $arg->asArray(); //参数
                        unset($arg['@']);
					   //$args[url]=http:://…
                        $args[$key] = call_user_func_array(array(Mage::helper($helperName), $helperMethod), $arg); 
                    } else {
                        /**
                         * if there is no helper we hope that this is assoc array
                         */
                        //处理action子节点还包含子节点的情况,一般没有这种情况
                        $arr = array();
                        foreach($arg as $subkey => $value) {
                            $arr[(string)$subkey] = $value->asArray();
                        }
                        if (!empty($arr)) {
                            $args[$key] = $arr;
                        }
                    }
                }
            }

            if (isset($node['json'])) { //action节点还可以带上一个json属性
                $json = explode(' ', (string)$node['json']);
                foreach ($json as $arg) {
                    $args[$arg] = Mage::helper('core')->jsonDecode($args[$arg]);
                }
            }

            $this->_translateLayoutNode($node, $args);
            //调用这个块的指定方法,并把子节点作为参数数组
            call_user_func_array(array($block, $method), $args); 
        }
        return $this;
    }

为了配合这段代码,先附上一个XML:

<reference name="top.links">
<action method="addLink" translate="label title" module="customer">
<label>Log In</label>
<url helper="customer/getLoginUrl"/>
<title>Log In</title>
<prepare/>
<urlParams/>
<position>100</position>
</action>
</reference>

从代码逻辑来看,action节点用method指定了将调用块的方法,用block来指定这个方法的块,用ifconfig来获取后台配置值,如果检查到值返回了假值,这个action不执行。action中的子节点都将作为你method的参数,如果子节点包含了helper,马上调用这个herper转换成键值对。子节点还可以包含子节点,它将转换成一个数组。这些子节点的顺序必须和method的参数顺序一致。比如上面的XML将转换成:

Array
(
    [label] => Log In
    [url] => http://learn.magento.com/index.php/customer/account/login/
    [title] => Log In
    [prepare] => 
    [urlParams] =>
    [position] => 100
)

这里就是布局的大部分内容。可以调整这些嵌套关系,获取不同的页面布局。

永久链接: http://blog.ifeeline.com/493.html
原创文章,转载务必保留出处。

Magento布局加载与Render

如果每个控制器要应用布局,最后还需要明确调用:

$this->loadLayout();
$this->renderLayout();

我们从这里进入查看实现细节:

	// 一般具体的控制器中可能会添加自己的的$handlers
    public function loadLayout($handles = null, $generateBlocks = true, $generateXml = true)
    {
        // if handles were specified in arguments load them first
		// 绝大部分情况下,都会添加default
        if (false!==$handles && ''!==$handles) { //指定了handlers
            $this->getLayout()->getUpdate()->addHandle($handles ? $handles : 'default');
        }

        // add default layout handles for this action
		// 添加三个handles
/*
•  STORE_german
•  THEME_frontend_default_default
•  helloworld_index_index
*/
        $this->addActionLayoutHandles(); 	//******  	添加句柄

        $this->loadLayoutUpdates();			//******	加载布局

        if (!$generateXml) {
            return $this;
        }
		// Mage_Core_Model_Layout 的 _xml
        $this->generateLayoutXml();			//******
/*
    public function generateLayoutXml()
{
        // generate xml from collected text updates
        $this->getLayout()->generateXml();
        return $this;
}
//Mage_Core_Model_Layout
    public function generateXml()
    {
        $xml = $this->getUpdate()->asSimplexml();
		//…… 寻找remove节点
        $this->setXml($xml);
        return $this;
}
*/
        if (!$generateBlocks) {
            return $this;
        }
        $this->generateLayoutBlocks();		//******
/* 根据_xml参数 创建块
    public function generateLayoutBlocks()
{
    	$this->getLayout()->generateBlocks();
		return $this;	
}
*/
        $this->_isLayoutLoaded = true;		//标记布局已经加载

        return $this;
}

添加句柄,是往Layout_Update对象中添加句柄,这个为后面从包布局中剥离需要的XML提供句柄。实际上,每个控制器都可以直接调用这个对象的addHandle()方法添加句柄。

    public function addActionLayoutHandles()
{
		//获取一个Mage_Core_Model_Layout单例对象中获取Mage_Core_Layout_Update对象
$update = $this->getLayout()->getUpdate();

        // load store handle    STORE_加商店标识码
		// $update-> _handles[‘STORE_ default’] = 1;
        $update->addHandle('STORE_'.Mage::app()->getStore()->getCode());

        // load theme handle
        $package = Mage::getSingleton('core/design_package');
		// $package->getArea() 得到 frontend    $package->getPackageName() 获取模板的包名比如default  $package->getTheme('layout')得到default
		// $update->_handles[‘THEME_frontend_default_default’] = 1
        $update->addHandle(
            'THEME_'.$package->getArea().'_'.$package->getPackageName().'_'.$package->getTheme('layout')
        );

        // load action handle
		// 模块名_控制器名_方法名
        $update->addHandle(strtolower($this->getFullActionName()));

        return $this;
}

句柄添加了之后,就是加载布局。它实际使用Layout_Update对象的load()方法。

    public function loadLayoutUpdates()
{
		// …
        $this->getLayout()->getUpdate()->load();
		// …
        return $this;
    }

上一步完成,其实就已经把所有的布局XML都已经提取出来了。不过要知道如何做到的,就需要继续跟入load()方法了:

// Mage_Core_Model_Layout_Update
    public function load($handles=array())
{
/*
default
STORE_german
THEME_frontend_default_default
helloworld_index_index
customer_logged_out
*/
        foreach ($handles as $handle) {
            $this->addHandle($handle);
        }
		// 循环全部已经添加的handle
        foreach ($this->getHandles() as $handle) {
            $this->merge($handle);
        }

        return $this;
    }
    public function merge($handle)
    {
        $packageUpdatesStatus = $this->fetchPackageLayoutUpdates($handle);
        if (Mage::app()->isInstalled()) {
			//从数据库中获取针对$handle的信息
            $this->fetchDbLayoutUpdates($handle);
        }
        return $this;
}
// fetchPackageLayoutUpdates会被执行多次,但是$this->_packageLayout对象只有一份,它保存了所有的布局合并了的文件,这个方法的多次执行中,如果遇到了update节点,还返回执行merge方法继续进入,这个过程只是为了添加update节点引用的句柄
    public function fetchPackageLayoutUpdates($handle)
    {
 
        if (empty($this->_packageLayout)) {
//设置$this->_packageLayout,Mage_Core_Model_Layout_Element类型   ************ 首先把布局文件全局加载进来
            $this->fetchFileLayoutUpdates();  
        }
		// 从包布局中提取 $handle, 包中可能包含多个节点,对应一个$handle
        foreach ($this->_packageLayout->$handle as $updateXml) {
//如果没有update子节点,这个方法什么都不做,否则添加update子节点handle属性指定的handle
            $this->fetchRecursiveUpdates($updateXml);  

/*--------------
    public function fetchRecursiveUpdates($updateXml)
    {
        foreach ($updateXml->children() as $child) {

// 			如果handle里的子标签名是update,并且对应了属性handle
//         	<helloworld_index_index>
//         		<reference name="root">
//         			<action method="setTemplate">
//         				<template>vfeelit/helloworld/simple_page.phtml</template>
//         			</action>
//         			<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
//         		</reference>
//         	</helloworld_index_index>
//         	<helloworld_index_goodbye>
//         		<update handle="helloworld_index_index"/>
//         	</helloworld_index_goodbye>

        	    // 使用了update节点,用handle指定了使用其它handle,
            if (strtolower($child->getName())=='update' && isset($child['handle'])) {
                $this->merge((string)$child['handle']);  //递归
                // Adding merged layout handle to the list of applied hanles
                $this->addHandle((string)$child['handle']);
            }
        }
        return $this;
    }
************/

//把所有与$handle匹配的 全部记录到$this_updates数组中,其实它就是记录了当前页面的所有布局信息 可以用asArray() 或 asString() 或asSimpleXml()查看内容
            $this->addUpdate($updateXml->innerXml());  
/*
Array
(
    [0] => <block name="formkey" type="core/template" template="core/formkey.phtml"/>
)
*/        }

        return true;
    }

到这步,Update对象已经从所有的布局文件中根据句柄提取了需要的布局文件。包的布局文件是如何加载合并的呢? 这个要看fetchFileLayoutUpdates()方法:

    public function fetchFileLayoutUpdates()
    {
        $storeId = Mage::app()->getStore()->getId(); 	// 当前store id
        $elementClass = $this->getElementClass();		// 布局元素类型
		// Mage_Core_Model_Design_Package 单例对象,用它来代表包布局
        $design = Mage::getSingleton('core/design_package');
        if (empty($layoutStr)) {
            $this->_packageLayout = $this->getFileLayoutUpdatesXml(
                $design->getArea(),
                $design->getPackageName(),
                $design->getTheme('layout'),
                $storeId
            );
    }
		return $this;
}

它又使用getFileLayoutUpdatesXml()寻找布局文件的:

//Mage_Core_Model_Layout_Update

    public function getFileLayoutUpdatesXml($area, $package, $theme, $storeId = null) //frontend  default  default  1
    {
        if (null === $storeId) {
            $storeId = Mage::app()->getStore()->getId();
        }
        /* @var $design Mage_Core_Model_Design_Package */
        $design = Mage::getSingleton('core/design_package');
        $layoutXml = null;
        $elementClass = $this->getElementClass();
        $updatesRoot = Mage::app()->getConfig()->getNode($area.'/layout/updates');
// 这部分信息来自每个模块的的config.xml配置文件,这个指出了模块的布局文件
// <default>
// <layout>
// 	<updates>
// 		<core><file>core.xml</file></core>
// 		<review module="Mage_Review"><file>review.xml</file></review>
// 	</updates>
// </layout>
// </default>
        
        Mage::dispatchEvent('core_layout_update_updates_get_after', array('updates' => $updatesRoot));
        $updateFiles = array();
		//循环所有模块,收集文件
        foreach ($updatesRoot->children() as $updateNode) {
            if ($updateNode->file) {
                $module = $updateNode->getAttribute('module'); //有些模块会有module属性
				//模块禁用了
                if ($module && Mage::getStoreConfigFlag('advanced/modules_disable_output/' . $module, $storeId)) {
                    continue;
                }
                $updateFiles[] = (string)$updateNode->file; //获取文件
            }
        }
        // custom local layout updates file - load always last
        $updateFiles[] = 'local.xml';	//最后添加local.xml

//         print_r($updateFiles);
//         Array
//         (
//         		[0] => core.xml
//         		[1] => page.xml
//         		[2] => directory.xml
//         		[3] => cms.xml
//         		[4] => customer.xml
//         		[5] => catalog.xml
// 				………
//         		[41] => local.xml
//         )

        $layoutStr = '';
		//循环找到的文件名,然后一个个加载进来
        foreach ($updateFiles as $file) {
        		//获取每个模块的布局文件,如果在指定的包 和 模板中找不到,则使用默认的, Design类的getLayoutFilename()方法实现了这个逻辑(间接)
            $filename = $design->getLayoutFilename($file, array(
                '_area'    => $area,
                '_package' => $package,
                '_theme'   => $theme
            ));
            if (!is_readable($filename)) {
                continue;
            }
            $fileStr = file_get_contents($filename);  //把内容读进来
            
/*
 * 其中的占位符使用实体替换
Array
(
    [0] => {{baseUrl}}
    [1] => {{baseSecureUrl}}
)
Array
(
    [0] => http://learn.magento.com/index.php/
    [1] => http://learn.magento.com/index.php/
)
*/
            $fileStr = str_replace($this->_subst['from'], $this->_subst['to'], $fileStr);
            $fileXml = simplexml_load_string($fileStr, $elementClass);
            if (!$fileXml instanceof SimpleXMLElement) {
                continue;
            }
            $layoutStr .= $fileXml->innerXml();
        }
		// 合并非常暴力
        $layoutXml = simplexml_load_string('<layouts>'.$layoutStr.'</layouts>', $elementClass);
        return $layoutXml;
       
    }

接下来看看布局渲染

    public function renderLayout($output='')
    {
        if ($this->getFlag('', 'no-renderLayout')) {
            return;
        }

        if (Mage::app()->getFrontController()->getNoRender()) {
            return;
        }

        $this->_renderTitles(); // _title没有设置  可以修改标题


        if (''!==$output) { //指定了输出块 添加到_output数组中
            $this->getLayout()->addOutputBlock($output);
        }

//布局对象_directOutput = false 默认就是false
        $this->getLayout()->setDirectOutput(false); 

        $output = $this->getLayout()->getOutput();
/*         
 		public function getOutput()
        {
        	$out = '';
        	if (!empty($this->_output)) {
        		foreach ($this->_output as $callback) { // $callback[0]块名  $callback[1]回调函数
        			$out .= $this->getBlock($callback[0])->$callback[1]();
        		}
        	}
        
        	return $out;
        } 
*/
        Mage::getSingleton('core/translate_inline')->processResponseBody($output); //处理翻译
        $this->getResponse()->appendBody($output);  //输出

        return $this;
} 

布局的渲染是相当的简单。把保存在_output数组中的块循环,它返回的内容合并。首先丢给翻译对象处理一下,然后丢给响应对象。

Magento布局相关类

Mage_Core_Model_Layout对象的初始化

    public function __construct($data=array())
{
		// 获得Mage_Core_Model_Layout_Element字符串
        $this->_elementClass = Mage::getConfig()->getModelClassName('core/layout_element');
		// simplexml_load_string生成一个Mage_Core_Model_Layout_Element对象集合的simplexml对象,把这个对象传递给setXml,这个方法在父类中,实际得到$this->_xml = simplexml对象;
        $this->setXml(simplexml_load_string('<layout/>', $this->_elementClass));
		// 获取一个Mage_Core_Layout_Update的对象
        $this->_update = Mage::getModel('core/layout_update');
        parent::__construct($data);
    }

布局对象初始化完成。(最重要的的是得到一个$this->_xml和一个$this->_update)

Mage_Core_Model_Layout_Update 对象的初始化

    public function __construct()
    {
        $subst = Mage::getConfig()->getPathVars();
/*
    public function getPathVars($args=null)
    {
        $path = array();

        $path['baseUrl'] = Mage::getBaseUrl();
        $path['baseSecureUrl'] = Mage::getBaseUrl('link', true);

        return $path;
    }
*/
//         Array
//         (
//         		[baseUrl] => http://learn.magento.com/index.php/
//         		[baseSecureUrl] => http://learn.magento.com/index.php/
//         )
        foreach ($subst as $k=>$v) {
            $this->_subst['from'][] = '{{'.$k.'}}';
            $this->_subst['to'][] = $v;
        }
//         $this->_subst['from'][0] = {{baseUrl}}
//         $this->_subst['to'][0] = {{http://learn.magento.com/index.php/}}
//         $this->_subst['from'][1] = {{baseSecureUrl}}
//         $this->_subst['to'][1] = {{http://learn.magento.com/index.php/}}
}
 

Mage_Core_Model_Design_Package对象的初始化
这个类并没有提供构造函数,它是单例对象,但是初始化并不是在加载布局时候进行的。

这个类初始化是在preDispatch()方法中的调用

Mage::app()->loadArea($this->getLayout()->getArea());

进行初始化的。这个方法最终会调用Mage_Core_Model_App_Area的_initDesign方法

    protected function _initDesign()  //Design初始化
    {
        if (Mage::app()->getRequest()->isStraight()) {
            return $this;
        }
        $designPackage = Mage::getSingleton('core/design_package');
        if ($designPackage->getArea() != self::AREA_FRONTEND)
            return;

        $currentStore = Mage::app()->getStore()->getStoreId();

        $designChange = Mage::getSingleton('core/design')
            ->loadChange($currentStore); //Mage_Core_Model_Design
        
        if ($designChange->getData()) {
            $designPackage->setPackageName($designChange->getPackage())
                ->setTheme($designChange->getTheme());
        }
    }

在后台,System->Design提供了一种改变设计的方法,它把这个改变记录到design_change表中。 所以在这里可能会改变包名 和 主题名。

永久链接:http://blog.ifeeline.com/488.html
原创文章,转载务必保留出处。