标签归档:Magento

Magento系统加速 – MySQL读写分离

Magento使用EAV模型,所以对数据库来说,读操作往往比较大,可以使用读写分离技术有效解决这个问题。所谓读写分离,简单来说就是所有写操作,在A服务器上完成,所有写操作到B服务器上完成,A和B服务器实现主从复制。

A和B数据库服务器实现主从复制,MySQL提供了内置支持。可以把读压力水平分解(多个从库)。MySQL主从复制可以参考:http://www.vfeelit.com/353.html

Magento程序本身对读写分离提供了内置支持,可以为每个模块都指定读写适配器,如果没有指定就使用默认的读写适配器,而默认的读写适配器又是使用安装时的适配器。

比如模块名叫eav,那么可以在配置文件中指定eav_write和eav_read适配器,如果没有指定就使用系统默认的default_write和default_read,而default_write和default_read通过use节点指定它实际使用default_setup配置的适配器。

# app/etc/local.xml
    <global>
        <resources>
            <db>
                <table_prefix><![CDATA[]]></table_prefix>
            </db>
            <default_setup>
                <connection>
                    <host><![CDATA[192.168.1.10]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_setup>
        </resources>
    </global>

默认的读写适配器是放置在app/etc/config.xml中的:

            <default_write>
                <connection>
                    <use>default_setup</use>
                </connection>
            </default_write>
            <default_read>
                <connection>
                    <use>default_setup</use>
                </connection>
            </default_read>

默认它使用use节点指定使用default_setup适配器,所以我们修改这两个配置就可以实现读写分离:

            <default_write>
                <connection>
                    <host><![CDATA[192.168.1.10]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_write>
            <default_read>
                <connection>
                    <host><![CDATA[192.168.1.20]]></host>
                    <username><![CDATA[magento]]></username>
                    <password><![CDATA[root]]></password>
                    <dbname><![CDATA[magento]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_read>

读操作全部到达192.168.1.20服务器,写操作全部在192.168.1.10进行。注意,不要在local.xml中修改这个配置。虽然local.xml的配置最优先,但是默认配置有use节点,只要有use节点,就使用use节点的适配器,所以需要在config.xml中直接修改(不要出现use节点)。

每个模块都可以指定自己的读写适配器,如果没有指定就使用默认的配置。这个为读压力的水平分解提供了内置支持。比如某个模块的读操作非常频繁,可以读定向到单独到一个服务器中。通过指定读写适配器,还可以为模块指定使用另一个库等等。

注意,读写分离时,由于采用了MySQL的主从复制,复制的数据库库决不能进行插入更新这种操作(否则可能导致从库无法同步),实际上,程序可能无法百分百保证初始化一个读适配器后不进行更新操作,所以,为了防止这个情况,只给读适配器这个用户Select权限,就能从数据库级别杜绝这个情况发生。

Magento读写分离权限控制

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

Magento缓存配置与Zend_Cache_Backend_TwoLevels

如果有一个快的存储介质,一个慢的存储介质,可以同时保存缓存到这两个介质中,当快的存储介质使用率到达一定值(比如80%)的时候,把缓存保存到慢速介质,然后删除快速介质中的同版本缓存以释放快速存储介质空间,当获取缓存时首先到快的存储介质中获取,没有命中就到慢的介质中获取,当删除时快的慢的都一起删除。这种方式,当快的介质用满时,可以保存到慢速介质中,这样就不至于接下来的需要缓存的内容缓存失败获取淘汰最旧的缓存。

Zend Framework提供了这个实现(Zend_Cache_Backend_TwoLevels),Magento中当后端存储类型是memcached 和 apc 和 xcache 和 eaccelerator时,默认把后端类型调整为TwoLevels。

首先观察构造函数:

// Zend_Cache_Backend_TwoLevels
    public function __construct(array $options = array())
    {
        parent::__construct($options);

        if ($this->_options['slow_backend'] === null) {
        } else {
            $this->_slowBackend = Zend_Cache::_makeBackend(
                $this->_options['slow_backend'],
                $this->_options['slow_backend_options'],
                $this->_options['slow_backend_custom_naming'],
                $this->_options['slow_backend_autoload']
            );
        }

        if ($this->_options['fast_backend'] === null) {
        } else {
            $this->_fastBackend = Zend_Cache::_makeBackend(
                $this->_options['fast_backend'],
                $this->_options['fast_backend_options'],
                $this->_options['fast_backend_custom_naming'],
                $this->_options['fast_backend_autoload']
            );
        }

        $this->_slowBackend->setDirectives($this->_directives);
        $this->_fastBackend->setDirectives($this->_directives);
    }

这个构造函数删除了点内容。后端类都继承自Zend_Cache_Backend,它的构造函数就是把传递进来的$options循环放入到_options数组中。然后利用这些参数进行快慢两个后端进行初始化。从类提供的配置默认值:

    protected $_options = array(
        'slow_backend' => 'File',
        'fast_backend' => 'Apc',
        'slow_backend_options' => array(),
        'fast_backend_options' => array(),
        'slow_backend_custom_naming' => false,
        'fast_backend_custom_naming' => false,
        'slow_backend_autoload' => false,
        'fast_backend_autoload' => false,

        'stats_update_factor' => 10,
        'auto_refresh_fast_cache' => true
    );

前面的4对应该比较容易理解。auto_refresh_fast_cache发生在load函数中,当从快速介质中取回一个缓存时,实际上这个时候它的生存时间是需要修改的,这个设置就是用来刷新快速缓存(如果是做长久缓存的,就没有必要)。

stats_update_factor默认是10,10 表示10分之1的几率,这个情况是当已经获取了快速存储的利用率的情况下,比如50%,那么接下来每次获取利用率时,有10分之1的几率重新去获取,剩下的10分之9几率直接使用已经设置的值(这里的50%)

这个类的其它的方法其实是快慢后端二次封装,所以使用方法类似。

Magento中的缓存完美应用ZF提供的实现。Magento中使用了Mage_Core_Model_Cache对缓存提供了封装,这个对象首次初始化是在App的_initCache方法中:

    $options = $this->_config->getNode('global/cache');
    if ($options) {
        $options = $options->asArray();//转换为数组
    } else {
        $options = array();
    }
    $options = array_merge($options, $cacheInitOptions);
    $this->_cache = Mage::getModel('core/cache', $options);

这里的$options直接取global/cache的配置传入构造函数。

//Mage_Core_Model_Cache,构造函数中_getTwoLevelsBackendOptions将被调用
    protected function _getTwoLevelsBackendOptions($fastOptions, $cacheOptions)
    {
        $options = array();
        $options['fast_backend']                = $fastOptions['type'];
        $options['fast_backend_options']        = $fastOptions['options'];
        $options['fast_backend_custom_naming']  = true;
        $options['fast_backend_autoload']       = true;
        $options['slow_backend_custom_naming']  = true;
        $options['slow_backend_autoload']       = true;

        if (isset($cacheOptions['auto_refresh_fast_cache'])) {
            $options['auto_refresh_fast_cache'] = (bool)$cacheOptions['auto_refresh_fast_cache'];
        } else {
            $options['auto_refresh_fast_cache'] = false;
        }
        if (isset($cacheOptions['slow_backend'])) {
            $options['slow_backend'] = $cacheOptions['slow_backend'];
        } else {
            $options['slow_backend'] = $this->_defaultBackend;
        }
        if (isset($cacheOptions['slow_backend_options'])) {
            $options['slow_backend_options'] = $cacheOptions['slow_backend_options'];
        } else {
            $options['slow_backend_options'] = $this->_defaultBackendOptions;
        }
        if ($options['slow_backend'] == 'database') {
            $options['slow_backend'] = 'Varien_Cache_Backend_Database';
            $options['slow_backend_options'] = $this->getDbAdapterOptions();
            if (isset($cacheOptions['slow_backend_store_data'])) {
                $options['slow_backend_options']['store_data'] = (bool)$cacheOptions['slow_backend_store_data'];
            } else {
                $options['slow_backend_options']['store_data'] = false;
            }
        }

        $backend = array(
            'type'      => 'TwoLevels',
            'options'   => $options
        );
        return $backend;
    }
变量
//$fastOptions  = $backendOptions
$fastOptions[‘type’] = $backendType
$fastOptions[‘options’] = $options  //->  global/cache/backend_options

$cacheOptions ->  global/cache/

当然$fastOptions[‘type’]为memcached,检查PHP扩展存在memcached时,$fastOptions[‘options’]被global/cache/memcached配置覆盖,并且此时后端的类型是Libmemcached,PHP扩展不存在memcached而memcache存储时,$fastOptions[‘options’]被global/cache/memcached配置覆盖,并且此时后端的类型是Memcached。

这个说明,后端的类型是Memcached时,backend_options节点无效(对files,sqlite有效),当后端的类型是databases时(),backend_options节点也是无效的。

由此我们可以得出如下配置结构:

<global>
<cache>
	<id_prefix>vfeelit</id_prefix>

	<backend>file|database|sqlite|memcached|apc|xcache|eaccelerator</backend>
        <memcached>memcached类型时</memcached>

	<backend_options>
		<cache_db_complete_path>类型是sqlite时</cache_db_complete_path>
        </backend_options>

        <auto_refresh_fast_cache>默认为false</auto_refresh_fast_cache>
        <slow_backend>默认为file</slow_backend>
        <slow_backend_options></slow_backend_options>
        <slow_backend_store_data>是database时</slow_backend_store_data>
</cache>
</global>

慢速存储一般是file,所以backend_options后面的参数可以不用指定。如果不希望使用默认设置,比如保存在不同目录等,如下是所有可用配置设置(单独使用file作为后端时,很多参数无法自定义):

<global>
	<cache>
        <slow_backend>默认为file<slow_backend>
        <slow_backend_options>
	    <cache_dir>null</cache_dir>
            <file_locking>true</file_locking>
            <read_control>true</read_control>
            <read_control_type>'crc32'</read_control_type>
            <hashed_directory_level>0</hashed_directory_level>
            <hashed_directory_umask>0700</hashed_directory_umask>
            <file_name_prefix>zend_cache</file_name_prefix>
            <cache_file_umask>0600</cache_file_umask>
            <metadatas_array_max_size>100</metadatas_array_max_size>
        </slow_backend_options>
</cache>
</global>

如果backend是apc(xcache和eaccelerator),不需要设置任何参数,也没有需要设置的参数。如果backend是sqlite,backend_options中的cache_db_complete_path必须给出数据库文件。如果backend是memcached,针对memcached的配置直接作为cache的子节点(不要放入backend_options中),这个可以配置的值可以参考它的类的_options:

    protected $_options = array(
        'servers' => array(array(
            'host' => self::DEFAULT_HOST,
            'port' => self::DEFAULT_PORT,
            'persistent' => self::DEFAULT_PERSISTENT,
            'weight'  => self::DEFAULT_WEIGHT,
            'timeout' => self::DEFAULT_TIMEOUT,
            'retry_interval' => self::DEFAULT_RETRY_INTERVAL,
            'status' => self::DEFAULT_STATUS,
            'failure_callback' => self::DEFAULT_FAILURE_CALLBACK
        )),
        'compression' => false,
        'compatibility' => false,
    );

举例如下配置:

<global>
	<cache>
            <memcached>
                <servers>
                    <server>
                        <host><![CDATA[]]></host>
                        <port><![CDATA[]]></port>
                        <persistent><![CDATA[]]></persistent>
                        <weight><![CDATA[]]></weight>
                        <timeout><![CDATA[]]></timeout>
                        <retry_interval><![CDATA[]]></retry_interval>
                        <status><![CDATA[]]></status>
                    </server>
                </servers>
                <compression><![CDATA[0]]></compression>
                <cache_dir><![CDATA[]]></cache_dir>
            </memcached>
        </cache>
</global>

如果Servers中包含了多个Server,则需要不同的名字包围。

配置文件:

        <cache>
       		<backend>memcached</backend>
          	<id_prefix>vfeelit_</id_prefix>
		<!-- 
		<backend_options></backend_options>
		-->
          	<memcached>
           		<servers>
             		<server_one>
                 		<host>127.0.0.1</host>
                   		<port>11222</port>
                     	        <weight>1</weight>
               		</server_one>
                	<server_two>
               			<host>127.0.0.1</host>
                      	        <port>11221</port>
                      	        <weight>1</weight>
              		</server_two>
          		</servers>
         	</memcached>

      		<slow_backend_options>
       			<cache_dir>/www/default/magento.vfeelit.com/public_html/var/cache</cache_dir>
          		<file_locking>true</file_locking>
            	<read_control>true</read_control>
            	<read_control_type>crc32</read_control_type>
          		<hashed_directory_level>2</hashed_directory_level>
           		<hashed_directory_umask>0700</hashed_directory_umask>
            	<file_name_prefix>vfeelit</file_name_prefix>
            	<cache_file_umask>0600</cache_file_umask>
          		<metadatas_array_max_size>100</metadatas_array_max_size>
      		</slow_backend_options>
        </cache>

直接抓取cache节点内容转换为数组:

Array
(
    [backend] => memcached
    [id_prefix] => vfeelit_
    [memcached] => Array
        (
            [servers] => Array
                (
                    [server_one] => Array
                        (
                            [host] => 127.0.0.1
                            [port] => 11222
                            [weight] => 1
                        )

                    [server_two] => Array
                        (
                            [host] => 127.0.0.1
                            [port] => 11221
                            [weight] => 1
                        )

                )

        )

    [slow_backend_options] => Array
        (
            [cache_dir] => /www/default/magento.vfeelit.com/public_html/var/cache
            [file_locking] => true
            [read_control] => true
            [read_control_type] => crc32
            [hashed_directory_level] => 2
            [hashed_directory_umask] => 0700
            [file_name_prefix] => vfeelit
            [cache_file_umask] => 0600
            [metadatas_array_max_size] => 100
        )

)

最终获取如下结果中的options传入后端:

Array
(
    [type] => TwoLevels
    [options] => Array
        (
            [fast_backend] => Libmemcached
            [fast_backend_options] => Array
                (
                    [servers] => Array
                        (
                            [server_one] => Array
                                (
                                    [host] => 127.0.0.1
                                    [port] => 11222
                                    [weight] => 1
                                )

                            [server_two] => Array
                                (
                                    [host] => 127.0.0.1
                                    [port] => 11221
                                    [weight] => 1
                                )

                        )

                )

            [fast_backend_custom_naming] => 1
            [fast_backend_autoload] => 1
            [slow_backend_custom_naming] => 1
            [slow_backend_autoload] => 1
            [auto_refresh_fast_cache] => 
            [slow_backend] => File
            [slow_backend_options] => Array
                (
                    [cache_dir] => /www/default/magento.vfeelit.com/public_html/var/cache
                    [file_locking] => true
                    [read_control] => true
                    [read_control_type] => crc32
                    [hashed_directory_level] => 2
                    [hashed_directory_umask] => 0700
                    [file_name_prefix] => vfeelit
                    [cache_file_umask] => 0600
                    [metadatas_array_max_size] => 100
                )

        )

)

fast_backend_options缓存传递到Libmemcached,这个是实例化对象需要的具体参数。

另外,传递到前端的是数据:

Array
(
    [caching] => 1
    [lifetime] => 7200
    [automatic_cleaning_factor] => 0
    [cache_id_prefix] => vfeelit_
)

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

Magento会话共享改进(快慢两层存储)

如果使用了服务器集群,那么会话数据共享是必须实现的,因为请求进入的服务器可能不一样,但是它的会话ID是一样的,所以它的会话内容应该向各个应用服务器开放。用很多方法可以实现会话共享,比如可以在数据库存储服务器上划分一个区,然后各个应用服务器都mount这个区,会话都写入这里,理论上就能解决会话共享问题,或者直接保存到后端的数据库,每个应用都链接到后端数据库获取会话内容,这个实现也非常简单,如果数据库服务器压力大,可以进行主从复制读写分离。或者存入文件和存入数据库都不够快,那么可以存入到共享内存中,比如Memecached,不过存入共享内存有一个缺点,共享内存用完可能会产生问题,于是有了一个想法,能不能实现像缓存一样实现两层存储?

所谓两层,简单描述如下:
保存到Memcached中的同时也保存到数据库中,写入时监控共享内存利用率,如果超过伐值,从共享内存中删除这个会话,这时可能释放了共享内存,只保存到了数据库,而取数据时,先到共享内存取,如果没有命中则到数据库取。

经过研究,在Magento中只要重写Mage_Core_Model_Resource_Session类就能实现。

首先在自定义模块的配置文件中重写这个类:

<global>
        <models>
            <mysession>
                <class>Vfeelit_Mysession_Model</class>
            </mysession>
            <core_resource>
                <rewrite>
                     <session>Vfeelit_Mysession_Model_Resource_Session</session>
            	</rewrite>
            </core_resource>
        </models>
</global>

然后我们的Vfeelit_Mysession_Model_Resource_Session继承自Mage_Core_Model_Resource_Session。在构造函数中引入:

	public function __construct()
	{
		$mySession = Mage::getConfig()->getNode(self::XML_NODE_MYSESSION)->asArray();
		
		$prefix = $mySession['session_prefix'];
		if(!empty($prefix)){
			$this->_prefix = $prefix;
		}

		$options = $mySession['session_servers'];
		$this->_backend = new Zend_Cache_Backend_Libmemcached($options);

		parent::__construct();
	}

我这里实例化一个Zend_Cache_Backend_Libmemcached对象(PHP必须安装了Memcached扩展),用它来链接Memcached服务器,把数据保存到共享内存中。我这里还设置了一个key的前缀(因为我自己控制数据存入,所以key就可以自定义了),后面就是调用父类的构造函数,它负责数据库初始化。

要自定义会话的处理,需要使用session_set_save_handler()绑定6个方法,这个已经在Mage_Core_Model_Resource_Session中实现:

	public function setSaveHandler()
	{
		if ($this->hasConnection()) {
			session_set_save_handler(
				array($this, 'open'),
				array($this, 'close'),
				array($this, 'read'),
				array($this, 'write'),
				array($this, 'destroy'),
				array($this, 'gc')
			);
		} else {
			session_save_path(Mage::getBaseDir('session'));
		}
		return $this;
	}	

看起来,我们不需要动这个方法。我们需要重载的方法是read和write和destory(gc方法直接使用继承过来的即可)。

首先看read重载方法的实现:

	public function read($sessId)
	{
		if($this->_backend){
			$id = $this->_prefix.$sessId;
			$data = $this->_backend->load($id);
		}
		if(!$data){
			return parent::read($sessId);
		}else{
			return $data;
		}
	}

实际上非常简单,从共享内存中取数据,如果没有取到(可能是共享服务器down了或被存满了),就去数据库里取,从数据库取数据的逻辑Magento本身已经实现。

然后看write方法:

	public function write($sessId, $sessData)
	{
		if($this->_backend){
			$id = $this->_prefix.$sessId;
			$this->_save($sessData, $id, array(), $this->getLifeTime());
		}
		return parent::write($sessId, $sessData);
	}

这个方法同时保存到共享内存和数据库,这个是read方法无法读取到数据时可以从数据库里读取到的保证。这里我实现了一个_save方法抽象了保存的方法:

	private function _save($data, $id, $tags = array(), $specificLifetime = false)
	{
		$usage = $this->_getFastFillingPercentage('saving');
		$boolFast = true;
	
		if($specificLifetime){
			$lifetime = $specificLifetime;
		}else{
			$lifetime = $this->getLifetime();
		}
	
		//$preparedData = $this->_prepareData($data, $lifetime);

		if ( 85 >= $usage) {
			$boolFast = $this->_backend->save($data, $id, array(), time()+$lifetime);
		} else {
			$boolFast = $this->_backend->remove($id);
			if (!$boolFast && !$this->_backend->test($id)) {
				// some backends return false on remove() even if the key never existed. (and it won't if fast is full)
				// all we care about is that the key doesn't exist now
				$boolFast = true;
			}
	
		}
		return $boolFast;
	}

通过_getFastFillingPercentage方法获取共享内存的利用率,当利用率没有超过85,直接保存到共享内存中。如果超过了,就把这个KEY从数据库中踢掉,最后确保已经成功删除。这样,到达伐值后,就释放空间,内容从数据库获取,释放的空间和内存过期的会话释放的空间如果低于了伐值,新的会话数据库继续写入共享内存,从而实现了快慢存储。

Magento会话数据快慢存储

从测试来看,完成没有问题。另外一个需要主要的是,会话的共享内存空间最好不要和其它的缓存内存混用,因为会话内容共享内存一定要设置过期时间,方便垃圾数据及时退出,而缓存共享内存就不一定了,可以设置它保存很长时间,甚至直到手动刷新或重启Memcached服务时才被清理,所有缓存空间可能会被快速占用而不释放,那么会话内容可能无法存入,加速会话读取(或共享内存)的美梦就会泡汤。

这里的自己写的代码不多,但是要非常清楚的内容就非常多,比如会话机制的原理,共享内存Memcached工作原理,Magento的类重写,Magento对会话的封装过程等。

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

Magento Session与Session共享(Memcached)

前台控制器从Mage_Core_Controller_Front_Action继承,后台控制器从Mage_Adminhtml_Controller_Action继承,而它们的父类都是Mage_Core_Controller_Varien_Action,这里面的preDispatch方法是每个控制器在调用它的方法前都被执行的。里面有初始化会话的代码:

$session = Mage::getSingleton('core/session', array('name' => $this->_sessionNamespace))->start();

这里的对应所在的具体对象不同,可能为fontend和adminhtml。实际上Mage_Adminhtml_Controller_Action覆盖了preDispatch方法,它实现了自己的逻辑。
Magento会话类层次

搜索:“extends Mage_Core_Model_Session_Abstract”发现有22个匹配。

Mage_Core_Model_Session的构造函数:

    public function __construct($data=array())
    {
        $name = isset($data['name']) ? $data['name'] : null;
        $this->init('core', $name);
    }

把传递进来的$name传递到init函数初始化,这个函数从它的父类Mage_Core_Model_Session_Abstract继承过来:

//Mage_Core_Model_Session_Abstract
	// $namespace->core  $sessionName->frontend
    public function init($namespace, $sessionName=null)
    {
        parent::init($namespace, $sessionName);
        $this->addHost(true);
        return $this;
    }

而这个方法中又调用它的父类的Mage_Core_Model_Session_Abstract_Varien方法:

    public function init($namespace, $sessionName=null)
{
		//启动会话,$sessionName -> frontend
        if (!isset($_SESSION)) {
            $this->start($sessionName);
        }
		//
        if (!isset($_SESSION[$namespace])) {
            $_SESSION[$namespace] = array();
        }

        $this->_data = &$_SESSION[$namespace];

        $this->validate();
        $this->revalidateCookie();

        return $this;
    }

这个方法如果没有设置$_SESSION(默认Magento把自动启动会话关闭),就可以调用start方法开启会话。实际上,全局这里只调用这一次(区分前后台),因为:

    public function start($sessionName=null)
    {
        if (isset($_SESSION)) {
            return $this;
        }
		//…
	}

start方法中,只要会话一开始,就直接返回。不过为了确保会话正常开始,在实例化Mage_Core_Model_Session时还是调用它的start方法。

接下来start中的这段代码:

        switch($this->getSessionSaveMethod()) {
            case 'db':
                ini_set('session.save_handler', 'user');
                $sessionResource = Mage::getResourceSingleton('core/session');
                /* @var $sessionResource Mage_Core_Model_Mysql4_Session */
                $sessionResource->setSaveHandler();
                break;
            case 'memcache':
                ini_set('session.save_handler', 'memcache');
                session_save_path($this->getSessionSavePath());
                break;
            case 'memcached':
                ini_set('session.save_handler', 'memcached');
                session_save_path($this->getSessionSavePath());
                break;
            case 'eaccelerator':
                ini_set('session.save_handler', 'eaccelerator');
                break;
            default:
                session_module_name($this->getSessionSaveMethod());
                if (is_writable($this->getSessionSavePath())) {
                    session_save_path($this->getSessionSavePath());
                }
                break;
        }

决定了存储会话数据的介质。在Magento安装的时候可以选择的是文件和数据库。第一种情况存储到数据库,那么就会使用到会话的资源模型(因为和数据库交互),如果是保存到文件中,那么路径可写是必须检查的。

我们这里看到很多的Session类实现,实际上,只是说明模块需要保留数据到会话而已,每个类继承过来的init方法的第一个参数是一个所谓的名空间,实际上是$_SESSION数组的一个下标,比如core对应$_SESSION[‘core’]=array(), customer对应$_SESSION[‘customer’]=array()。

实际上这里的封装也不复杂。关于SESSION的内容可以参考http://blog.ifeeline.com/300.html。

这里探讨一下存储到共享内存的方法。处了保存到数据库和文件,Magento支持把SESSION数据通过memeche和memcached和eaccelerator保存到共享内存中。这三个都是PHP的扩展库。需要安装才能用。

在Magento的app/etc/local.xml中添加如此代码:

<global>
<!— 保存到文件 -->
<session_save><![CDATA[files]]></session_save>

		<!—通过memcached扩展把数据保存到memcached服务端 -->
        <session_save><![CDATA[memcached]]></session_save>
        <session_save_path><![CDATA[127.0.0.1:11212]]></session_save_path>
</global>

注意这里的memcached不是memcache(没有d)。

可以参考:http://www.php.net/manual/zh/memcached.sessions.php。这里给出了一个最佳实践,SESSION使用的缓存最好不要和其它的缓存共享。

针对memcached的配置,还有一个可以配置的参数是session_save_limiter,如果不理解这个最好不要设置,默认是no-cache,用来控制页面的缓存,参考http://www.php.net/manual/zh/function.session-cache-limiter.php。

这样我们在服务器上开启memcached服务:

/usr/local/bin/memcached -d -m 32 -u root -l 127.0.0.1 -p 11212

Magento共享内存数据查看

可见,数据已经保存到memcached共享内存中。在共享内存中保存SESSION有一个问题,当访问量比较大,可能用完共享空间,共享空间用完会产生什么情况呢,剔除最老的SESSION还是拒绝存入都会产生问题,另外如果多个应用都保存到了内存空间,Key的前缀都是memc.sess.key,后面的串看起来总不能保存唯一(应该有产生碰撞的可能)。这个问题的解决是使用快慢存储,比如在保存到Memcached中的同时也保存到数据库中,写入时监控共享内存利用率,如果超过伐值,从共享内存中删除这个会话,这时可能释放了共享内存,只保存到了数据库,而取数据时,先到共享内存取,如果没有命中则到数据库取,从而解决了以上的问题。

一般,为了有效共享内存,会话存在时间不应该设置过长甚至设置成永不过期(时间设置得很大),常见的时间应该是低于30分钟(1800秒,Magento默认是3600),SESSION的过期时间就是根据当前时间加上这个生存时间设置的,每次把会话内存读出来,都会自动写回去,这个主要是为了更新时间戳,写入共享内存的也是如此,共享内存的每块数据都有时间戳,超时就会自动从内存剔除。

以下看看生存时间的代码:

    public function getLifeTime()
    {
        if (is_null($this->_lifeTime)) {
            $configNode = Mage::app()->getStore()->isAdmin() ?
                    'admin/security/session_cookie_lifetime' : 'web/cookie/cookie_lifetime';
            $this->_lifeTime = (int) Mage::getStoreConfig($configNode);

            if ($this->_lifeTime < 60) {
                $this->_lifeTime = ini_get('session.gc_maxlifetime');
            }

            if ($this->_lifeTime < 60) {
                $this->_lifeTime = 3600; //one hour
            }

            if ($this->_lifeTime > self::SEESION_MAX_COOKIE_LIFETIME) {
                $this->_lifeTime = self::SEESION_MAX_COOKIE_LIFETIME; // 100 years
            }
        }
        return $this->_lifeTime;
    }

前台的从web/cookie/cookie_lifetime取出值,看起来是设置cookie的时间,实际上,保存会话ID的这个cookie生存时间,应该是和会话的生存时间一样长的。不过,在其它应用中,可能不这样设置,保存会话ID的cookie生存时间可能会设置为0,这个表示这个cookie在浏览器关闭后马上过期(就是浏览器关闭的同时删除这个cookie),可见,虽然服务器端的会话内容还没有过期,但是这个数据已经作废了。重新打开浏览器访问,服务器只能给它一个新的会话(因为你没有把SID传过来,所以不知道你之前状态),这样服务器端就会产生很多作废数据。如果保存会话ID的这个cookie跟会话生存时间一样长,就不会因为浏览器关闭重新打开时重新开启一个会话,不过用户把浏览器关闭了很大情况可能时不想再浏览了,另外关闭浏览器cookie不过期还有一个问题时,如果另一个人在你的电脑上用同一浏览器打开了相同的站点,你的信息可能被暴露,所以我们通常会被教育,要记得登出。

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

Magento缓存系统与Zend Framework缓存组件

Mage_Core_Model_Cache的构造函数有如下代码:

    $this->_frontend = Zend_Cache::factory('Varien_Cache_Core', $backend['type'], $frontend, $backend['options'],
        true, true, true
    );

这里生成一个缓存对象,前端是Varien_Cache_Core,后端是$backend[‘type’]。
Zend Framework缓存系统

首先,Magento中实现了自己的前端Varien_Cache_Core,Varien_Cache_Core直接继承自Zend_Cache_Core,跟Zend Framework中的中Zend_Cache_Frontend_File、Zend_Cache_Frontend_File_Output、Zend_Cache_Frontend_Page等是一个层次的,本身它们都不太复杂,实际上是设置前端配置参数,比如缓存生存时间等,另一个就是调用后端保存/取回/清除缓存。实际上更多是使用Zend_Cache_Core提供的封装方法(Magento中还使用了一个Mage_Core_Model_Cache来封装前端操作)。

Zend Framework中缓存的后端有很多类型,每个后端都需要是实现Zend_Cache_Backend_ExtendedInte接口(这个即可又从Zend_Cache_Backend_Interface继承)和继承自Zend_Cache_Backend,Zend Framework中提供的缓存后端有:

Zend_Cache_Backend_File
Zend_Cache_Backend_Sqlite
Zend_Cache_Backend_Memcached
Zend_Cache_Backend_Apc
Zend_Cache_Backend_Xcache
Zend_Cache_Backend_ZendPlatform
Zend_Cache_Backend_Libmemcached
Zend_Cache_Backend_TwoLevels //封装其它后端,实现慢快保存

我们可以看到,ZF并没有提供针对Database和Eaccelerat的后端类型,所以Magento添加了这两种。另外,使用Varien_Cache_Backend_Memcached扩展了Zend_Cache_Backend_Memcached(从Magento源代码看来Varien_Cache_Backend_Memcached并没有在Magento中使用)。

我们可以从Magento的Mage_Core_Model_Cache的_getBackendOptions方法中知道,它支持的后端类型有sqlite、memcached、apc、xcache、eaccelerator、database、file。

缓存对象一般指前端对象(也可能使用另外一个对象进行封装),前端对象必定需要一个后端为它服务。一般不直接实例化前端对象,而是使用Zend_Cache的factory方法:

    public static function factory($frontend, $backend, $frontendOptions = array(), $backendOptions = array(), $customFrontendNaming = false, $customBackendNaming = false, $autoload = false)
    {
        if (is_string($backend)) {
            $backendObject = self::_makeBackend($backend, $backendOptions, $customBackendNaming, $autoload);
        } else {
        }
        if (is_string($frontend)) {
            $frontendObject = self::_makeFrontend($frontend, $frontendOptions, $customFrontendNaming, $autoload);
        } else {
        }
        $frontendObject->setBackend($backendObject);
        return $frontendObject;
    }

$frontend和$backend一般都是使用字符串(也可以传递具体的对象,必须是符合类型的),$frontendOptions和$backendOptions对应前端和后端需要使用参数,$customFrontendNaming和$customBackendNaming表示是否使用自定义的命名,$autoload表示当使用自定义的命名时,是否自定装载类。

在初始化后端时,调用了_makeBackend,它的$frontend是一个字符串,不管怎么样,首先匹配标准的后端,如果无法匹配,就根据$customBackendNaming进行类名组装,如果是ture,直接就把$frontend作为了类名。所以,如果使用自定义的后端类,需要把它设置为true并且要把全类名传递进来, $autoload为true还会检查类名是否可读,最终把$backendOptions传递到构造函数中,返回这个后端实例。_makeFrontend函数跟这个过程一样。

由于后端基本都实现Zend_Cache_Backend_ExtendedInterface和Zend_Cache_Backend_Interface接口:

// Zend_Cache_Backend_Interface
public function setDirectives($directives);
public function load($id, $doNotTestCacheValidity = false);
public function test($id);
public function save($data, $id, $tags = array(), $specificLifetime = false);
public function remove($id);
public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array());

//Zend_Cache_Backend_ExtendedInterface
public function getIds();
public function getTags();
public function getIdsMatchingTags($tags = array());
public function getIdsNotMatchingTags($tags = array());
public function getIdsMatchingAnyTags($tags = array());
public function getFillingPercentage();
public function getMetadatas($id);
public function touch($id, $extraLifetime); //修改时间
    public function getCapabilities();

Zend_Cache_Backend_Interface提供了最基本的保存、取回、清除缓存的方法。Zend_Cache_Backend_ExtendedInterface提供了管理缓存的基本方法。

后端还会继承Zend_Cache_Backend,它提供了后端一般实现,注意,后端是直接实现以上接口的,所有后端需要直接实现接口方法。比如setOption()选项保存到_options属性中。看它的构造函数:

    public function __construct(array $options = array())
    {
        while (list($name, $value) = each($options)) {
            $this->setOption($name, $value);
        }
    }

以下以文件后端(Zend_Cache_Backend_File)为例,探讨它的内部细节。首先看构造函数:

    public function __construct(array $options = array())
    {
        parent::__construct($options); //设置_options
        if ($this->_options['cache_dir'] !== null) { // particular case for this option
            $this->setCacheDir($this->_options['cache_dir']);
        } else {
            $this->setCacheDir(self::getTmpDir() . DIRECTORY_SEPARATOR, false);
        }
        if (isset($this->_options['file_name_prefix'])) { // particular case for this option
            if (!preg_match('~^[a-zA-Z0-9_]+$~D', $this->_options['file_name_prefix'])) {
                Zend_Cache::throwException('Invalid file_name_prefix : must use only [a-zA-Z0-9_]');
            }
        }
        if ($this->_options['metadatas_array_max_size'] < 10) {
            Zend_Cache::throwException('Invalid metadatas_array_max_size, must be > 10');
        }
        if (isset($options['hashed_directory_umask']) && is_string($options['hashed_directory_umask'])) {
            // See #ZF-4422
            $this->_options['hashed_directory_umask'] = octdec($this->_options['hashed_directory_umask']);
        }
        if (isset($options['cache_file_umask']) && is_string($options['cache_file_umask'])) {
            // See #ZF-4422
            $this->_options['cache_file_umask'] = octdec($this->_options['cache_file_umask']);
        }
    }

这里主要对Options值进行纠正。这个类内部有一个默认值:

    protected $_options = array(
        'cache_dir' => null, //缓存目录
        'file_locking' => true, //文件锁
        'read_control' => true, //读控制
        'read_control_type' => 'crc32', //读控制类型
        'hashed_directory_level' => 0, //采用几层目录存储
        'hashed_directory_umask' => 0700, //目录umask
        'file_name_prefix' => 'zend_cache', //文件名前缀
        'cache_file_umask' => 0600, //缓存文件umask
        'metadatas_array_max_size' => 100 //元数据数组最大大小
    );

从构造函数中可知,如果如果没有传递cache_dir,那么它自动去寻找系统临时目录。下面跟踪save方法的实现:

    public function save($data, $id, $tags = array(), $specificLifetime = false)
    {
        clearstatcache();
        $file = $this->_file($id);
        $path = $this->_path($id);
        if ($this->_options['hashed_directory_level'] > 0) {
            if (!is_writable($path)) {
                // maybe, we just have to build the directory structure
                $this->_recursiveMkdirAndChmod($id);
            }
            if (!is_writable($path)) {
                return false;
            }
        }
        if ($this->_options['read_control']) {
            $hash = $this->_hash($data, $this->_options['read_control_type']);
        } else {
            $hash = '';
        }
        $metadatas = array(
            'hash' => $hash,
            'mtime' => time(),
            'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
            'tags' => $tags
        );
        $res = $this->_setMetadatas($id, $metadatas);
        if (!$res) {
            $this->_log('Zend_Cache_Backend_File::save() / error on saving metadata');
            return false;
        }
        $res = $this->_filePutContents($file, $data);
        return $res;
    }

首先获取将要缓存的文件的完整路径,然后是文件前缀。举例来说,文件名为Test_Cache,路径为/tmp/, file_name_prefix设置为zen_cache:

//hashed_directory_level为2
/tmp/zen_cache--*/zen_cache--**/
/tmp/zen_cache--*/zen_cache--**/zen_cache---Test_Cache

#如果hashed_directory_level为1
/tmp/zen_cache--*/
/tmp/zen_cache--*/zen_cache---Test_Cache

#如果没有设置hashed_directory_level(默认为0)
/tmp/zen_cache/
/tmp/zen_cache/zen_cache---Test_Cache

注意这里的*代表一个字符,是对文件名字进行哈希得来的。文件的存储路径和名字已经确定,接下来可能需要创建目录(如果是多层目录存放,这个时候hashed_directory_umask设置会直接影响创建目录的权限)。然后是判断是否有读控制(默认是true),如果true就使用read_control_type指定的类型获取数据哈希(就是数据签名,用来作为缓存的元数据),然后设置元数据:

        $metadatas = array(
            'hash' => $hash,
            'mtime' => time(),
            'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
            'tags' => $tags
        );
        $res = $this->_setMetadatas($id, $metadatas);

如果跟踪下去,它和保存缓存文件的过程非常类似(在进入_setMetadatas时,会对$metadatas数组是否大于metadatas_array_max_size进行判断,如果大于就进行裁切)。这里需要提一下的,缓存文件是创建时间 和 过期时间是放入元数据文件中的。

最后:

$res = $this->_filePutContents($file, $data);
return $res;

把数据保存到文件,返回是否成功的状态。

然后简单看下load()方法:

    public function load($id, $doNotTestCacheValidity = false)
{
		//测试是否命中,$doNotTestCacheValidity控制是否检查缓存有没有过期,如果为false,则检查,也是默认值,这样可以避免很多问题
        if (!($this->_test($id, $doNotTestCacheValidity))) {
            // The cache is not hit !
            return false;
        }
        $metadatas = $this->_getMetadatas($id);
        $file = $this->_file($id);
        $data = $this->_fileGetContents($file);
		//如果设置了读控制,则会校验数据hash,如果读出来的数据的hash和元数据hash不一样,说明这个缓存有问题,记录日志后将它删除
        if ($this->_options['read_control']) {
            $hashData = $this->_hash($data, $this->_options['read_control_type']);
            $hashControl = $metadatas['hash'];
            if ($hashData != $hashControl) {
                // Problem detected by the read control !
                $this->_log('Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match');
                $this->remove($id);
                return false;
            }
        }
        return $data;
    }

读写文件需要用到file_locking和cache_file_umask的配置。

接下来看看remove

    public function remove($id)
    {
        $file = $this->_file($id);
        $boolRemove   = $this->_remove($file);
        $boolMetadata = $this->_delMetadatas($id);
        return $boolMetadata && $boolRemove;
}

根据ID把缓存文件和对应的元数据删除。除了这个方法,还提供了更加通用的方法:

    public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
    {
        // We use this protected method to hide the recursive stuff
        clearstatcache();
        return $this->_clean($this->_options['cache_dir'], $mode, $tags);
    }

Zend_Cache提供了预定义的几个模式:

    const CLEANING_MODE_ALL              = 'all';
    const CLEANING_MODE_OLD              = 'old';
    const CLEANING_MODE_MATCHING_TAG     = 'matchingTag';
    const CLEANING_MODE_NOT_MATCHING_TAG = 'notMatchingTag';
    const CLEANING_MODE_MATCHING_ANY_TAG = 'matchingAnyTag';

clean方法根据不同模式,第二参数需要给定Tag。

还有其它一些有用方法,比如touch, getIds, getTags, getMetadatas等,实际也不复杂,另外要注意的时候,获取缓存只能是用load()给出ID,不能使用tag来load,道理是很明白的。

不过这里讨论的后端的有用方法,一般都不是直接操作的,后端是为前端服务的,所有一般是用前端对象的方法(间接调用后端的方法),由于有很多不同类型的后端,所有引入前端的概念可以提供一个通用的方案。

在Magento中,对应了一个Mage_Core_Model_Cache的对象,它用来封装cache对象(它需要和数据库交互),App的_cache引用到这个对象,这个cache的_frontend属性引用的才是前端对象(Varien_Cache_Core),Mage_Core_Model_Cache的save方法对$tags数组中只要没有包含CONFIG这个tag,就统一在最后添加一个叫MAGE的tag。

Magento中的Mage_Core_Model_Cache对象的方法:

    protected function _getFrontendOptions(array $cacheOptions)
{
        $options = isset($cacheOptions['frontend_options']) ? $cacheOptions['frontend_options'] : array();
        if (!array_key_exists('caching', $options)) {
            $options['caching'] = true;
        }
        if (!array_key_exists('lifetime', $options)) {
            $options['lifetime'] = isset($cacheOptions['lifetime']) ? $cacheOptions['lifetime']
                : self::DEFAULT_LIFETIME;
        }
        if (!array_key_exists('automatic_cleaning_factor', $options)) {
            $options['automatic_cleaning_factor'] = 0;
        }
        $options['cache_id_prefix'] = $this->_idPrefix;
        return $options;
    }

这里把_idPrefix赋值给$options[‘cache_id_prefix’],这个参数会被传入前端(已经写死了),覆盖前端对象的cache_id_prefix值,这里的_idPrefix如果配置文件中没有指定id_prefix,就使用prefix,如果还没有指定就是 ../app/etc目录MD5后取前三个字符加下滑线。

在Zend_Cache_Core中在保存 和 取出 和 清除缓存时,都会把id经过_id()方法过滤

    protected function _id($id)
    {
        if (($id !== null) && isset($this->_options['cache_id_prefix'])) {
            return $this->_options['cache_id_prefix'] . $id; // return with prefix
        }
        return $id; // no prefix, just return the $id passed
    }

可以看到前端的_options的cache_id_prefix不为null,那么还会在id名前加上这个前缀。

我们可以从Zend_Cache_Core中获取可配置的前端参数:

    protected $_options = array(
        'write_control'             => true,
        'caching'                   => true,
        'cache_id_prefix'           => null,
        'automatic_serialization'   => false,
        'automatic_cleaning_factor' => 10,
        'lifetime'                  => 3600,
        'logging'                   => false,
        'logger'                    => null,
        'ignore_user_abort'         => false
    );

Mage_Core_Model_Cache在_getFrontendOptions方法根据global/cache/ frontend_options的配置,设置caching,lifetime,automatic_cleaning_factor,这些如果没有配置就是它的默认值,所以基本是写死了。注意cache_id_prefix是真写死。从而知道它的配置模式:

<global>
	<cache>
	<frontend_option>
	<caching />
	<lifetime />
<automatic_cleaning_factor />
</frontend_option>
</cache>
</global>

所以,你看到magento中的缓存文件:

mage---841_TEST_CACHE

就不需要奇怪了,TEST_CACHE是ID, 841_则是MD5前三个字符加下划线。mage是缓存前缀,这个只有后端是File时才有的:

//Mage_Core_Model_Cache
    protected $_defaultBackendOptions = array(
        'hashed_directory_level'    => 1,
        'hashed_directory_umask'    => 0777,
        'file_name_prefix'          => 'mage',
);

	//进入default时$backendType 没有设置, 这里就是指后端是File时,看到把$_defaultBackendOptions传递入后端类型,那么file_name_prefix就被设置了,另外,默认hashed_directory_level是0,这里改为了1
        if (!$backendType) {
            $backendType = $this->_defaultBackend;
            foreach ($this->_defaultBackendOptions as $option => $value) {
                if (!array_key_exists($option, $options)) {
                    $options[$option] = $value;
                }
            }
        }

hashed_directory_level是一层,file_name_prefix默认为mage,那么就产生这样的文件:

mage—1/mage---841_TEST_CACHE

你可以看到,以上那段代码是写死了的。意思是说,对于后端是File类型的,前缀必须是mage,层次只能是1,你可以把这里的1改成2,试手一下。

File的类型,很多参数都写死了。可用的参数:

    protected $_options = array(
        'cache_dir' => null, //写死为 /../var/cache
        'file_locking' => true,
        'read_control' => true,
        'read_control_type' => 'crc32',
        'hashed_directory_level' => 0, //写死为1
        'hashed_directory_umask' => 0700, //写死为0511
        'file_name_prefix' => 'zend_cache', //写死为mage
        'cache_file_umask' => 0600,
        'metadatas_array_max_size' => 100
    );

可以通过配置来改变File类型的后端参数不多:

<global>
	<cache>
<backend>File</backend>
<id_prefix></id_prefix>
			<backend_options>
				<cache_file_umask>0700</cache_file_umask>
</backend_options>
</cache>
</global>

不过你可以指定id_prefix,如果没有指定id_prefix就使用prefix作为id_prefix,否则就系统自动生成。prefix是id_prefix的候补(id_prefix更加明确,应该使用这个设置缓存名字的前缀),除此,没有其它作用。(在源码中,prefix没有传递到后端模型)

backend如果是File就可以省略。如果是其它类型就需要指定,比如APC,由于它是共享内存,一个名字对应一份缓存,所以这时候指定id_frefix非常有必要(如果是文件,存储路径会由于每个应用不同而不同),比如有一个叫tCache保存到了APC中,如果其它应用程序也设置了一个叫tCache的放入内存中,这样新的将覆盖旧的,那么你可能面临莫名其妙的错误。

另外,APC不支持tag。也没有backend_options需要设置。简单说就是backend指定为APC,id_prefix指定为缓存名前缀,Magento中的配置就完成了。

能够搞明白文件保存缓存的过程,对应比如apc等就很容易明白了。尽管ZF提供的实例化和管理缓存的方法已经很好了,但是Magento本身还是提供了一个自己的缓存抽象,它封装了自己的实现,实际上它是利用ZF缓存机制的非常成功的例子。不过,Magento最通用使用缓存的方法还不是直接操作它的Cache方法,通用的方法由Mage_Core_Model_App中提供:

public function getCacheInstance()
public function getCache()
public function loadCache($id)
public function saveCache($data, $id, $tags=array(), $lifeTime=false)
public function removeCache($id)
public function cleanCache($tags=array())

可以调用getCacheInstance获取Mage_Core_Model_Cache获取实例引用,然后通过它直接操作(它有save,load,remove, clear方法)。不过在Magento中只要记住通用方法即可。

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

Magento缓存与全局配置文件缓存

使用如下例子:
1 先关闭缓存
然后在任何一个控制器中添加一个方法加入如下代码:

$xml = Mage::getConfig()->getNode()->asXml();
file_put_contents('D:/config_file.xml', $xml);

在我这里,产生的文件大小为684K。这是一个非常让我吃惊的数字。如果每个请求都重复这个过程,如果100个同时请求,将吃掉68400K=68.4M内存,注意,这只是针对全局配置, 还没有包含布局系统等。

2 开启缓存
多次刷新刚才那个方法,发现产生的文件只有220K。

问题:为何会如此?比对先后两次产生的文件代码:
Magento全局配置文件结构

从缓存中获取的文件,admin adminhtml install stores crontab websites节点不见了。于是就产生了一个很大的困惑,如果开启了缓存,那么如何获取某个店铺的配置(因为缓存取回的配置没有店铺的设置)?

下面我们运行如下代码:

$xml = Mage::getConfig()->getNode('stores');
file_put_contents('D:/config_store.xml',$xml->asXml());

发现,它输出:

<store>
	<default></defatul>
	<admin></admin>
</store>

刚才消失的store节点这里获取出来了。

看起来,我们必须搞明白缓存对象干了什么事情。

首先进入App的run方法,它首先运行baseInit(),它里面有:

        $cacheInitOptions = is_array($options) && array_key_exists('cache', $options) ? $options['cache'] : array(); //空
        $this->_initCache($cacheInitOptions);

注意,这里的$cacheInitOptions是空的。然后调用:

    protected function _initCache(array $cacheInitOptions = array())
    {
        $this->_isCacheLocked = true;
        $options = $this->_config->getNode('global/cache');
        if ($options) {
            $options = $options->asArray();
        } else {
            $options = array();
        }
        $options = array_merge($options, $cacheInitOptions);
        $this->_cache = Mage::getModel('core/cache', $options);
        $this->_isCacheLocked = false;
        return $this;
    }

可以看到,这里初始化了一个core/cache对象。另外,我们可以知道,它会从global/cache中获取配置信息,关于缓存配置的都是在这里设置。

接下看看Mage_Core_Model_Cache的构造函数:

    public function __construct(array $options = array())
    {
    	    // ../var/cache
        $this->_defaultBackendOptions['cache_dir'] = Mage::getBaseDir('cache');
        /**
         * Initialize id prefix
         */
        // id_prefix
        $this->_idPrefix = isset($options['id_prefix']) ? $options['id_prefix'] : '';
        // id_prefix没有指定,但是设置了prefix,那么 id_prefix = prefix
        if (!$this->_idPrefix && isset($options['prefix'])) {
            $this->_idPrefix = $options['prefix'];
        }
        // 如果_idPrefix还为空,MD5 ../etc目录,然后取前3个字符,然后再加下划线作为_idPrefix
        if (empty($this->_idPrefix)) {
            $this->_idPrefix = substr(md5(Mage::getConfig()->getOptions()->getEtcDir()), 0, 3).'_';
        }

        $backend    = $this->_getBackendOptions($options);
        $frontend   = $this->_getFrontendOptions($options);

        $this->_frontend = Zend_Cache::factory('Varien_Cache_Core', $backend['type'], $frontend, $backend['options'],
            true, true, true
        );

        if (isset($options['request_processors'])) {
            $this->_requestProcessors = $options['request_processors'];
        }

        if (isset($options['disallow_save'])) {
            $this->_disallowSave = $options['disallow_save'];
        }
    }

最终使用Zend_Cache生成缓存对象。$backend得到的是一个包含两个字段的数组,一个是type,表示存储类型,另一个是options,对应这个存储类型的选项。具体需要继续跟踪进入:

   protected function _getBackendOptions(array $cacheOptions)
    {
        $enable2levels = false;
        // 存储类型,默认为 File
        $type   = isset($cacheOptions['backend']) ? $cacheOptions['backend'] : $this->_defaultBackend;
        // 检查是否提供了backend_options
        if (isset($cacheOptions['backend_options']) && is_array($cacheOptions['backend_options'])) {
            $options = $cacheOptions['backend_options'];
        } else {
            $options = array();
        }
	// 根据$type,获取$backendType,如果配置中没有给出backend_options,那么$options就是空的,但是根据不同的类型,还是可能产生其它$option
        $backendType = false;
        switch (strtolower($type)) {
            case 'apc':
                if (extension_loaded('apc') && ini_get('apc.enabled')) {
                    $enable2levels = true;
                    $backendType = 'Apc';
                }
                break;
            case 'database':
                $backendType = 'Varien_Cache_Backend_Database';
                $options = $this->getDbAdapterOptions();
                break;
            default:
                if ($type != $this->_defaultBackend) {
                }
        }

        $backendOptions = array('type' => $backendType, 'options' => $options);
        if ($enable2levels) {
            $backendOptions = $this->_getTwoLevelsBackendOptions($backendOptions, $cacheOptions);
        }
        return $backendOptions;
    }

$options的中的值根据不同存储类型会不同(还受到配置中是否给出backend_options配置的影响),可以推导出配置结构:

<global>
	<cache>
		<backend></backend>
		<backend_options>
                        <memcached>只有backend为memcached时</memcached>	
                </backend_options>

                <auto_refresh_fast_cache><auto_refresh_fast_cache>
                <slow_backend><slow_backend>
                <slow_backend_options><slow_backend_options>
                <slow_backend_store_data><slow_backend_store_data>
                <cache_db_complete_path>只有backend为sqlite时</ cache_db_complete_path/>
                <slow_backend_store_data>只有backend为database时</slow_backend_store_data>

                <frontend_options>
	            <caching>不设置默认为true</caching>
                    <lifetime>不设置默认是7200</lifetime>
                    <automatic_cleaning_factor>不设置默认为0</automatic_cleaning_factor>
                </frontend_options>
        </cache>
</global>

如果类型是memcached时,backend_options应该需要提供memcached节点。

注意_getBackendOptions函数返回$backendOptions前,还经过了_getTwoLevelsBackendOptions方法的处理(除了类型是File和Database时),所以最终返回的数据:

$options['fast_backend'] //存储类型,根据backend节点转换而来
$options['fast_backend_options'] //只有backend节点为database和memcached时有内容
$options['fast_backend_custom_naming']  = true;
$options['fast_backend_autoload']       = true;
$options['slow_backend_custom_naming']  = true;
$options['slow_backend_autoload']       = true;

$options['auto_refresh_fast_cache']  //来自auto_refresh_fast_cache节点,否则为false
$options['slow_backend'] //来自slow_backend节点,否则为File
$options['slow_backend_options'] // 来自slow_backend_options节点,否则为默认
$options['slow_backend_options']['store_data']//来自slow_backend_store_data,只有backend是database时,不过看起来它不可进入

回到缓存对象的构造函数,接下来执行_getFrontendOptions函数,它去寻找frontend_options节点,设置是否缓存 和 缓存时间,参考以上给出的配置文件。

接下执行:

$this->_frontend = Zend_Cache::factory('Varien_Cache_Core', $backend['type'], $frontend, $backend['options'],
            true, true, true
        );

实际利用Zend_Cache生成了一个缓存对象(Varien_Cache_Core继承自Zend_Cache_Core),它保存到缓存对象的_frontend中(****)。

通过App的_initCache,它的_cache字段都保持了一份Mage_Core_Model_Cache对象的引用(注意它内部的__frontend,它应该才是核心)。

现在把目光转移回到App的run方法中,看如下代码:

        if ($this->_cache->processRequest()) {
            $this->getResponse()->sendResponse();
        } else {}

这里调用缓存对象的processRequest,如果顺利,可以直接响应。那么去看看processRequest方法:

    public function processRequest()
    {
        if (empty($this->_requestProcessors)) {
            return false;
        }

        $content = false;
        foreach ($this->_requestProcessors as $processor) {
            $processor = $this->_getProcessor($processor);
            if ($processor) {
                $content = $processor->extractContent($content);
            }
        }

        if ($content) {
            Mage::app()->getResponse()->appendBody($content);
            return true;
        }
        return false;
    }

马上就有疑问,_requestProcessors如何被设置的,好吧,在构造函数中,刚才少说了如下代码:

// Mage_Core_Model_Cache
        if (isset($options['request_processors'])) {
            $this->_requestProcessors = $options['request_processors'];
        }

request_processors是在配置文件的cache中指定的:

	<global>
		<cache>
	<request_processors></request_processors>
</cache>
</global>

如果指定了,那么就使用它处理响应。这个处理器至少有extractContent方法,看起来应该是和全页缓存相关。我试图去搜索extractContent,没有找到这个方法,结论是,Magento CE还没有提供全页缓存,这个也是这种插件热卖的原因了。

如果进入System->Configuration->ADVANCED->System,你将看到:
Magento后台设置全页缓存
这个配置和我这里说的全页缓存不是一个概念。这里的zend_page_cache是Zend Server提供的一个扩展,具体怎么搞就没有研究了。
Zend_Page_Cahce配置
这个探讨先到这里。我们继续回到App的_initModules()方法中:

    protected function _initModules()
    {
        if (!$this->_config->loadModulesCache()) {
            $this->_config->loadModules();
 
            $this->_config->loadDb();
            $this->_config->saveCache();
        }
        return $this;
    }

这里的saveCache()方法是首次开始缓存。既然如此,那么继续:

    public function saveCache($tags=array())
    {
        if (!Mage::app()->useCache('config')) {
            return $this;
        }
	//为缓存打标签
        if (!in_array(self::CACHE_TAG, $tags)) {
            $tags[] = self::CACHE_TAG;
        }
        $cacheLockId = $this->_getCacheLockId();
	//这是上锁保护 如果缓存正在进行,那么就被取消
        if ($this->_loadCache($cacheLockId)) {
            return $this;
        }
	// … 省略
    }

这里首先使用App的useCache方法检查是否启用了缓存:

//Mage_Core_Model_App
    public function useCache($type=null)
    {
        return $this->_cache->canUse($type);
}
//Mage_Core_Model_Cache
    public function canUse($typeCode)
{
	// _allowedCacheOptions为空
        if (is_null($this->_allowedCacheOptions)) {
            $this->_initOptions();
        }

        if (empty($typeCode)) {
            return $this->_allowedCacheOptions;
        } else {
            if (isset($this->_allowedCacheOptions[$typeCode])) {
                return (bool)$this->_allowedCacheOptions[$typeCode];
            } else {
                return false;
            }
        }
}

    protected function _initOptions()
{
	// OPTIONS_CACHE_ID->core_cache_options 这里首先从缓存中获取这个值
        $options = $this->load(self::OPTIONS_CACHE_ID);
        if ($options === false) { //没有获取值
	    //从数据库获取值
            $options = $this->_getResource()->getAllOptions();
            if (is_array($options)) {
                $this->_allowedCacheOptions = $options;
	        //把值缓存起来
                $this->save(serialize($this->_allowedCacheOptions), self::OPTIONS_CACHE_ID);
            } else {
                $this->_allowedCacheOptions = array();
            }
        } else {
	    //取到了值
            $this->_allowedCacheOptions = unserialize($options);
        }

        if (Mage::getConfig()->getOptions()->getData('global_ban_use_cache')) {
            foreach ($this->_allowedCacheOptions as $key => $val) {
                $this->_allowedCacheOptions[$key] = false;
            }
        }

        return $this;
    }

这里先开个小差,哪些东西可以设置缓存,后台可以指定:
Magento缓存管理

这些设置保存到表core_cache_option中:
Magento缓存配置表
实际canUse就是返回对应的value值,1表示使用缓存,0表示不使用。另外,还有core_cache和core_cache_tag这两个表和core_cache_option没有任何干系,它们是使用数据库缓存时保存缓存数据的地方。

回到saveCache方法:

//Mage_Core_Model_Config
    public function saveCache($tags=array())
    {
        if (!Mage::app()->useCache('config')) {
            return $this;
        }
	//为缓存打标签  保存的时候可以打标签
        if (!in_array(self::CACHE_TAG, $tags)) {
            $tags[] = self::CACHE_TAG;
        }
	// Config对象在构造函数中$this->setCacheId('config_global');设置了ID
	// 这里获取config_global.lock,如果能load进来,直接就返回了,说明在上锁
        $cacheLockId = $this->_getCacheLockId();
        if ($this->_loadCache($cacheLockId)) {
            return $this;
        }
/*
$_cacheSections = array(
        'admin'     => 0,
        'adminhtml' => 0,
        'crontab'   => 0,
        'install'   => 0,
        'stores'    => 1,
        'websites'  => 0
    );
*/
        if (!empty($this->_cacheSections)) {
            $xml = clone $this->_xml;
	    //根据_cacheSections数组把全局配置文件拆分
            foreach ($this->_cacheSections as $sectionName => $level) {
                $this->_saveSectionCache($this->getCacheId(), $sectionName, $xml, $level, $tags);
                unset($xml->$sectionName);
            }
	    // config_global 是被剥离后剩余部分
            $this->_cachePartsForSave[$this->getCacheId()] = $xml->asNiceXml('', false);
        } else {
            return parent::saveCache($tags);
        }

        $this->_saveCache(time(), $cacheLockId, array(), 60); //上锁
        //移除打了CONFIG标签的cache(全部配置相关的缓存,全部打了这个标志,默认)
        $this->removeCache();
        foreach ($this->_cachePartsForSave as $cacheId => $cacheData) {
            $this->_saveCache($cacheData, $cacheId, $tags, $this->getCacheLifetime());
/*
    protected function _saveCache($data, $id, $tags=array(), $lifetime=false)
    {
        return Mage::app()->saveCache($data, $id, $tags, $lifetime);
    }
*/
        }
        unset($this->_cachePartsForSave);
        $this->_removeCache($cacheLockId); //解锁
        return $this;
}

    protected function _saveSectionCache($idPrefix, $sectionName, $source, $recursionLevel=0, $tags=array())
    {
        if ($source && $source->$sectionName) {
			// config_global_admin config_global_websites
            $cacheId = $idPrefix . '_' . $sectionName;
            if ($recursionLevel > 0) {
                foreach ($source->$sectionName->children() as $subSectionName => $node) {
                    $this->_saveSectionCache(
                        $cacheId, $subSectionName, $source->$sectionName, $recursionLevel-1, $tags
                    );
                }
            }
		    //部分保存的XML
            $this->_cachePartsForSave[$cacheId] = $source->$sectionName->asNiceXml('', false);
        }
        return $this;
    }

这部分代码总体上实现了把全局对象拆分成几段缓存:

config_global
config_global_admin
config_global_adminhtml
config_global_install
config_global_websites
config_global_stores_default
config_global_stores_admin
config_global_stores_german
config_global_stores_french

config_global是被拆了admin等之后剩余的XML。这些缓存全部打上了CONFIG这个Tag,Config中的removeCache方法实际就是移除所有打上了这个Tag的缓存。

生成缓存就这样够一段了。下面如何加载缓存。还是从App的run方法进入,它之中运行的baseInit主要是初始化环境(_initEnvironment),初始化基础配置(app/etc/config.xml和local.xml,由_initBaseConfig调用Config的loadBase完成,说明这两个文件的内容尽管已经缓存,但是每次都会被读取)和生成缓存对象,接下来的_initModules方法:

    protected function _initModules()
    {
        if (!$this->_config->loadModulesCache()) {
        }
        return $this;
}

实际调用Config的loadModulesCache方法加载缓存,如果能加载就使用加载的缓存文件:

//Mage_Core_Model_Config
    public function loadModulesCache()
    {
        if (Mage::isInstalled(array('etc_dir' => $this->getOptions()->getEtcDir()))) {
            if ($this->_canUseCacheForInit()) {
                $loaded = $this->loadCache();
                
                if ($loaded) {
                    $this->_useCache = true;
                    return true;
                }
            }
        }
        return false;
    }

这里重点是loadCache方法,这个负责加载缓存,如果成功加载使用_useCache标识在使用缓存:

// lib/Varien/Simplexml/Config
    public function loadCache()
    {
        if (!$this->validateCacheChecksum()) {
            return false;
        }

        $xmlString = $this->_loadCache($this->getCacheId());
        $xml = simplexml_load_string($xmlString, $this->_elementClass);
        if ($xml) {
            $this->_xml = $xml;
            $this->setCacheSaved(true);
            return true;
        }

        return false;
    }

getCacheId()获取config_global,它会把这个缓存加载进来。所以从缓存拿回来的_xml只是一部分,这才解释了本文刚开始遇到的疑问。可是新疑问又来了,既然是部分配置,那么如何获取店铺的配置呢?

首先是获取配置的用法:

$config->getNode('admin');
$config->getNode('stores');
$config->getNode('stores/default');

进入getNode()方法就可以看到,它根据第一个字段,相应的从缓存中取出缓存的内容。注意,其它获取配置的包装方法,都是间接使用getNode()方法,比如Mage::getStoreConfig()方法,实际调用Mage_Core_Model_Store的getConfig方法,而这个方法内部就是调用getNode:

    public function getConfig($path)
    {
        $fullPath = 'stores/' . $this->getCode() . '/' . $path;
        $data = $config->getNode($fullPath);
    }

最后总结一下Magento中使用cache的方法:

//缓存一块数据
saveCache($data, $id, $tags=array(), $lifeTime=false)
//根据ID加载缓存
loadCache($id)
//根据ID删除缓存
removeCache($id)
//根据TAGS清楚缓存
cleanCache($tags=array())

来一段测试程序:

		$data = "12345678901234567890000000";
		$id = "Test_Cache";
		$tags = array('xTest','xCache');
		
		$fromCache = Mage::app()->loadCache($id);
		if($fromCache){
			echo "From cache---><br />";
			echo $fromCache;
			Mage::app()->removeCache($id);
		}else{
			echo "No cache ---><br />";
			Mage::app()->saveCache($data, $id, $tags);
		}

Magento缓存文件

Magento缓存文件内容

这种缓存的功能是ZF提供的。对于全局配置文件,如果不缓存,每次都读取合并很多文件,这个过程将产生大量的IO,如果缓存了,将可以减少大部分的IO操作和减少计算资源,但是还是要把缓存读入内存,如果能够把这些缓存放入共享内存中,理论上应该可以提升性能(减少了从磁盘调人内存这个IO操作,所以把缓存放入磁盘和放入共享内存,性能提升不明显的原因就在这里),如缓存已经在内存中,不用每次都调用文件写入内存,但是获取缓存转换成PHP对象仍然占有比较多内存,所以Magento是很非常耗内存,并发一多,就会很明显。

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

Magento的全局变量设计模式

PHP中,变量有全局的概念,比如$_GET $_POST等数组中保存的值都是全局的,如果直接使用一个变量,那么它也是全局可用的,如果在函数中使用global关键字声明一个变量,那这个函数就可以使用来自外部的这个变量值,函数中的这个变量值还可以从函数带出到外部,所有直接声明的变量或用global关键字声明的变量,实际上全部注册到$GLOBALS数组中。比如声明了$var,当要引用这个变量时可以直接用$var或$GLOBALS[‘var’]。这种搞法其实已经非常怪异了,不过还有更加怪异的,比如$_GET[‘name’],你可能可以使用$name直接引用或用$GLOBALS[‘name’]来引用这个变量(具体决定于PHP的配置,不过一般都是关闭的了)。 这些诡异的用法是PHP的历史原因导致的。不过作为一个PHP程序员,仍然需要知道这个。

这里的全局变量的使用有很大风险,比如我声明了一个$var,然后在其它地方再次声明一个同名的$var,那么后面的将悄悄覆盖前面的。更加糟糕的是,如果这个变量引用一个对象,但是由于一些第三方代码的引入,也声明了一个同名的变量,那么这个变量在其它地方调用方法时将出现错误,这个问题很令人烦恼。所以我们得到的教训是:慎用全局变量。

以上说得情况在面向过程的代码中中,尤为明显。由于这个问题,全局变量设计模式就产生了。以下主要探讨Magento中的全局设计模式和单态设计模式。

Magento中使用如下方法实现了全局设计模式:

Mage::register
Mage::unregister   
Mage::registry 

分别对应注册 和 注销 和 获取全局变量。首先看看注册一个全局变量时发生了什么事情:

    public static function register($key, $value, $graceful = false)
    {
        if (isset(self::$_registry[$key])) {
            if ($graceful) {
                return;
            }
            self::throwException('Mage registry key "'.$key.'" already exists');
        }
        self::$_registry[$key] = $value;
    }

首先检查$key是否存在$_registry数组中,如果存在就根据$graceful的设置,返回空或者抛出异常。如果不存在就直接赋值。可见,一个变量注册了,就不能再注册一个同名的变量了,这个很好解决了变量被覆盖的威胁。但是还是有不足,因为程序员可以先调用unregister来注销变量,然后设置自己的变量:

    public static function unregister($key)
    {
        if (isset(self::$_registry[$key])) {
            if (is_object(self::$_registry[$key]) && (method_exists(self::$_registry[$key], '__destruct'))) {
                self::$_registry[$key]->__destruct();
            }
            unset(self::$_registry[$key]);
        }
    }

注销全局变量时如果全局变量是对象并且设置了__destruct析构函数,那么直接调用它的析构函数释放资源。否则就是简单unset,这个就依赖PHP的垃圾回收器来回收了。

当想要获取这个全局变量时,直接调用registry,可以把它看做是注册表:

    public static function registry($key)
    {
        if (isset(self::$_registry[$key])) {
            return self::$_registry[$key];
        }
        return null;
    }

Magento内核高度使用这个模式,我们知道Magento中的控制器并不会直接和视图交互,真正和视图交互的是Block,控制器中只要使用renderLayout就可以获取输出:

$this->loadLayout();
$this->getLayout()->getBlock('content')->setSomeVar($var);
$this->renderLayout();

我们可以在控制器中添加:

Mage::register('product', $product);

一个Block中可能包含如下方法:

public function getProduct()
{
    return Mage::registry('product');
}

这样就可以在视图中使用:

echo $this->getProduct()->getName();


Magento中的单态设计模式(Singleton Pattern)

获取一个对象的一份单态实例:

Mage::getSingleton('group/class');

过多解释在面对代码时非常苍白,所以我们这里直接看看这个方法的实现:

#File: app/Mage.php
public static function getSingleton($modelClass='', array $arguments=array())
{
    $registryKey = '_singleton/'.$modelClass;
    if (!self::registry($registryKey)) {
        self::register($registryKey, self::getModel($modelClass, $arguments));
    }
    return self::registry($registryKey);
}

它使用’_singleton/’.$modelClass组建名字,然后调用register把getModel()返回的实例记录到注册表中,你可以看到,如果这个名称对应的值存在了,直接就放回这个实例,就是这样保证了全局唯一(看起来不是严格的单态模式,因为可以unregister)。这个封装另一个模式来实现的别的设计模式,也算是一个亮点吧。不过一般单态设计模式是控制类的构造函数来保证的,常见的使用方法是:

$bar = Object_Type::getInstance();

可是Magento中并没有采用这种方式。尽管可以unregister,但是它的灵活性也体现出来了。

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

Magento中基准目录

http://blog.ifeeline.com/411.html中探讨了全局XML文件如何构建,其中涉及到了目录的获取。这里继续这个内容。
Mage_Core_Model_Config类的构造函数会实例化一个Mage_Core_Model_Config_Options类,这个类设置了Magento的基准目录。不过需要记住,Mage_Core_Model_Config是一个最新被实例化的对象之一(在Mage::app()中,实例化Mage_Core_Model_App后),所有Mage_Core_Model_Config对象的Option对象很早就进入场景了(因为它在Config类构造函数中实例化)。Option提供的getDir方法其实是其它方法的包装器:

    public function getDir($type)
    {
        $method = 'get'.ucwords($type).'Dir';
        $dir = $this->$method();
        if (!$dir) {
            throw Mage::exception('Mage_Core', 'Invalid dir type requested: '.$type);
        }
        return $dir;
    }

具体实现就不陈述了。

如果要获取这个Option对象可以这样做:

Mage::getConfig()->getOptions();

那么要获取某个目录自然是:

Mage::getConfig()->getOptions()->getDir("var"); //获取var目录

这种做法在任何代码中都是可行的。不过Mage.php中提供了一个包装器,然你可以忘记Config对象 和 Option对象:

    public static function getBaseDir($type = 'base')
    {
        return self::getConfig()->getOptions()->getDir($type);
    }

以下是一览表:

//以网站根目录为/www/vfeelit/public_html为例
Mage::getBaseDir('base'); # /www/vfeelit/public_html
Mage::getBaseDir('app'); # /www/vfeelit/public_html/app
Mage::getBaseDir('code'); # /www/vfeelit/public_html/app/code
Mage::getBaseDir('design'); # /www/vfeelit/public_html/app/design
Mage::getBaseDir('etc'); # /www/vfeelit/public_html/app/etc
Mage::getBaseDir('lib'); # /www/vfeelit/public_html/lib
Mage::getBaseDir('locale'); # /www/vfeelit/public_html/app/locale
Mage::getBaseDir('media'); # /www/vfeelit/public_html/media
Mage::getBaseDir('skin'); # /www/vfeelit/public_html/skin
Mage::getBaseDir('var'); # /www/vfeelit/public_html/var
Mage::getBaseDir('tmp'); # /www/vfeelit/public_html/var/tmp
Mage::getBaseDir('cache'); # /www/vfeelit/public_html/var/cache
Mage::getBaseDir('log'); # /www/vfeelit/public_html/var/log
Mage::getBaseDir('session'); # /www/vfeelit/public_html/var/session
Mage::getBaseDir('upload'); # /www/vfeelit/public_html/media/upload
Mage::getBaseDir('export'); # /www/vfeelit/public_html/var/export

接下来看看模块的基础目录,对应的包装器是Mage类中的getModuleDir方法:

    public static function getModuleDir($type, $moduleName)
    {
        return self::getConfig()->getModuleDir($type, $moduleName);
    }

可见它是Config对象的getModuleDir的包装器:

    public function getModuleDir($type, $moduleName)
    {
        $codePool = (string)$this->getModuleConfig($moduleName)->codePool;
        $dir = $this->getOptions()->getCodeDir().DS.$codePool.DS.uc_words($moduleName, DS);

        switch ($type) {
            case 'etc':
                $dir .= DS.'etc';
                break;

            case 'controllers':
                $dir .= DS.'controllers';
                break;

            case 'sql':
                $dir .= DS.'sql';
                break;
            case 'data':
                $dir .= DS.'data';
                break;

            case 'locale':
                $dir .= DS.'locale';
                break;
        }

        $dir = str_replace('/', DS, $dir);
        return $dir;
    }

代码很简单,不需要做很多解释。一下是一览表:

Mage::getModuleDir('','Packagename_Modulename'); //不要忽略这个用法,它直接返回模块的基路径
Mage::getModuleDir('etc','Packagename_Modulename');
Mage::getModuleDir('controllers','Packagename_Modulename');
Mage::getModuleDir('sql','Packagename_Modulename');
Mage::getModuleDir('data','Packagename_Modulename');
Mage::getModuleDir('locale','Packagename_Modulename');

getBaseDir 和 getModuleDir务必记住,就和getModel() getHelper()一样必须牢记。

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

Magento中的Compiler

Compiler就是编译器的意思,编译给我的概念是把一种高级语言代码转换为一种更低级的语言代码的过程,基于这个理解,想当然地认为Magento中的Compiler也是这个意思。不过它的Compiler和我的理解相差太多了。

首先可以参考:http://blog.ifeeline.com/406.html获取类自动装载的概念。

在index.php文件中有如下代码:

$compilerConfig = MAGENTO_ROOT . '/includes/config.php';
if (file_exists($compilerConfig)) {
    include $compilerConfig;
}

这段代码常被忽略,因为includes/config.php文件只有两行被注释的代码:

#define('COMPILER_INCLUDE_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR.'src');
#define('COMPILER_COLLECT_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR.'stat');

随着app/Mage.php被包含进来,会执行如下代码:

Mage::register('original_include_path', get_include_path());

if (defined('COMPILER_INCLUDE_PATH')) {
    $appPath = COMPILER_INCLUDE_PATH; // incdlues/src
    set_include_path($appPath . PS . Mage::registry('original_include_path'));
    include_once COMPILER_INCLUDE_PATH . DS . "Mage_Core_functions.php";
    include_once COMPILER_INCLUDE_PATH . DS . "Varien_Autoload.php";
} else {
}

Varien_Autoload::register();

可以看到COMPILER_INCLUDE_PATH如果已经定义就使用上面这段代码,它就是往PHP的include_path中添加…/incdlues/src路径。之后把Varien_Autoload.php加载进来后(注意这时候是从incdlues/src中取),调用了register方法:

class Varien_Autoload
{
    const SCOPE_FILE_PREFIX = '__';
 
    static protected $_instance;
    static protected $_scope = 'default';
 
    protected $_isIncludePathDefined= null;
    protected $_collectClasses      = false;
    protected $_collectPath         = null;
    protected $_arrLoadedClasses    = array();
 
    /**
     * Class constructor
     */
    public function __construct()
    {
        $this->_isIncludePathDefined = defined('COMPILER_INCLUDE_PATH');
        if (defined('COMPILER_COLLECT_PATH')) {
            $this->_collectClasses  = true;
            $this->_collectPath     = COMPILER_COLLECT_PATH;
        }
        self::registerScope(self::$_scope);
    }
    /**
     * Singleton pattern implementation
     *
     * @return Varien_Autoload
     */
    static public function instance()
    {
        if (!self::$_instance) {
            self::$_instance = new Varien_Autoload();
        }
        return self::$_instance;
    }
    /**
     * Register SPL autoload function
     */
    static public function register()
    {
        spl_autoload_register(array(self::instance(), 'autoload'));
    }
 
    /**
     * Load class source code
     *
     * @param string $class
     */
    public function autoload($class)
    {
        if ($this->_collectClasses) {
            $this->_arrLoadedClasses[self::$_scope][] = $class;
        }
        if ($this->_isIncludePathDefined) {
            $classFile =  COMPILER_INCLUDE_PATH . DIRECTORY_SEPARATOR . $class;
        } else {
            $classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $class)));
        }
        $classFile.= '.php';
        //echo $classFile;die();
        return include $classFile;
    }
}

这个类的方法都非常简单,register方法实际把Varien_Autoload对象的autoload方法作为自动加载类的方法。注意看构造函数,COMPILER_INCLUDE_PATH设置与否将直接影响到类的装载。进入autoload方法,$this->_collectClasses只是用一个数组记录加载的类(这里看起来并没有什么作用),接下来的$this->_isIncludePathDefined就非常明显了,如果这个值设置了则直接使用include/src构建路径,跟下面代码不一样的是构建的路径对应了一层层的目录(比如Zend_XmlRpc_Server -> Zend/XmlRpc/Server),然后include文件。

这就意味着,如果启用了COMPILER_INCLUDE_PATH,所有类文件必须拷贝到include/src文件夹中,并且需要使用完整类名命名文件(比如:include/src/Zend_XmlRpc_Server.php)。

明白这个就已经明白了Magento的Compiler的一半以上了。

我们去到网站跟目录下的shell目录:

[root@vfeelit public_html]# cd shell/
[root@vfeelit shell]# ls
abstract.php  compiler.php  indexer.php  log.php
[root@vfeelit shell]# php compiler.php help
Usage:  php -f compiler.php -- [options]

  state         Show Compilation State
  compile       Run Compilation Process
  clear         Disable Compiler include path and Remove compiled files
  enable        Enable Compiler include path
  disable       Disable Compiler include path
  help          This help

对应有state compile clear enable disable help。state显示状态,compile开始进行编译,clear表示清理,enable表示启用,disable表示禁用。

[root@vfeelit shell]# php compiler.php compile
Compilation successfully finished

去include/src查看一下(首先是src目录被创建)

.....
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Country.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Datetime.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Font.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Image.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Page.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Tabs.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Form_Element_Theme.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Grid.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Grid_Renderer_Bool.php
Mage_XmlConnect_Block_Adminhtml_Mobile_Grid_Renderer_Type.php
Mage_XmlConnect_Block_Adminhtml_Mobile.php
....

随便查看一个文件的内容,可以证明它就是原始的类文件的一份拷贝。这个时候你应该意识的,这个就是Magento的所谓编译(把所有类文件拷贝到这个文件夹中,避免PHP引擎频繁打开不同的目录)。

不过还有一个疑问,COMPILER_INCLUDE_PATH默认并没有设置,那么它是如何让这个编译起作用呢?
打开shell/combiler.php文件,在类Mage_Shell_Compiler声明之后有:

$shell = new Mage_Shell_Compiler();
$shell->run();

那么进入run方法:

    public function run()
    {
        if (isset($this->_args['compile'])) {
            try {
                $this->_getCompiler()->run();
                echo "Compilation successfully finished\n";
            } catch (Mage_Core_Exception $e) {
                echo $e->getMessage() . "\n";
            } catch (Exception $e) {
                echo "Compilation unknown error:\n\n";
                echo $e . "\n";
            }
        }
    }

    protected function _getCompiler()
    {
        if ($this->_compiler === null) {
            $this->_compiler = Mage::getModel('compiler/process');
        }
        return $this->_compiler;
    }

实际获取compiler/process类实例运行它的run方法,这个方法根据传递的参数来决定运行那块代码,这里传递的是compile参数,对应Mage_Compiler_Model_Process类的run方法,继续查看Mage_Compiler_Model_Process类的run方法:

    public function run()
    {
        $this->_collectFiles();
        $this->_compileFiles();
        $this->registerIncludePath();
        return $this;
    }

    public function registerIncludePath($flag = true)
    {
        $file = $this->_compileDir.DS.'config.php';
        if (is_writeable($file)) {
            $content = file_get_contents($file);
            $content = explode("\n", $content);
            foreach ($content as $index => $line) {
                if (strpos($line, 'COMPILER_INCLUDE_PATH')) {
                    if ($flag) {
                        $content[$index] = ltrim($line, '#');
                    } else {
                        $content[$index] = '#'.$line;
                    }
                }
            }
            file_put_contents($file, implode("\n", $content));
        }
        return $this;
    }

run方法最后运行registerIncludePath方法解释了上面的疑问。它直接操作include/config.php文件,注释或解注释COMPILER_INCLUDE_PATH所在的行。

实际上,enable和disable和clear都会调用registerIncludePath方法操作include/config.php文件。当不启用编译时只要disable就可以了。如果新添加了模块,你的新的模块类并没有添加到include/src的路径中,这个时候include肯定出错,所以你需要先clear一下,然后再次compile即可。但是如果手动删除include/src看起来不太可取,除非你删除了之后马上运行compile。没有compile直接enable也是一样,不过新版本的Magento中在enable之前检查include/src文件夹中是否有文件用来判断是否是已经编译了,如果没有编译就不启用从而避免了include错误(没有编译当然无法include到任何文件)。

如果对拷贝文件的过程有兴趣,可以直接参看源代码,非常直观。另外,再次强调,千万不要和一般意义上的Compiler搞乱。不过我十分怀疑,这个所谓的Compiler到底有多少改进?它只是修改为到同一个文件夹下include文件,这样避免频频切换目录,但是在我可感知的世界里,这个操作不见得有多少性能提升,所以我们最好把性能提升的注意力关注到内存缓存对象,比如APC扩展,它把PHP的编译的中间代码放到内存中缓存(避免多次编译原始的PHP文件),这个才是真正意义上的编译,性能提升非常明显(特别是计算成本)。

另外,除了通过shell操作,还可以通过后台System->Tools->Compiler来设置编译。

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

Magento System Overrides and Upgradability

An oft-touted经常吹捧 and often overused过度使用 feature of The Magento Ecommerce System is the ability to override core system behavior. Another oft-discussed topic经常谈论的主题 for Magento developers is upgradability, and how overrides get in the way of that. Today we’re going to look at the various ways overrides make switching versions difficult.

Before we begin, it’s important to point out we’re talking about changing the core “Business Logic” of Magento. Changes to phtml templates are both expected and common in all but the simplest of stores.

Hacking the Source
The “least upgradable” way to change the behavior of Magento (or any PHP based system) is to alter the source code directly. If you want to change the behavior of the the Product Model, you edit the Product Model file

app/code/core/Mage/Catalog/Model/Product.php

When you do this, you’ve forked the Magento code base(这么干你就和基础代码分道扬镳). Anytime you upgrade the system you’ll need to do a file by file merge文件合并 with your forked version. This rarely goes well. 很少能得好。

Also, your run the risk of changing expected预期的 behavior of methods by having them return different values, not taking actions that the system may depend on, or alter data in unexpected, (rather than expected), ways. We’ll talk more about this below.

Unfortunately, despite its inadvisability尽管不可取, this is the easiest and most understandable way for many PHP developers to start working with Magento. Before starting any new project I always download a clean version of the source and run a diff against both lib and app/code/core to see what sort of changes have been made to the core. 新版本出来,下载一个干净版本和旧版比对,看有哪些改动。

Including Different Classes
Magento, or more accurately PHP, searches for class files in the following Magento folders.通过如下路基搜索类文件:

lib/*
app/code/core/*
app/code/community/*
app/code/local/*

Because of this, and because of the order Magento constructs PHP’s include paths(Magento构建PHP的include路径的顺序), placing a copy of a core file in the app/code/local folder means PHP will include it first. So if you wanted to change the functionality of the Product Model, you’d add your own copy (拷贝一份核心代码放到local下,覆盖核心代码,在拷贝中做修改。这里你可能不知道为何这样做可覆盖核心代码,因为构建PHP include时先把local添加,所以local先搜索)

YOURS:    app/code/local/Mage/Catalog/Model/Product.php
ORIGINAL: app/code/core/Mage/Catalog/Model/Product.php

Your file defines the class instead of the core file, and therefore the core file never needs to be included. This avoids the problem of merging files避免了合并文件的问题 that hacking the source creates, and also centralizes all your customizations in one directory structure.

However, this is still only marginally better稍微好点, and a solution you should avoid. Similar to hacking the core system files, you’re risking a changing the behavior of vital class methods.当修改重要的类是还是很有风险。

For example, consider the getName method on the afformentioned Product Model(产品模型的getName方法举例)

public function getName()
{
    return $this->_getData('name');
}

While overriding this method, you might inadvertently不经意间 add a code path in your override where this method returns null

public function getName($param=false)
{
    if($param == self:NICKNAME)
    {
        return $this->_getData('nickname');
    }
    else if($param == self::BAR)
    {
        return $this->_getData('name')
    }

    //forgot a return because we're working too hard
}

If other parts of the system rely on this method to return a string系统的其它代码依赖这个方法返回字符串, your customizations might break those other parts of the system你的修改将中断系统依赖它的部分. This gets even worse更糟 when methods are returning objects, as trying to call a method on null will result in a fatal error (this is part of what Java and C# programmers are harping on about w/r/t type safety in PHP)

Next, consider the validate method in the same Model.

public function validate()
{
    Mage::dispatchEvent($this->_eventPrefix.'_validate_before', array($this->_eventObject=>$this));
    $this->_getResource()->validate($this);
    Mage::dispatchEvent($this->_eventPrefix.'_validate_after', array($this->_eventObject=>$this));
    return $this;
}

Here, you might inadvertently remove the dispatching events. 不经意删除了dispatching事件

//My local override!
public function validate()
{
    $this->_getResource()->validate($this);
    $this->myCustomValidation($this);
    return $this;
}

Other parts of the system that rely on these events being fired中枪 would stop working.

Finally, you’re still not out of the woods during an upgrade(在更新时仍然无法走出困境). If the methods of any class change during an upgrade在更新中类的方法被改变, Magento will still be including your old你还是包含旧版本, outdated class with the old, outdated methods过时的类和过时的方法. Practically speaking, this means you still need to perform a manual merge during your upgrade.还是要手动合并。

Using the Override/Rewrite System
Magento’s class override/Rewrite system relies on the use of a factory pattern for creating Models, Helpers, and Blocks. When you say重载重写系统使用工厂模式创建模型 助手和块。

Mage::getModel('catalog/product');

you’re telling Magento
“Hey, go lookup the class to use for a “catalog/product” and instantiate it for me.

In turn, Magento then consults咨询 its system configuration files and says

“Hey, config.xml tree! What class am I supposed to use for a “catalog/product”?

Magento then instantiates and returns the Model for you.

When you override a class in Magentoo, you’re changing the configuration files to say

“hey, if a “catalog/product” Model is instantiated, use my class (Myp_Mym_Model_Product) instead of the core class

Then, when you define your class, you have it extend the original class(通过配置重载,继承原来的类)

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
}

This way, your new class has all the old functionality of the original class.通过继承获取原始类的所有功能。 Here you avoid the problem of merging files during an upgrade and the problem of your class containing outdated methods after an upgrade. 这样可以在更新中避免合并文件的问题。

However, there’s still the matter of changing method behavior修改方法行为还是面临问题. Consider, again, the getName and validate methods. Your new methods could just as easily forget/change-the-type-of a return value, or leave out a critical piece of functionality in the original methods.

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {    
        $this->_getResource()->validate($this);
        $this->myCustomValidation($this);
        return $this;
    }   

    public function getName($param=false)
    {
        if($param == self:NICKNAME)
        {
            return $this->_getData('nickname');
        }
        else if($param == self::BAR)
        {
            return $this->_getData('name')
        }

        //forgot a return because we're working too hard
    }       
}

The override/rewrite system won’t protect you from this, but it will give you ways to avoid it提供了方法避免出现这样的问题. Because we’re actually extending the original class, we can call the original method using PHP’s parent:: construct继承了原始类,可以通过parent::调用原始的方法:

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {   
        //put your custom validation up here
        return parent::validate();
    }   

    public function getName($param=false)
    {
        $original_return = parent::getName();
        if($param == self::SOMECONST)
        {
            $original_return = $this->getSomethingElse();
        }           
        return $original_return;
    }       
} 

By calling the original methods, you’ve ensured that any actions that need to take place will take place. Also, by getting the original return value, you’ve reduced the chances that your method will return something unexpected.

That’s reduced, not eliminated. It’s still up to you as the developer to ensure that your custom code returns objects or primitives that are the same as the original method’s. That means even if you use the provided override system, it’s still possible to break the system. 可以减少,但是还是可能中断系统。

Because of this, when I have control of the architecture of a solution I try to keep my overrides to a minimum保持重写最小花. When I do have to override I always try to end my methods with a

return parent::originalMethod();

If my overrides need that original method to run first, I used a construct something like 如果需要首先运行原始的方法

public function someMethod()
{
    $original_return = Mage::getModel('mymodule/immutable')
    ->setValue('this is a test');

    //my custom code here
    return $original_return->getValue();
}

The ‘mymodule/immutable’ URI points to a simple immutable object implementation. (注意看setValue方法)

class Alanstormdotcom_Mymodule_Model_Immutable
{
    protected $_value=null;
    public function setValue($thing)
    {
        if(is_null($this->_value))
        {
            $this->_value = $thing;
            return $this;
        }

        //if we try to set the value again, throw an exception.
        throw new Exception('Already Set');
    }

    public function getValue()
    {
        return $this->_value;
    }
}

This doesn’t prevent someone (including me) from overwriting $original_return with something else, but it does discourage阻碍 it and makes it obvious when someone has. If there’s a method in my override class that doesn’t end in either $original_return->getValue(); or parent::method my eyes are immediately drawn to it as a possible culprit for whatever problem I’m debugging.不这么干首先要意识到是问题的根源。(这里给出了一个类重写的最佳实践)

Finally, there are times where you want, (or think you want) to change the return value of a core method. When the need for this arrises, I find it’s much safer to define a new method that calls the original, and then change your theme to call this new method.定义一个新方法调用原始的方法更加安全,修改主题去调用这个新的方法。

class Mage_Catalog_Model_Original extends Mage_Core_Model_Abstract
{
    protected function getSomeCollectionOriginal()
    {
        return Mage::getModel('foo/bar')
        ->getCollection()->addFieldToFilter('some_field', '42');
    }
}

class Myp_Mym_Model_New extends Mage_Catalog_Model_Original
{
    public function getSomeCollectionWithAdditionalItems()
    {
        $collection = $this->getSomeCollectionOriginal();
        //now, alter or override the $collection with 
        //your custom code

        return $collection;
    }
}

This ensures any additional side effects created by the original method still occur, the original return type/result is the same, and you can still add your custom logic to specific parts of the system.

Wrapup
Upgradability to any major, or even minor, version of a software package you don’t control is always going to be a bumpy ride. Apple and Microsoft spend millions of dollars testing their upgrade paths during new OS releases, and the Internet is still filled with horror stories of the edge cases they miss. Even inside organizations where everyone’s working towards the same goal new versions often bring unspoken assumptions to the surface quickly as builds break and developers are forced to acknowledge the reality that they work with other human beings.

As a user of the Magento Ecommerce System, your job is to ensure that changes to the core system are kept to a minimum, and that when those changes are needed they’re done in a clean, easily diagnosable matter. Leveraging the Magento override/rewrite system is a powerful tool towards this end.

永久链接:http://blog.ifeeline.com/679.html