A universal recursion method for Magento

Magento 历遍 category 时用的 Tree/Node 模型,我看了一知半解。当我把 CMS page 整理成树状,想对所有 pages 如同 category 历遍操作时,我抛开 Tree/Node,就写了两个简单的方法来套用(感谢 php 5.3 让这方法成为可能)。

public function recurseSubpages($function, &$params = null) {
$this->_recurse($this, $function, 0, $params);
}

protected function _recurse($page, $function, $level, &$params = null) {
$function($page, $level, $params);
foreach ($page->getSubpageCollection() as $subpage) {
$this->_recurse($subpage, $function, $level + 1, $params);
}
}

若要 CMS page 历遍 someOperation(),则只要临时定义一个 $function,然后调用 $page->recurseSubpages(),具体代码是

$function = function($page, $level, &$params) {
$page->someOperation();
};
Mage::getModel('cms/page')->recurseSubpages($function);

$params 是用来回传参数的,如果不需要回传,可以省略。

我认为这样让复杂的递归变得很简单,可是在想做一张 html 格式的 CMS page sitemap (用<ul><li>…</li></ul>层次嵌套体现 CMS page 的层次,如同 Magento top navigation 那段代码。顺便说一下,我认为 Magento category sitemap 的代码不够好,它把树形结构线性化了,不够 semantic)时,遇到困难。当时我觉得要在 _recurse() 里再次调用 _recurse() 之前和之后有条件地插入一些操作,不是简单定义一个 $function 可以做到的。

所以第一版的 CMS page sitemap 绕开了我自己的 page recurse 模型独自写了很长一段代码,大意如下:

public function prepareSitemap() {
if ($id = Mage::getSingleton('cms/page')->getId()) {
$page = Mage::getModel('cpfp/page')->load($id);
}
else {
$page = Mage::getModel('cpfp/page');
}
$this->_output = self::_recurseSitemap($page, 0);
return $this;
}

protected function _recurseSitemap($page, $level) {
$output = '';
$hasId = ($page->getId())?true:false;
$title = Mage::helper('cpfp')->htmlEscape($page->getTitle());

foreach ($page->getSubpageCollection() as $subpage) {
$output .= self::_recurseSitemap($subpage, $level+1);
}

if ($hasId) {
if ('xhtml' == $this->_outputFormat) {
$pageContent = "<a href='{$this->getUrl($page->getIdentifier())}'>" . $title . '</a>';
if ($output) {
$output = "<li>$pageContent<ul>$output</ul></li>";
}
else {
$output = "<li>$pageContent</li>";
}
}
elseif ('text' == $this->_outputFormat) {
$pageContent = str_repeat("\t", $level) . $title . "\n";
$output = $pageContent . $output;
}
else {
//do nothing
}
}
else { //!$hasId
//do nothing
}

if ($level == 0 && $output && 'xhtml' == $this->_outputFormat) { //final wrap here
$output = '<ul>' . $output . '</ul>';
}
return $output;
}

以上代码是放在一个 Block 类里,该 Block 没有使用 template 直接输出了 html。Magento top navigation 也是由 Mage_Catalog_Block_Navigation 直接输出了 html,所以当时我想,输出递归结果也只能是这样了。

过了一段时间,突然有了灵感,其实还有更好的方法输出递归结果!具体步骤是:

  1. 定义这么一个 $function,把当前 $level 信息存储到当前 $page 里,把 $page 压入 collection 通过 $params 返回;
  2. 调用 recurseSubpages($function, $params) 历遍 CMS pages,就得到了树状顺序、包含层次信息的 collection;
  3. 在 template 里 foreach $collection 的元素时,比对前一个元素和后一个元素和当前元素的 level
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent = $levelLatter,输出<li>当前元素</li>
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent < $levelLatter,输出<li>当前元素
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent > $levelLatter,输出<li>当前元素</li></ul></li>
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent = $levelLatter,输出<ul><li>当前元素</li>
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent < $levelLatter,输出<ul><li>当前元素
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent > $levelLatter,输出<ul><li>当前元素</li></ul></li>
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent = $levelLatter,输出<li>当前元素</li>
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent < $levelLatter,输出<li>当前元素
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent > $levelLatter,输出<li>当前元素</li></ul></li>

一一列出 template 的 9 种情况只为理清思路,实际代码并不需要分 9 种情况处理。不管怎样,我把历遍和表现相分离了,相比 Mage_Catalog_Block_Navigation 的代码更体现 MVC 的精髓。

讲了这么多,如果不摆完整的例子出来,估计谁也看不明白。那就请关注一下 Cpfp (Cms Page Foot Print)这个模块吧,在 Cpfp 模块中,我实现了 CMS page 的层次化,附送 CMS Page Sitemap,并修改了 CMS page breadcrumb 以充分体现页面的层次。有人用 category page 去构筑需要层次的内容,有人用 blog 模块去管理层次内容,有人干脆整合 wordpress 去管理内容。我的 Cpfp 只是用另一种方法去实现内容的层次,出发点是在内容表现方式上,CMS page 比 category 要强大,我在不修改 Magento 原有 cms_page 数据表的前提下实现层次化,是非常“绿色”、非常“轻量级”的模块。

Leave a Reply

Your email address will not be published. Required fields are marked *