Author: 芳草苑主

  • How to pager array items in Magento

    The Mage class Mage_Page_Block_Html_Pager is handy to pager collection items, but it is designed to work with a collection derived from Mage_Core_Model_Mysql4_Collection_Abstract.

    Now I run into a situation to pager arbitrary array items. The array items are pre-built and not loaded via a resource model, so the pager lost a clue how to control which items to display. (Which makes me think – Magento is powerful, but not so powerful without the help of mysql – Magento collection functionality rely heavily on mysql operation – it is a bit off topic.)

    So I need to tell the pager how to load the array after array items are loaded already. The following is my solution class.

    <?php
    class Qian_Msdk_Model_PagerableCollection extends Varien_Data_Collection {
    
    /***
    * load only current page items
    */
    public function load($printQuery = false, $logQuery = false) {
    if ($this->isLoaded()) {
    return $this;
    }
    $this->loadForPager();
    $this->_setIsLoaded();
    return $this;
    }
    
    public function loadForPager() {
    if (!$this->getPageSize()) {  //not pagerized
    return $this;
    }
    $items = array();
    $currentPage = $this->getCurPage();
    $i = 0;
    foreach ($this->_items as $item) {
    if ($i < ($currentPage-1) * $this->getPageSize()) {
    $i++;
    }
    elseif ($i >= $currentPage * $this->getPageSize()) {
    break;
    }
    else {
    $items[] = $item;
    $i++;
    }
    }
    $this->_items = $items;
    return $this;
    }
    
    /***
    * get the size of collection before pagerize
    */
    public function getSize()
    {
    if (is_null($this->_totalRecords)) {
    $this->_totalRecords = count($this->_items);
    }
    return intval($this->_totalRecords);
    }
    
    }
    
  • 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 数据表的前提下实现层次化,是非常“绿色”、非常“轻量级”的模块。

  • Magento extension project: Grouped DIY Product

    这是我用了 Magento 后站在需求分析的角度写得相对完整的文档,以前都直接 get hands dirty 了。

    What is the motive?

    We want to sell Christmas selections of products. Usually each unit product is of very low value, but the total value of selection is significant. If we pre-define the selection as a product, we can only create a limited number of selection products. Actually, we do not know what our customers want, so we leave the choice of selections to customers.

    Why not use Simple Product?

    As mentioned above, we do not think our pre-defined products will meet customers’ demand. Even we work hard to create hundreds of products, there are still millions of possible combinations left out.

    Why not use Grouped Product?

    Grouped Product provides a facility to add multiple products to cart in one go, but indeed products are added separately. Looking at the sales_flat_quote_item table after adding a Grouped Product to cart, I find each item does not have a parent item. Grouped Product is not fit for us because we have comprehensive promotion rules. Shopping cart takes more than 10 seconds to load when 10 items in cart. Customers’ shopping experience is even worse if they mix order Christmas selection with normal products.

    There are also some limitations of Grouped Product.

    1. It requires Simple Products before grouping. Our Christmas products are nothing new but dispersed pieces of normal products. I have spent hours and hours in eliminating duplicated information between related products. I think it is extremely bad to go back to the old route duplicating images, descriptions, specifications, prices, inventory quantity and location from normal products to Christmas Simple Products.
    2. The price of Grouped Product can not be set, which means we do not have a control on selling price as a whole. Although we can still use cart sales rule for Christmas selection, it even slows down everything.

    Why not use Configurable Product?

    Configurable Product offers configuration over shared attributes. It is difficult to offer configuration over product choice, and NOT possible to associate products of different attribute sets into one Configurable Product. Configurable Product also require price difference being set up, which is not synchronised when associated product price changes. Its typical usage is to sell a style of garment offering XL, L, M, S, XS configuration (no price difference).

    Why not use Bundle Product?

    Bundle Product offer add-ons. Each add-on product quantity is pre-defined. Customers can only say yes or no to add-ons, but can not change the quantities. Its typical usage is selling PC with all-in-one printer, anti-virus software, insurance.

    What we want to achieve?

    1. Define where unit product come from (from existing normal products) without creating another Simple Product. For example, we are selling 500 Santa-on-ice bags as a normal product. If we now extract 1 Santa-on-ice bag and add to Christmas selection, we just re-use all product information from the normal product.
    2. Define the selection freely and easily. Here is the list of fields for defining a product.
      • SKU of normal product
      • Piece Price (overwriting the price of normal product)
      • Piece Quantity (overwriting the Bunch Quantity of normal product)
      • Product name (We may want to describe it differently from the normal product name)
    3. Define an overall price (overwriting the total of Piece Price) to serve as lightweight promotion before adding to cart.
    4. If customers would like to know more about selection product information, they can click and go to normal product page.
    5. Allow drag and drop product thumbnails to give fun shopping experience. The progress of making choices is similar to a sub-shopping-cart remembering what have been selected before adding to cart.

    What we do not want to achieve?

    1. No stock control on unit products.
    2. No price relations between unit product and normal product.
    3. No GUI customised for creating Grouped DIY Product in admin end. In the time scale, I am focusing on improving shopping experience in customer end.

    Detailed approach

    I call it Grouped DIY Product as terminology must be very clear – it is not Magento’s native Grouped Product. The new product type is built on top of Simple Product because Simple Product works better in the admin end. However, how order is assembled is closest to Grouped Product. Grouped DIY Product lives in the best of both worlds.

  • Counter down for Magento CMS blocks

    我做了一个 Magento CMS static blocks 里用的倒计时 block。虽然功能比较简单,但还是比较智能的,所以还是想自我表彰一下。这不是独立的模块,我也没打算就拿这么单一的功能去生成一个模块,类似的功能我都会合并在一个 Msdk (Magento SDK) 模块里。

    
    <?php
    /***
    * Usage:
    *
    * Example 1:
    * {{block type="msdk/date_countdown" deadline="2010-10-31" before="Halloween in %d days!!" after="Happy Halloween!"}}
    *
    * Example 2:
    * {{block type="msdk/date_countdown" deadline="2010-10-31 19:35:00" before="Lucky draw in %i minute!!" after="" smart_plural="true"}}
    *
    * Obviously, "deadline" value is required. And it should be fed in ISO 8601 format.
    *
    * "before" and "after" are the message strings to be used before and after deadline respectively.
    * If any of them is not set, it uses "%h" as default.
    * If you want the message string be empty, you must set it to "", explicitly. (Not setting it will result in "%h")
    * You can use "%d" for days, "%m" for months, ... in the message strings. For a complete list of usable symbols, visit
    * http://www.php.net/manual/en/dateinterval.format.php
    *
    * "smart_plural" is a switch you can turn it on using smart_plural="true" (It is default to false).
    * When it is on, it looks for singular words in the message strings, and turn them into plurals when appropriate.
    * This feature is in experiment stage, so it may not be so smart.
    */
    class Qian_Msdk_Block_Date_Countdown extends Mage_Core_Block_Template {
    
    //unix timestamp
    protected $_deadline;
    
    protected $_before = '%h'; //default to hour diff
    
    protected $_after = '%h'; //default to hour diff
    
    protected $_smartPlural = false; //default to false;
    
    //DateInterval
    protected $_interval; //to store value used by different methods, avoid passing value
    
    protected function _toHtml() {
    $now = Mage::getModel('core/date')->date();
    $dateNow = date_create($now);
    $dateDeadline = date_create($this->_deadline);
    $interval = $dateNow->diff($dateDeadline);
    if ($interval->format('%R%s') > 0) {  //before deadline
    $display = $this->_before;
    }
    else { //after deadline
    $display = $this->_after;
    }
    
    $this->_interval = $interval;
    $display = $this->_pluralize($display);
    return $interval->format($display);
    }
    
    public function setDeadline($strDeadline) {
    $this->_deadline = Mage::getModel('core/date')->date($strDeadline);
    return $this;
    }
    
    public function setBefore($before) {
    $this->_before = $before;
    return $this;
    }
    
    public function setAfter($after) {
    $this->_after = $after;
    return $this;
    }
    
    public function setSmartPlural($smartPlural) {
    if ($smartPlural == 'true' || $smartPlural == 'yes' || $smartPlural == 1) {
    $this->_smartPlural = true;
    }
    else {
    $this->_smartPlural = false;
    }
    return $this;
    }
    
    protected function _pluralize($str) {
    if (!$this->_smartPlural) {  //smart plural not turned on, no further processing
    return $str;
    }
    $pattern = '/(.*)(%[YyMmDdaHhIiSs])(\s+)([a-zA-Z]+)([^a-zA-Z]|$)(.*)/Ui';
    return preg_replace_callback($pattern, "self::_pluralizeMatches", $str);
    }
    
    protected function _pluralizeMatches($matches) {
    return $matches[1] .
    Mage::helper('msdk/plural')->english($this->_interval->format($matches[2]), $matches[4]) .
    $matches[5] . $matches[6];
    }
    }
    
    
  • Use collection model without creating resource model in Magento

    问题的起因是为 Magento CMS pages 增加一个 html sitemap。我新建了一个类 Qian_Cpfp_Model_Page extends Mage_Cms_Model_Page,但我不想在 config.xml 让 Qian_Cpfp_Model_Page rewrite Mage_Cms_Model_Page。在 Qian_Cpfp_Model_Page 里,有一个方法 getSubpagesCollection(),返回某种规则下的子页集。

    我想递归调用 Qian_Cpfp_Model_Page::getSubpagesCollection() 得到子页树。调试时发现,getSubpagesCollection() 返回的是 collection of Mage_Cms_Model_Page instead of collection of Qian_Cpfp_Model_Page,而 Mage_Cms_Model_Page 里没有 getSubpagesCollection(),所以递归嵌套一层就进行不下去了。

    于是我又建了第2个类 Qian_Cpfp_Model_Mysql4_Page_Collection extends Mage_Cms_Model_Mysql4_Page_Collection。我想让 Qian_Cpfp_Model_Page::getSubpagesCollection() 返回 Qian_Cpfp_Model_Mysql4_Page_Collection, which is a collection of Qian_Cpfp_Model_Page。通常一个 model 和它的 collection 是通过一个 resource model 来沟通的,这样需要再建第3个类 Qian_Cpfp_Model_Mysql4_Page extends Mage_Cms_Model_Mysql4_Collection。但我实在不想再多建一个文件,因为:

    • Qian_Cpfp_Model_Mysql4_Page 即使建了也没有提供额外的功能。
    • 为了让 Qian_Cpfp_Model_Mysql4_Page 正常运转,config.xml 还得多几行关于 <resourceModel> 的定义及其 <entities> 的定义,而相同的定义已经在 Mage_Cms module 里已经做过了。
    • 我实在不喜欢用拷贝代码的方式去解决问题。

    到底能不能跳过 resource model 让 model 和 collection 互访呢?准确地说,我的问题是,能不能不建子类的 resource model 而让子类的 model 和 collection 互访?

    答案是能!具体实施步骤是:

    1. 在 Qian_Cpfp_Model_Page 以特殊方式指定 collection

    
    public function getResourceCollection() {
    return Mage::getModel('cpfp/mysql4_page_collection');
    }
    
    

    2. 在 Qian_Cpfp_Model_Mysql4_Page_Collection 以特殊方式指定 model 和 resouce model (通常方式不明示 resource model,根据 model 自行寻找,一找不存在就出错了)

    
    public function _construct()
    {
    parent::_construct();
    $this->_init('cpfp/page', 'cms/page'); //the second param is vital here
    }
    
    
  • Less is more

    想把人民币换成英镑。

    我知道好多人有外汇留成,所以我从一开始就想跟他们兑换以节省汇兑损失。可是事有不凑巧,几个人问过来,他们要么出差、要么外汇用光、要么嫌眼下美元价位太低,总之一时没换成。我正想再询问几个人,无意中有朋友告诉我,找银行换嘛。

    是啊,我怎么这么傻,怎么就没想到找银行,而且还更实惠!以今天的汇率为例,RMB100 究竟可以换到多少英镑?

    • 如果跟熟人以美元现汇买入价结算,USD100 = RMB663.86,我仍需要把美元兑换成英镑,GBP100 = USD158.62,那么 RMB100 最终可以换成 GBP9.4965 (100/6.6386/1.5862)。
    • 如果跟熟人以美元中间价结算,USD100 = RMB667.54,那么 RMB100 最终可以换成 GBP9.4442 (100/6.6754/1.5862)。
    • 如果跟银行购汇,GBP100 = RMB1050.83,那么 RMB100 最终可以换成 GBP9.5163。

    所以我觉得很傻,忙乎了半天,结果却是这么简单!亏我数学还不错,怎么就没一开始动手算一算?!

  • Be careful: Magento layer filter label is not html escaped

    我发现 Magento 1.4.1.1 app/design/frontend/base/default/template/catalog/layer/filter.phtml 是这么写的:

    <ol>
    <?php foreach ($this->getItems() as $_item): ?>
    <li>
    <?php if ($_item->getCount() > 0): ?>
    <a href="<?php echo $this->urlEscape($_item->getUrl()) ?>"><?php echo $_item->getLabel() ?></a>
    <?php else: echo $_item->getLabel() ?>
    <?php endif; ?>
    (<?php echo $_item->getCount() ?>)
    </li>
    <?php endforeach ?>
    </ol>
    

    我觉得不太好,因为 staff 在设置 filter label 通常会忽视 html escape,所以最好是

    $this->htmlEscape($_item->getLabel());
    

    但这样一来把 price filter 正常的 html 输出给搞乱了。要兼顾普通 filter 和 price filter,我还没有一个比较 neat 的方案,只好要求 staff 做好 filter label 的 html escape。

    同样的问题在 app/design/frontend/base/default/template/catalog/layer/state.phtml 中也存在。

  • Limit table column width in IE6

    I have a 2×2 table coded like this:

    <table>
    <tr>
    <th id="r1c1">
    Short heading
    </th>
    <th id="r1c2">
    Short heading
    </th>
    </tr>
    <tr>
    <td id="r2c1">
    Very very long content blah blah blah
    </td>
    <td id="r2c2">
    Short content
    </td>
    </tr>
    </table>
    

    In the td cell r2c1, if the content does wrap, it takes more width than r1c1, say r1c1 requires 100px width, and r2c1 requires 300px.

    But I want r2c1 content words wrap and its width limits to 100px. I put width style in the first row, i.e.:

    <th id="r1c1" style="width:100px">
    

    It does not work in IE6.

    Then I put width attribute on col, i.e.:

    <table>
    <col width="100"/>
    <col width="100"/>
    ...
    

    It does not work in IE6, either.

    Then I put width attribute in r2c1, i.e.:

    <th id="r2c1" width="100">
    

    It does not work in IE6, either.

    At last, I put width style in r2c1, i.e.:

    <th id="r2c1" style="width:100px">
    

    Finally it works in IE6.

    I knew td’s width attribute is deprecated but I didn’t know deprecated IE6 does not read deprecated attribute.

  • Brand name for a new wine

    突然想给一种未上市的酒取名叫小人醴,大家觉得怎么样?

  • Open source for a purpose

    我花了一年多时间写了一个 branded made to order 的程序。整个结构多次变化,经历了多次从繁到简,再到繁,再到简。直到今天,结构变得非常简单、却能应付不预知的复杂行业。

    刚开始写时,我也没想要写一个行业无关的程序,只想有一个能用在我所在的行业就可以了;我也没想结构可以重构得如此简单,如果跳过过程看结果的话,10 分钟(不夸张)就可以读懂结构,只有我自己知道这样的结构是凭无数次的灵感堆积起来的。

    言归正传,这个程序设计成 magento module,最初我也想开源发布的。但时至今日,我想敝帚自珍了。我本身非常崇尚开源,这是我第一个不想开源的程序,为什么我有这样的想法?我演绎了一下:

    • 开源项目都有使用门槛,人人会用的项目就不开源了;
    • 就如,谁都预测不了彩票,所以才有这么多的彩票预测公司。