Author: 芳草苑主

  • 1and1 cloud server datasheet

    从 1&1 新订了一个合同,cloud server,就是为了让 magento 跑快一些。那 1and1 的 cloud server 究竟能有多快呢?

    先看看 cat /proc/cpuinfo 的情况
    processor : 0
    vendor_id : AuthenticAMD
    cpu family : 16
    model : 2
    model name : Quad-Core AMD Opteron(tm) Processor 2352
    stepping : 3
    cpu MHz : 2109.718
    cache size : 512 KB
    fpu : yes
    fpu_exception : yes
    cpuid level : 4
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat clflush mmx fxsr sse sse2 syscall mmxext fxsr_opt lm 3dnowext 3dnow pni cx16 popcnt lahf_lm cr8_legacy altmovcr8 abm sse4a misalignsse
    bogomips : 4219.43
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 48 bits physical, 48 bits virtual
    power management:

    processor : 1
    vendor_id : AuthenticAMD
    cpu family : 16
    model : 2
    model name : Quad-Core AMD Opteron(tm) Processor 2352
    stepping : 3
    cpu MHz : 2109.718
    cache size : 512 KB
    fpu : yes
    fpu_exception : yes
    cpuid level : 4
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat clflush mmx fxsr sse sse2 syscall mmxext fxsr_opt lm 3dnowext 3dnow pni cx16 popcnt lahf_lm cr8_legacy altmovcr8 abm sse4a misalignsse
    bogomips : 4220.53
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 48 bits physical, 48 bits virtual
    power management:

    processor : 2
    vendor_id : AuthenticAMD
    cpu family : 16
    model : 2
    model name : Quad-Core AMD Opteron(tm) Processor 2352
    stepping : 3
    cpu MHz : 2109.718
    cache size : 512 KB
    fpu : yes
    fpu_exception : yes
    cpuid level : 4
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat clflush mmx fxsr sse sse2 syscall mmxext fxsr_opt lm 3dnowext 3dnow pni cx16 popcnt lahf_lm cr8_legacy altmovcr8 abm sse4a misalignsse
    bogomips : 4223.97
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 48 bits physical, 48 bits virtual
    power management:

    processor : 3
    vendor_id : AuthenticAMD
    cpu family : 16
    model : 2
    model name : Quad-Core AMD Opteron(tm) Processor 2352
    stepping : 3
    cpu MHz : 2109.718
    cache size : 512 KB
    fpu : yes
    fpu_exception : yes
    cpuid level : 4
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat clflush mmx fxsr sse sse2 syscall mmxext fxsr_opt lm 3dnowext 3dnow pni cx16 popcnt lahf_lm cr8_legacy altmovcr8 abm sse4a misalignsse
    bogomips : 4219.25
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 48 bits physical, 48 bits virtual
    power management:

    在 cloud server 上我使用 zend server。我特意从 1 Virtual processor core 1GB Ram 一步步往上加,使用 ab -c 5 -n 500 对比测试,客户端是 10 Mb down stream / 1Mb up stream adsl 连接。

    在 1 Virtual processor core 1GB Ram 时,Requests per second: 5.31 [#/sec]
    在 2 Virtual processor cores 1GB Ram 时,Requests per second: 8.34 [#/sec]
    在 2 Virtual processor cores 2GB Ram 时,Requests per second: 8.26 [#/sec]
    在 3 Virtual processor cores 1GB Ram 时,Requests per second: 10.57 [#/sec]
    在 4 Virtual processor cores 1GB Ram 时,Requests per second: 11.33 [#/sec]

    在 shopping cart 里有 7 条不同商品时,checkout/cart/index 页面时间为 10 秒左右。

    回头看看老 server
    # cat /proc/cpuinfo
    processor : 0
    vendor_id : AuthenticAMD
    cpu family : 15
    model : 67
    model name : Dual-Core AMD Opteron(tm) Processor 1216 HE
    stepping : 3
    cpu MHz : 1000.000
    cache size : 1024 KB
    physical id : 0
    siblings : 2
    core id : 0
    cpu cores : 2
    apicid : 0
    initial apicid : 0
    fpu : yes
    fpu_exception : yes
    cpuid level : 1
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow rep_good nopl pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy
    bogomips : 1999.96
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 40 bits physical, 48 bits virtual
    power management: ts fid vid ttp tm stc

    processor : 1
    vendor_id : AuthenticAMD
    cpu family : 15
    model : 67
    model name : Dual-Core AMD Opteron(tm) Processor 1216 HE
    stepping : 3
    cpu MHz : 1000.000
    cache size : 1024 KB
    physical id : 0
    siblings : 2
    core id : 1
    cpu cores : 2
    apicid : 1
    initial apicid : 1
    fpu : yes
    fpu_exception : yes
    cpuid level : 1
    wp : yes
    flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow rep_good nopl pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy
    bogomips : 1999.96
    TLB size : 1024 4K pages
    clflush size : 64
    cache_alignment : 64
    address sizes : 40 bits physical, 48 bits virtual
    power management: ts fid vid ttp tm stc

    运行的是 nginx,同等条件下,Requests per second: 8.12 [#/sec],checkout/cart/index 页面生成时间也是 10 秒左右。

    虽 zend server 和 nginx 不同,靠不是完全对等的对比结果,我还是可以得出两个结论:

    1. 瓶颈仍是 cpu 速度;
    2. cloud server 并未显著提高速度,我对它预期过高,略有失望。
  • How to detect current connection is secure using Php?

    Over a long time I had believed there was a function or an environment variable I could use in Php to tell whether the current connection secure or not, i.e. a page requested via http or https protocol. Unfortunately I could not find a good solution. I can not find it does not mean it does not exist.

    Recently I intend to believe the perfect solution does not exist, i.e. I can not write something in Php safely to tell whether the current connection is secure unless I know the server setting.

    Look at the code snippet Magento use for isSecure function:

    $secure = (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off')) || $_SERVER['SERVER_PORT']=='443';
    

    This code is not perfect because it assumes the server is not listening http on 443 AND server is listening HTTPS on 443.

    Unfortunately I can not do more than that. I am wondering whether the situation is the same in Python or Java.

  • Clean temporary files left by editors

    打开文件进行编辑或其他操作后,有时会留下一些临时文件。按理只有非正常中止会留下临时文件,但有时即使正常退出仍在硬盘上留下一堆垃圾,可能某些编辑器有 bug。

    不过,清理临时文件也不费力,找准文件名特征可以一次性删除它们。

    find /var/www -name ".goutputstream-*" -exec rm -rf {} \;

    据说 .goutputstream-* 是 GNOME 拷贝粘帖文件时的临时文件。

    find /var/www -name ".tmp*~" -exec rm -rf {} \;

    .tmp*~ 是 Text Editor 编辑时的临时文件。

    保持良好的习惯,不要用 root 身份去执行此类操作,执行前也要再三检查查找起始位置是否正确。

  • Why not use over-sized images in Magento?

    为什么不要在 Magento 下使用过高像素的图片?其实这个命题跟 Magento 没有多大关系——任何网站都不要使用超过实际需要尺寸的图片。

    只是 Magento 强大的图片处理机制容易让人产生惰性:如今买个相机动辄 10m pixels,拍完照一上传,只要 php.ini 允许足够大的值,Magento 也认可,但是,如果你真的这么做了,至少有这几点不良后果:

    • Can not send large images to Google Base (now called Google Merchant Centre). 虽然 Magento 能在自己的网站上缩放图片,但显然没有为 Google Base 考虑到。若为了让 Magento 缩放图片兼容去修改 GoogleBase module,在我看来有点头痛医脚的感觉。
    • Cost longer time to generate images in cache. 如果整个网站最大图片也只要 600x600px,那么何必为每次清空 image cache 后等上几秒钟来一张 10 兆像素的照片缩小成 600x600px?
    • Waste bandwidth to synchronise / backup media folder. 或许 unlimited bandwidth 不值得担心,但为数据同步/备份节省点时间不好吗?
    • My daily backup excludes some over-sized folders. 我有每日备份的机制,但这个机制无力顾及一些体积过大的文件或文件夹(通常也是不常更新的文件)。所以我会排除某些文件让备份文件保持小巧。

    作为最佳实践,图片处理不应该依赖于 online resizing。我想 Magento 在管理图片上,也可以更周到一些,适当时提示说,“照片太大,需要缩小保存吗(不保存原图)?”。

  • A tool to synchronise Magento database between servers

    我时不时需要在测试服务器上加载生产服务器的实时数据,以前都是把数据下载到本地的测试服务器后,手工键入一些命令完成数据加载,每次都要花费几分钟时间。为了避免一再“浪费”这几分钟,我今天一次性投入了几小时完成了一个 php 脚本。虽然这是为 magento 的数据迁移而写的脚本,但我写完一看,用在其他地方也是可以的。

    为了安全起见,该脚本是用 php 命令行运行的,所有输出针对 terminal 美化,不是 browser。保存源码为 data_mover.php,同一目录下要有 mysqldump 得到的经 gzip 的 sql 文件,文件名以 FILENAME_PREFIX 开头,以 .sql.gz 结尾。启动时只需键入

    /path/to/php -f data_mover.php

    即可。

    初始化 PDO 对象时,按理只需要 host=localhost,不需要 unix_socket=MYSQL_SOCKET。但奇怪的是,如果通过 apache 调用本程序(虽然不是本程序的初衷,但我希望它在浏览器下也能运行),仅指定 host=localhost 作 PDO __construct() 参数,会产生一个莫名其妙的错误:

    SQLSTATE[HY000] [2002] Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)

    似乎是 PDO bug。可以通过 host=127.0.0.1 或者追加 unix_socket=MYSQL_SOCKET 来避免。如果通过 php 命令行启动则没有这个问题。

    而后,还涉及怎么删除所有数据表的问题。看似简单的一个问题,我发现 mysql 竟然没有一个类似于 DROP/TRUNCATE TABLE * 单行命令。于是除了本脚本用的方法外,我还想了不下两种办法:

    其一,删掉整个数据库重新创建。
    mysqladmin -f -h localhost -u (DB_ROOT_USERNAME) -p(DB_ROOT_PASSWORD) drop (DB_NAME)
    mysqladmin -h localhost -u (DB_ROOT_USERNAME) -p(DB_ROOT_PASSWORD) create (DB_NAME)
    但这需要比数据表操作更高权限的用户,在这个无关大局的脚本里去使用高权限的用户的密码,实在非我所愿。

    其二,是我 google 来的,方法很巧,但很可惜,因为 foreign keys 的存在,运行这条命令会出错。
    mysqldump -u (DB_USERNAME) -p(DB_PASSWORD) –add-drop-table –no-data (DB_NAME) | grep ^DROP | mysql -u (DB_USERNAME) -p(DB_PASSWORD) (DB_NAME)

    
    <?php
    
    define('MYSQL_SOCKET', '/path/to/mysql/socket');
    define('DB_NAME', 'db_name');
    define('DB_USERNAME', 'db_username');
    define('DB_PASSWORD', 'db_password');
    define('FILENAME_PREFIX', 'filename_prefix');
    define('TEST_URL', 'http://test.domain/');
    
    if ($handle = opendir(dirname(__FILE__))) {
    $found = false;
    /* This is the correct way to loop over the directory. */
    while (false !== ($file = readdir($handle))) {
    if (substr($file, 0, 12) == FILENAME_PREFIX && substr($file, -7) == '.sql.gz') {
    if ($found) {
    //compare which one is newer
    if (filemtime($file) > filemtime($fileFound)) {
    $fileFound = $file;
    }
    }
    else {
    $found = true;
    $fileFound = $file;
    }
    }
    }
    
    if ($found) {
    echo "Found the newest file $fileFound, and will work on it.\n";
    try {
    $pdo = new PDO(
    'mysql:host=localhost;dbname=' . DB_NAME . ';unix_socket=' . MYSQL_SOCKET,
    DB_USERNAME,
    DB_PASSWORD,
    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")
    );
    echo "Connected to database.\n";
    }
    catch (Exception $e) {
    die ("Error occurred when connecting to database. Quoting error message: " . $e->getMessage() . "\n");
    }
    $sql = 'SET FOREIGN_KEY_CHECKS = 0';
    $pdo->prepare($sql)->execute();
    
    /* query all tables */
    $sql = "SHOW TABLES FROM ". DB_NAME;
    $stmt = $pdo->prepare($sql);
    $stmt->execute();
    $result = $stmtDale->fetchAll(PDO::FETCH_COLUMN, 0);
    $magentoTableNames = array();
    /* add table name to array */
    foreach ($result as $tableName) {
    if (substr($tableName, 0, 8) == 'magento_') {
    $magentoTableNames[] = $tableName;
    }
    }
    
    //drop all magento tables
    $count = count($magentoTableNames);
    if ($count > 0) {
    $sql = 'DROP TABLE '. implode(',', $magentoTableNames);
    $pdo->prepare($sql)->execute();
    echo "Found and dropped $count magento tables.\n";
    }
    else {
    echo "No existing magento tables found.\n";
    }
    
    //import data via pipe
    echo "Importing data. It may take a while...\n";
    $output = shell_exec("gunzip < $fileFound | mysql -h localhost -u " . DB_USERNAME . " -p" . DB_PASSWORD . " " . DB_NAME);
    echo "Importing data completed.\n";
    if ($output) {
    echo "This is output during data import:\n$output\n";
    }
    
    //after import, change some data for test domains
    $sql = 'UPDATE magento_core_config_data SET value=? WHERE scope=? AND scope_id=? AND path=?';
    $stmt = $pdo->prepare($sql);
    $stmt->execute(array(TEST_URL, 'default', 0, 'web/unsecure/base_url'));
    $stmt->execute(array(TEST_URL, 'default', 0, 'web/secure/base_url'));
    $stmt->execute(array(TEST_URL, 'websites', 2, 'web/unsecure/base_url'));
    $stmt->execute(array(TEST_URL, 'websites', 2, 'web/secure/base_url'));
    echo "Config data changed to fit testing environment.\n";
    echo "All missions completed.\n";
    }
    else {
    echo "File not found. What else can I do?\n";
    }
    closedir($handle);
    }
    else {
    echo "File access not permitted.\n";
    }
    
    
  • Magento Googlecheckout module ignore free shipping shopping cart price rule

    We charge a fixed shipping rate for subtotal under a certain amount, and offer free shipping to orders over this threshold.

    To achieve that, initially I created a shopping cart price rule in Magento promotions, and specified free shipping conditions and actions. Then I enabled Flatrate as a shipping method. This shipping method working in conjunction with the promotion rule served me well until I find it does not work for Google Checkout as a payment method.

    Free shipping promotion conditions
    Free shipping promotion conditions
    Free shipping promotion actions
    Free shipping promotion actions

    Magento Googlecheckout always charges that fixed shipping rate regardless of the promotion rules. I assume it is a bug. To avoid the bug, I delete the free shipping promotion rule and enable Tablerates to replace Flatrate. I can not fix this bug. The bug exists in Magento 1.4.0.1. Giving time it might be fixed by Magento core team.

  • How to override abstract class in Magento?

    If you are googling “override abstract class in magento” to find my blog, probably you already tried Mage::getModel(‘module/model_abstract’) as it worked for instantiatable classes.

    The scenario when requires override an abstract class is overriding isActive() method in Mage_Shipping_Model_Carrier_Abstract. If Mage_Shipping_Model_Carrier_Abstract can be overridden, all carrier methods’ isActive logic can be changed without changing each instantiatable class. However, whether Magento overriding mechanism can work has a prerequisite –

    All instantiation of parent class (in Mage namespace, to be overridden) are using Mage::getModel(‘module/model_class’), or $layout->getBlock(‘module/block_class’), or Mage::helper(‘module/helper_class’) instead of new Class_Name(). This guarantees Mage is the single entry point of class instantiation, then Mage Config can always instantiate an overridden class if any.

    The codes coming with Magento follow this rule to the maximum. But when it comes to class inheritance, the rule is broken. Say Mage_Class_A extends Mage_Abstract_B, Mage_Class_A makes reference to Mage_Abstract_B using normal php syntax. It means even if Mynamespace_Abstract_B is overriding Mage_Abstract_B, Mage_Class_A still inherit from Mage_Abstract_B.

    Come back to the question: How to override abstract class in Magento? Use Mage_Shipping_Model_Carrier_Abstract for example, to override isActive() method, you need to override all chid classes, e.g. Mage_Shipping_Model_Carrier_Flatrate, Mage_Shipping_Model_Carrier_Freeshipping, Mage_Shipping_Model_Carrier_Tablerate, etc.

    You may not satisfied with the answer simply because you do not want to override every carrier class. Let me raise the question again: Is there another way to override abstract class in Magento? Yes! Copy Mage_Shipping_Model_Carrier_Abstract from app/code/core/Mage/Shipping/Model/Carrier to app/code/local/Mage/Shipping/Model/Carrier, do NOT change the class name, just change or add the methods as you need. This is a trick. Magento loads a class from several locations, and app/code/local comes before app/code/core. That’s why it works.

    By the way, if php_apc is running, you must force apc to refresh for new class created in include_path. Please refer to “when does php apc refresh cache data” for details.

  • Magento meta keywords field name inconsistent

    I don’t know whether Magento did this on purpose. Both of category page and product page have a meta keywords attribute. Because meta keywords attribute of category or product belongs to EAV system, it has two entries in eav_attribute table, one for catalog_category (entity_type_id = 3) and one for catalog_product (entity_type_id = 4). However, if you look closely at attribute_code, you will find –

    • for category, the code is meta_keywords
    • for product, the code is meta_keyword

    The cms_page also has meta keywords, but it does not belongs to EAV system. The field name is meta_keywords.

    In summary, you won’t retrieve any meta keywords of a product if you write code like this:

    Mage::getModel('catalog/product')->load($productId)->getData('meta_keywords');
    
    
  • Magento code snippet

    Magento provide a beautiful GUI, which let you setup or change nearly everything. However, occasionally you prefer do it with php code. Here are a collection of codes for various functionalities. I will keep editing this post to add new methods.

    Read if a product attribute is “Use Default Value” in a Store View:

    Assume the storeview ID is $storeId, the product attribute code is $attributeCode;

    $result = Mage::getModel(‘catalog/product’)->setStoreId($storeId)->getAttributeDefaultValue($attributeCode);

    $result === false when the attribute IS “Use Default Value”;

    $result === Mage::getModel(‘catalog/product’)->setStoreId(0)->getAttributeDefaultValue($attributeCode) when the attribute HAS storeview specific value.

    Please note even if the attribute is “Yes/No” choice type, the attribute value is 1 or 0. So there is no conflict – when getAttributeDefaultValue returns false means “Use Default Value”; when it returns 0 means “default value is No”.

    Read storeview specific value to a product attribute:

    public function getAttributeOptionValue($optionId, $storeId = 0, $attributeId = null)
    {
    	$valuesCollection = Mage::getResourceModel('eav/entity_attribute_option_collection')
    		->setStoreFilter($storeId, false)
    		->addFieldToFilter('main_table.option_id', $optionId)
    		->setAttributeFilter($attributeId);
            return $valuesCollection->getFirstItem()->getValue();
    }
    

    CRUD manipulation on entity attributes, such as product attribute

    All need to do is construct an array in the right structure, add to the attribute, and save. Do not operate on ‘eav/entity_attribute_option’ directly. It won’t work because relationship between table eav_attribute_option and eav_attribute_option_value is not set in this model.

    Add options to attribute

    $attribute = Mage::getModel('eav/entity_attribute')->loadByCode('catalog_product', $attributeCode);
    $data = array(
    	'option' => array(
    		'value' => array(
    			$optionId => array(
    				$storeId => $value
    			)
    		)
    	)
    );
    $attribute->addData($data)->save();
    

    Bear in mind, as this is to add a new option, you do not need to specify an actual $optionId. It can be any string which, if convert to number, must be 0. If $optionId’s numeric value is positive, and if that option does not exist or does not belong to the $attribute, it will throw exception like:

    SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails.

    You can construct an array with more than one option ID’s and store ID’s inside to add multiple options or give option values to multiple stores at a time.

    Read options

    $attribute = Mage::getModel('eav/entity_attribute')->loadByCode('catalog_product', $attributeCode);
    $optionCollection = Mage::getResourceModel('eav/entity_attribute_option_collection')
    	->setStoreFilter($storeId, false)
    	->setAttributeFilter($attribute->getId());
    foreach ($optionCollection as $option) {
    	echo $option->getValue();
    }
    

    The code is straight forward. I did not find another way retrieving options. I tried
    $attribute->getOptions() and $attribute->getOption(). Neither worked.

    Delete options

    $attribute = Mage::getModel('eav/entity_attribute')->loadByCode('catalog_product', $attributeCode);
    $data = array(
    	'option' => array(
    		'value' => array(
    			$optionId => ''
    		),
    		'delete' => array(
    			$optionId => 1
    		)
    	)
    );
    $attribute->addData($data)->save();
    

    The tricky thing is: the $optionId to delete must exist in both ‘value’ and ‘delete’, although the value itself is not critical (because it will be deleted).

    Update options

    $attribute = Mage::getModel('eav/entity_attribute')->loadByCode('catalog_product', $attributeCode);
    $data = array(
    	'option' => array(
    		'value' => array(
    			$optionId => array(
    				$storeId => $value
    			)
    		)
    	)
    );
    $attribute->addData($data)->save();
    

    It is the same code as adding options. The difference is in $optionId. You need work out the existing option ID.

    CRUD manipulation does not need to construct $data with the complete list of existing options. Any options not in $data is untouched.

  • Magento extension: Root Category As Homepage

    It is my first time publish an Magento extension – Qian_Rcah. By default, Magento prevent root category from being shown at frontend, not to say use it as homepage. I do not see significant security leaks if showing root category. There are some benefits using root category as homepage:

    • Layered navigation starts with a complete product collection on site.
    • Selective products show on homepage without prerequisite knowledge of cms block.

    Download RootCategoryAsHomepage.tar.gz

    How to install:
    Copy the whole “app” folder to magento/app. That’s it.

    How to use:
    Log in Magento backend, go to System -> Configuration -> General -> Web -> Default Pages, change “Default web URL” to “rcah” (default is cms). Besides of root category, this module can load an arbitrary category page as homepage. In this case, change “Default web URL” to “rcah/index/index/id/ARBITRARY_CATEGORY_ID”.

    How is it created:
    This module is created by koukou1985’s module creator (http://www.magentocommerce.com/wiki/custom_module_with_custom_database_table). Thanks to koukou1985.

    How good is it:
    This module does not override any Mage classes. It is safe to use with other extensions as long as namespace and module name do not conflict.