Tag: magento

  • Magento promotional free shipping does not apply to tablerate

    Magento 自有的 shipping calculation 已足够强大,如果谁还说不够用,我就会说是经营上的问题。

    最近我被要求做这么一件事,在一个 storeview 下,对部分商品不论金额大小,一律 free shipping。一听之下我觉得可以用 cart salesrule free shipping action 来实现,实际尝试后却发现,cart salesrule free shipping 不能对 tablerate 起作用。而 tablerate 是 frontend 唯一一个 shipping method,被设置强制选择,顾客不会被问到 choose shipping method。在这个逻辑下,如果我启用 free shipping method,checkout 就多问一个问题,老板说这不好。而 flatrate 另有他用(backend 接单时对特殊地区额外征收运费),也无法拿它来实现部分商品 free shipping。

    这件事最后用一个普通 discount 抵消 shipping charge 的方式迂回解决,但解决得很别扭。我认为

    • shipping rates 是一种全局设置,就算能,也不应该为某些商品创建例外的;
    • 如果想为某些商品创建例外,那么再想一下为什么要创建例外,比如这些商品针对是一群特殊用户,如果 cross selling 对特殊用户不起作用,那就专门为他们建一个 store 或 website;
    • 新建 store 或 website 的成本与销售预期收益相比,是否值得?如果不值得,再反思一下是否值得为这些商品创建例外;
    • 自己写一个 shipping module 肯定可以实现任何稀奇古怪的 shipping rules,但写程序不是为了照顾 business 复杂化的需要。
  • Place Magento model, resource, collection class at your will

    Most Magento model, resource, collection are placed this way:

    • Model is located at Model folder
    • Resource is located at Model/Mysql4
    • Collection is located at Model/Mysql4/(ClassName)

    What if you want organise them in a different way? Say how place all of 3 classes under one folder?

    I enclose a complete a working sample.

    1. Model class

    
    class Qian_Msdk_Model_MediaGallery_Mg extends Mage_Core_Model_Abstract
    {
    public function _construct()
    {
    parent::_construct();
    $this->_init('msdk_mg/resource');
    }
    }
    
    

    2. Resource class

    
    class Qian_Msdk_Model_MediaGallery_Resource extends Mage_Core_Model_Mysql4_Abstract
    {
    public function _construct()
    {
    $this->_init('msdk_mg/product_attribute_media_gallery', 'value_id');
    }
    }
    
    

    3. Collection class

    
    class Qian_Msdk_Model_MediaGallery_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
    {
    public function _construct()
    {
    parent::_construct();
    $this->_init('msdk_mg/mg');
    }
    }
    
    

    4. config.xml

    
    <?xml version="1.0" encoding="UTF-8"?>
    <config>
    <global>
    <models>
    <msdk_mg>
    <class>Qian_Msdk_Model_MediaGallery</class>
    <resourceModel>msdk_mg_mysql4</resourceModel>
    </msdk_mg>
    <msdk_mg_mysql4>
    <class>Qian_Msdk_Model_MediaGallery</class>
    <entities>
    <product_attribute_media_gallery>
    <table>catalog_product_entity_media_gallery</table>
    </product_attribute_media_gallery>
    <product_attribute_media_gallery_value>
    <table>catalog_product_entity_media_gallery_value</table>
    </product_attribute_media_gallery_value>
    </entities>
    </msdk_mg_mysql4>
    </models>
    </global>
    </config>
    
    
  • How to read Magento product custom options correctly?

    Magento product custom options 的存储结构比较“绕”,我绕了好长时间才解开。

    假设 $product 有两个 custom options,一个是 textarea input type,另一个是 multiple select input type:

    先用 $product->getOptions(),得到是包含两个 custom options 的集合;
    然后 foreach 一下,得到每个 $option。对第一个 textarea 类型的,处理起来相对简单,用 $option->getData(‘title’) 查看 title,以此类推。对第二个 multiple select 类型的,用 $option->getData() 只能查看概况,对于 select 里的每个 option (对应 <select><option>…</option></select>),还得用 $option->getValues(),然后再次 foreach 才能得到 select option。

    够复杂吧。

  • 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
    }
    
    
  • 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 中也存在。

  • Open source for a purpose

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

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

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

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