Magento开发系列之八 后台配置开发

Magento系统中另一个强大的组件是后台Admin的系统配置部分,开发过程中需要让用户可以通过在后台配置系统和自建模块来进行设置。

首先创建一个app/code/local/Alanhou/Helloworld/etc/system.xml文件,和全局配置相似,系统配置信息将单独存储。如果想要查看系统配置文件 ,可以在任意控制器Action中添加如下 代码并执行(这里在前面创建的添加到Helloworld下的IndexController>indexAction方法内):

header('Content-Type: text/xml');
echo $config = Mage::getConfig()
->loadModulesConfiguration("system.xml")
->getNode()
->asXML();
exit;

loadModulesConfiguration方法会查找所有模块下的etc文件夹是否包含所传入的文件名称(这里传入的是system.xml)。Magento中实际上有很多配置文件如api.xml, wsdl.xml, wsdl2.xml, convert.xml, compilation.xml, install.xml等,开发者可以通过创建自己的配置文件来实现更多的功能。输出结果如下:

Magento system.xml

本例中我们首先将在系统配置中添加自己的选项卡,这里说的选项卡是指在后台System > Configuration下左边的导航,默认的有General, Catalog, Customers, Sales, Services和Advanced。我们将创建的选项卡名称为Hello Config,在system.xml中加入如下代码:

<config>
	<tabs>
		<helloconfig translate="label" module="helloworld">
			<label>Hello Config</label>
			<sort_order>99999</sort_order>
		</helloconfig>
	</tabs>
</config>

代码中的<helloconfig />标签名是任意选定的,但不应与其它选项卡重复,这用于标识配置应用于的选项卡。module=”helloworld”属性表示所属模块,<label>定义选项卡显示的名称。<sort_order>表示选项卡在左侧导航中显示的位置,此时访问后台System > Configuration,会出现如下报错:

Fatal error: Class 'Mage_Helloworld_Helper_Data' not found in D:\xampp\htdocs\magento\app\Mage.php on line 547

和大多数流行的PHP MVC系统一样,Magento也会使用Helper类,主要用于处理一些不宜放在Model, View或Controller处理的任务。Helper类是一种抽象类群的名称,也就是说系统用户可以重载默认类,此外模块开发者也需要在config.xml中添加一个代码块来指定Helper的基类名称。

很多系统代码都会假定每个模块有个默认的Helper类,所以出现上面报错的原因是因为在Helloworld模块中并没有默认Helper类而系统尝试调用该类。所似接下来我们在config.xml里加入如下代码:

<global>
	<helpers>
		<helloworld>
			<class>Alanhou_Helloworld_Helper</class>
		</helloworld>
	</helpers>
</global>

基中的<helloworld />标签就是模块名,<class />标签则包含的是Helper类的基础名称,通常按照Packagename_Modulename_Helper的方式命名。Helper通过全局Mage对象的静态方法helper来加载,以本例而言

Mage::helper('helloworld/foo');

会加载以下文件中的以下类

app/code/local/Alanhou/Helloworld/Helper/Foo.php
class Alanhou_Helloworld_Helper_Foo

Magento中还有一个模块默认类的概念,比如只传入模块名时

Mage::helper('helloworld');

会加载以下文件中的以下类

app/code/local/Alanhou/Helloworld/Helper/Data.php
class Alanhou_Helloworld_Helper_Data

也就是说下面两个是等价的

Mage::helper('helloworld');
Mage::helper('helloworld/data');

下面我们在app/code/local/Alanhou/Helloworld/Helper/Data.php中加入如下代码

<?php
class Alanhou_Helloworld_Helper_Data extends Mage_Core_Helper_Abstract{

}

现在我们清除缓存重新访问System > Configuration报错就会消失,但是并不会显示新创建的选项卡。那么接下来我们就来查看为什么新的选项卡没有显示,每个选项卡中都有多个版块(Section),比如Advanced下有Admin, System, Advanced和Developer。那如果选项卡没有添加版块选项也是不显示的,下面我们就在system.xml中添加

代码段

<config>
	<tabs>
		<helloconfig translate="label" module="helloworld">
			<label>Hello Config</label>
			<sort_order>99999</sort_order>
		</helloconfig>
	</tabs>
	<sections>
		<helloworld_options translate="label" module="helloworld">
			<label>Hello World Config Options</label>
			<tab>helloconfig</tab>
			<frontend_type>text</frontend_type>
			<sort_order>1000</sort_order>
			<show_in_default>1</show_in_default>
			<show_in_website>1</show_in_website>
			<show_in_store>1</show_in_store>
		</helloworld_options>
	</sections>
</config>

与标签相似,<helloworld />标签也是任意命名用于标识新的section;标签定义在后台显示的名称;用于指定section添加到哪个选项卡下,这里使用了新创建的;在其它的section有着特别的作用,但在这里没有特别的意义,Core模块中的section会用到这个标签,所以建议也照旧添加这个标签;标签决定了这个版块在垂直方向上的显示顺序;<show_in_default />,<show_in_website /> ,<show_in_store /> 中传入的为布尔值1或0,表明section的作用范围。
重新访问System > Configuration,就会在左侧导航中显示新添加的选项卡和section:
后台创建Helloconfig选项卡
如果点击左侧的Hello World Config Options,会出现空白页面,并且左侧的导航会消失,这主要是因为Adminhtml应用在ACL(Access Control List)中找不到我们新加的Section。系统中有一些资源是需要经过验证才可以使用的,比如后台中的一个页面,使用一些功能。在Magento中系统配置就受到ACL的保护。
资源是通过URI来定义的,比如General下的web版块就是通过admin/system/config/web来定义的,我们创建的helloworld_options对应的URI是admin/system/config/helloworld_options。Admin应用(常被称作Adminhtml)是用与store应用(常被称作frontend)相同的框架构建的,在Adminhtml的action控制器中,无论何时用户要获取受ACL保护的资源,都要:

  1. 获取终端用户想要访问资源的URI
  2. 到ACL中去进行查看,决定登录的用户是否有访问这一资源的权限
  3. 如果用户有相应权限,继续执行,如果没有,就中断执行或采取适当措施

所以我们需要为新的section定义ACL资源,这一操作只针对新添加的section,在已有的section中增加配置无需动到ACL。在config.xml中添加如下代码:

<config>
... ...
<adminhtml>
	<acl>
		<resources>
			<admin>
				<children>
					<system>
						<children>
							<config>
								<children>
									<helloworld_options>
										<title>Store Hello World Module Section</title>
									</helloworld_options>
								</children>
							</config>
						</children>
					</system>
				</children>
			</admin>
		</resources>
	</acl>
</adminhtml>
... ...
</config>

上面的代码信息量有些大,我们来逐一讲解,首先,所有的资源都放在以下节点中

<adminhtml>
    <acl>
        <resources>
        </resource>
    </acl>
</adminhtml>

在资源中,每下一级节点都代表URI的一部分,

<admin>
    <children>
        <system>
            <children>

上面的这段代码所对应的URI就是admin/system,再往下看

<helloworld_options>
	<title>Store Hello World Module Section</title>
</helloworld_options>

其中的title就是admin权限中显示的内容。清除缓存再次点击Hello World Config Options就会出现配置版块了(注意:可能需要登出再登录),此时右侧就会出现标题为Hello World Config Options的空白配置页
Hello World Config Options
接下来我们再加组(group),组是将不同的配置选项放到一起并在后台显示,比如Advanced版块下就有一个名为Disable Modules Output的组。这里我们在system.xml中添加一个messages组

<config>
	<tabs>
		<helloconfig translate="label" module="helloworld">
			<label>Hello Config</label>
			<sort_order>99999</sort_order>
		</helloconfig>
	</tabs>
	<sections>
		<helloworld_options translate="label" module="helloworld">
			<label>Hello World Config Options</label>
			<tab>helloconfig</tab>
			<frontend_type>text</frontend_type>
			<sort_order>1000</sort_order>
			<show_in_default>1</show_in_default>
			<show_in_website>1</show_in_website>
			<show_in_store>1</show_in_store>
			<groups>
				<messages translate="label">
					<label>Demo Of Config Fields</label>
					<frontend_type>text</frontend_type>
					<sort_order>1</sort_order>
					<show_in_default>1</show_in_default>
					<show_in_website>1</show_in_website>
					<show_in_store>1</show_in_store>
				</messages>
			</groups>
		</helloworld_options>
	</sections>
</config>

可以看到组内的标签与section中基本是对应的,现在再重新打开Hello World Config Options就会出现一个空的弹出框
Demo of Config Fields
最后我们要添加一些配置项,主要是通过在中添加,下面我们就添一个名为hello_message的选项

... ...
<messages translate="label">
	<label>Demo Of Config Fields</label>
	<frontend_type>text</frontend_type>
	<sort_order>1</sort_order>
	<show_in_default>1</show_in_default>
	<show_in_website>1</show_in_website>
	<show_in_store>1</show_in_store>
	<fields>
		<hello_message>
			<label>Message</label>
			<frontend_type>text</frontend_type>
			<sort_order>1</sort_order>
			<show_in_default>1</show_in_default>
			<show_in_website>1</show_in_website>
			<show_in_store>1</show_in_store>
		</hello_message>
	</fields>
</messages>
... ...

同样,fields内的标签和其它节点也基本相似,但是这里具备了实际含义,系统通过其中的值来展示不同的表单元素。重新打开Hello World Config Options,就会在右侧多出一个文本框:
Message文本框
除了文本框外还可以添加其它内容,比如时间下拉框,下面添加一个

... ...
<fields>
	<hello_message>
		<label>Message</label>
		<frontend_type>text</frontend_type>
		<sort_order>1</sort_order>
		<show_in_default>1</show_in_default>
		<show_in_website>1</show_in_website>
		<show_in_store>1</show_in_store>
	</hello_message>
	<hello_time>
		<label>Time</label>
		<frontend_type>time</frontend_type>
		<sort_order>2</sort_order>
		<show_in_default>1</show_in_default>
		<show_in_website>1</show_in_website>
		<show_in_store>1</show_in_store>
	</hello_time>
</fields>
... ...

注意这里用到的是

<frontend_type>time</frontend_type>

后台时间下拉框
这里支持大多数的表单类(lib/Varien/Data/Form/Element),下面我们再添加一个下拉框:

... ...
<hello_select>
	<label>Select</label>
	<frontend_type>select</frontend_type>
	<sort_order>3</sort_order>
	<show_in_default>1</show_in_default>
	<show_in_website>1</show_in_website>
	<show_in_store>1</show_in_store>
</hello_select>
... ...

重新访问会发现下拉框中没有内容,这就需要为这个下拉框添加一个source model,修改上面的代码为:

<hello_select>
	<label>Select</label>
	<frontend_type>select</frontend_type>
	<source_model>helloworld/words</source_model>
	<sort_order>3</sort_order>
	<show_in_default>1</show_in_default>
	<show_in_website>1</show_in_website>
	<show_in_store>1</show_in_store>
</hello_select>

为模型类定义了一个URI,我们将用这个URI来获取select下拉框中的默认值,在config.xml中添加如下代码:

<global>
... ...
	<models>
		<helloworld>
			<class>Alanhou_Helloworld_Model</class>
		</helloworld>
	</models>
... ....
</global>

重新访问会出现如下报错

Warning: include(Alanhou/Helloworld/Model/Words.php)

创建app/code/local/Alanhou/Helloworld/Model/Words.php并加入如下代码:

<?php class Alanhou_Helloworld_Model_Words{ public function toOptionArray(){ return array( array('value'=>1, 'label'=>Mage::helper('helloworld')->__('Hello')),
			array('value'=>2, 'label'=>Mage::helper('helloworld')->__('Goodbye')),
			array('value'=>3, 'label'=>Mage::helper('helloworld')->__('Yes')),
			array('value'=>4, 'label'=>Mage::helper('helloworld')->__('No')),
		);
	}
}

Source Model是对toOptionsArray方法响应的类,这个方法会返回一组值供表单使用,对于select标签而言,这意味着定义一组值、标签对。上面的代码中,我们通过Helper的translation方法(__)传入标签,这也是推荐的做法。
再次访问页岩,select下拉框中就会正常显示值了
Magento后台下拉框
感兴趣的朋友可以查看app/code/core/Mage/Adminhtml/Block/System/Config/Form.php中的initFields方法来看看source Model是如何设定选项值的。
接下来我们再在系统已有版块中进行内容添加,在system.xml中加入如下代码:

<sections>
... ...
	<general>
		<groups>
			<example>
				<label>Example of Adding a Group</label>
				<frontend_type>text</frontend_type>
				<sort_order>1</sort_order>
				<show_in_default>1</show_in_default>
				<show_in_website>1</show_in_website>
				<show_in_store>1</show_in_store>
			</example>
		</groups>
	</general>
... ...
</sections>

此时在General版块下就会多出一个名为Example of Adding a Group的组
Example of Adding a Group
前面我们已经讲过如何在配置中添加表单,要从客户应用或模块中获取值,我们将使用全局Mage对象中的getStoreConfig方法,比如要获得我们前面创建的select中的值,就可以使用:

Mage::getStoreConfig('helloworld_options/messages/hello_message');

getStoreConfig中的URI参数格式为

section_name/group_name/field_name

也可以通过指定部分路径来获取配置值的一个数组,如:
Mage::getStoreConfig(‘helloworld_options/messages’);
Mage::getStoreConfig(‘helloworld_options’);
最后,如果想要获取非当前店铺(store)的配置,可在getStoreConfig中传入一个店铺ID

Mage::getStoreConfig('helloworld_options',1);

Magento开发系列之七 EAV-更高级的ORM

前面关于ORM的章节我们讲述了在Magento中有两种模型,一种是普通模型,另一种是EAV(Entity Atrribute Value)模型。本节我们就来进一步的了解EAV模型。

在Magento中所有与数据库交互的模型都继承自Mage_Core_Model_Abstract, Varien_Object,而普通模型与EAV模型的区别在于模型资源。所有的资源都继承基础类Mage_Core_Model_Resource_Abstract,普通模型有一个继承自Mage_Core_Model_Resource_Db_Abstract的资源,而EAV模型则有一个继承自Mage_Eav_Model_Entity_Abstract的资源。

那么什么是EAV呢?维基百科上的解释是:

Entity–attribute–value model (EAV) is a data model to describe entities where the number of attributes
(properties, parameters) that can be used to describe them is potentially vast, but the number that will actually apply to a given entity is relatively modest. In mathematics, this model is known as a sparse matrix. EAV is also
known as object–attribute–value model, vertical database model and open schema.

还可以这样理解,EAV为数据库表格schema带来某些方面的标准化,传统的数据库中数据表具有固定的列数。

+------------------+
| products         |
+------------------+
| product_id       |
| name             |
| price            |
| etc..            |
+------------------+

+------------+----------------+------------------+---------+
| product_id | name           | price            | etc...  |
+------------+----------------+------------------+---------+
| 1          | Widget A       | 11.34            | etc...  |
+------------+----------------+------------------+---------+
| 2          | Dongle B       | 6.34             | etc...  |
+------------+----------------+------------------+---------+

以产品表为例,就是每个产品都有名称、价格等等。而在EAV模型中,每个实体(entity)如产品都可能会有一组不同的属性,从电商平台来看EAV有着重要的价值。比如对笔记本网站而言会有CPU 频率、颜色、内存大小等属性,而毛衣网站则有颜色属性但没有内存大小。即使针对毛衣相关的产品,有些是按长度计的(如毛线),而有些则按直径大小计的(如棒针)。

开源或付费数据库中默认支持EAV的并不多,因而Magento团队通过以MySQL做为数据存储的PHP对象创建了EAV系统,换句话说是在传统的关系型数据库基础上创建了EAV数据库系统。
从实践角度来说就是任何使用EAV资源的模型,属性将散布在多张MySQL数据表中。

实体、属性、值-数据 库

上表大致演示了在操作catalog_product实体查询一条EAV记录时所涉及的一些数据表,每个产品都记录在catalog_product_entity表的某一行中。整个系统中所有的属性都会存储在eav_attribute中,而产品真正的属性值则存储在catalog_product_entity_varchar, catalog_product_entity_decimal等以catalog_product_entity_开头的表格中。

EAV所具有的灵活性让我们在实际工作中添加新的属性时无需使用ALTER TABLE语句来操作,仅需在eav_attribute表中新插入一行即可。而在传统的关系型数据库中,这种操作需要通过ALTER语句来修改数据结构,对于较大的表而言不仅耗时而且存在潜在风险。
这样做也有一个缺点,就是如果想要获取产品数据时,一个简单的SQL语句是肯定无法实现的,需要嵌套多条语句或连接多张数据表。

以上就是关于EAV的简介,下面我们会通过实例来一步步创建EAV模型,这次我们再创建一个博客文章的模型但是我们将用到会是EAV
模型,如果大家看过前的章节一定已经非常熟悉了,点此略过,这里简单说一下,创建
app/code/local/Alanhou/Complexworld/etc/config.xml文件加入如下代码:

<config>
	<modules>
		<Alanhou_Complexworld>
			<version>0.1.0</version>
		</Alanhou_Complexworld>
	</modules>
	<frontend>
		<routers>
			<complexworld>
				<use>standard</use>
				<args>
					<module>Alanhou_Complexworld</module>
					<frontName>complexworld</frontName>
				</args>
			</complexworld>
		</routers>
	</frontend>
</config>

创建app/etc/modules/Alanhou_Complexworld.xml加入以下代码:

<config>
	<modules>
		<Alanhou_Complexworld>
			<active>true</active>
			<codePool>local</codePool>
		</Alanhou_Complexworld>
	</modules>
</config>

创建app/code/local/Alanhou/Complexworld/controllers/IndexController.php文件并加入如下代码:

<?php
class Alanhou_Complexworld_IndexController extends Mage_Core_Controller_Front_Action{
	public function indexAction(){
		echo 'Hello Complexworld!';
	}
}

此时访问http://localhost/magento/complexworld即会在页面上输出Hello Complexworld!

EAV风格的Weblog

下一步我们创建一个名为Weblogeav的模型,首先在config.xml中加入以下代码:

<global>
	<models>
		<complexworld>
			<class>Alanhou_Complexworld_Model</class>
			<resourceModel>complexworld_resource</resourceModel>
		</complexworld>

		<complexworld_resource>
			<class>Alanhou_Complexworld_Model_Resource</class>
			<entities>
				<eavblogpost>

<table>eavblog_posts</table>

				</eavblogpost>
			</entities>
		</complexworld_resource>
	</models>
</global>

截至目前和创建一个普通的模型资源还没有什么区别,我们通过<class />标签设置了PHP类,同时添加<entities />代码段来告知Magento我们所要创建模型的基本表名称,<eavblogpost />标签是将要创建的模型名称,而其中的<table/>标签用于指定这个模型将用到的基础表。

在我们前面的IndexController.php中加入如下代码:

	public function indexAction(){
		$weblog2 = Mage::getModel('complexworld/eavblogpost');
		$weblog2->load(1);
		var_dump($weblog2);
	}

此时访问http://localhost/magento/complexworld/会出现类似如下报错

Warning: include(Alanhou\Complexworld\Model\Eavblogpost.php): failed to open stream: No such file or directory  
in D:\xampp\htdocs\magento\lib\Varien\Autoload.php on line 94

这段提示不仅告诉了我们需要在哪个路径下添加新资源,它还进行了配置检查,如果是下面这样的提示:

Warning: include(Mage\Complexworld\Model\Eavblogpost.php) [function.include]: failed to open stream: No such file or directory  in D:\xampp\htdocs\magento\lib\Varien\Autoload.php on line 94

我们就会知道模型的配置存在问题,因为此时Magento是在code/core/Mage中查找模型,而不是在code/local/Alanhou中查找。下面我就来创建模型类,在app/code/local/Alanhou/Complexworld/Model/Eavblogpost.php中添加如下代码:

<?php
	class Alanhou_Complexworld_Model_Eavblogpost extends Mage_Core_Model_Abstract{
		protected function _construct()
		{
			$this->_init('complexworld/eavblogpost');
		}
	}

注意:模型本身独立于资源,不论是普通模型还是EAV模型都继承自相同的类,它们的区别在资源上。刷新缓存重新加载页面会出现新的提示:

Warning: include(Alanhou\Complexworld\Model\Resource\Eavblogpost.php): failed to open stream: No such file or 

directory  in D:\xampp\htdocs\magento\lib\Varien\Autoload.php on line 94

按照提示我们再创建app/code/local/Alanhou/Complexworld/Model/Resource/Eavblogpost.php并添加如下代码:

<?php class Alanhou_Complexworld_Model_Resource_Eavblogpost extends Mage_Eav_Model_Entity_Abstract{
 	protected function _construct(){
 		$resource = Mage::getSingleton('core/resource');
 		$this->setType('complexworld_eavblogpost');
		$this->setConnection(
			$resource->getConnection('complexworld_read'),
			$resource->getConnection('complexworld/write')
		);
	}
}

这里我们已经可以看到普通模型资源和EAV模型资源的差别,首先我继承的类名是Mage_Eav_Model_Entity_Abstract,虽然Mage_Eav_Model_Entity_Abstract也使用了与普通模型资源相同的_construct方法,但这里并没有用到_init方法,取而代之的是使用了自己的代码。这里指明了使用何种连接资源,并将唯一标识符传递给我们对象中的setType方法。
刷新缓存重新访问页面,会出现如下报错

Invalid entity_type specified: complexworld_eavblogpost

这里报的的entity_type就是前面通过setType所设置的

$this->etType('complexworld_eavblogpost');

每个实体都有类型,类型就是为了让EAV系统知道模型要使用哪些属性,以及连接哪些存储性值的表格。我们需要告诉Magento添加了新的实体类型,那么我们查看一下eav_entity_type一表:

mysql> select * from eav_entity_type limit 2 \G;
*************************** 1. row ***************************
             entity_type_id: 1
           entity_type_code: customer
               entity_model: customer/customer
            attribute_model: customer/attribute
               entity_table: customer/entity
         value_table_prefix: NULL
            entity_id_field: NULL
            is_data_sharing: 1
           data_sharing_key: default
   default_attribute_set_id: 1
            increment_model: eav/entity_increment_numeric
        increment_per_store: 0
       increment_pad_length: 8
         increment_pad_char: 0
 additional_attribute_table: customer/eav_attribute
entity_attribute_collection: customer/attribute_collection
*************************** 2. row ***************************
             entity_type_id: 2
           entity_type_code: customer_address
               entity_model: customer/address
            attribute_model: customer/attribute
               entity_table: customer/address_entity
         value_table_prefix: NULL
            entity_id_field: NULL
            is_data_sharing: 1
           data_sharing_key: default
   default_attribute_set_id: 2
            increment_model: NULL
        increment_per_store: 0
       increment_pad_length: 8
         increment_pad_char: 0
 additional_attribute_table: customer/eav_attribute
entity_attribute_collection: customer/address_attribute_collection
2 rows in set (0.00 sec)

这张表中包含了系统中所有的实体属性,本例中的complexworld_eavblogpost对应于entity_type_code一列。

接下来我们就来创建Setup资源。理论上可以手动在eav_entity_type表中为complexworld_eavblogpost插入一行值,但我们不推荐这么做。还好Magento提供了专门的安装资源,含有一些helper方法可以自动创建一所需的记录来让系统正常运行。
首先在config.xml中添加一段代码:

<global>
	<!--...-->
	<resources>
		<complexworld_setup>
			<setup>
				<module>Alanhou_Complexworld</module>
				<class>Alanhou_Complexworld_Model_Resource_Setup</class>
			</setup>
		</complexworld_setup>
	</resources>
	<!--...-->
</global>

然后创建app/code/local/Alanhou/Complexworld/Model/Resource/Setup.php并添加如下类:

<?php
class Alanhou_Complexworld_Model_Resource_Setup extends Mage_Eav_Model_Entity_Setup{

}

注意我们这里继承的类是 Mage_Eav_Model_Entity_Setup而不是Mage_Core_Model_Resource_Setup,最后我们就来创建安装脚本,创建app/code/local/Alanhou/Complexworld/sql/complexworld_setup/install-0.1.0.php并加入如下代码:

<?php
$installer = $this;
throw new Exception("This is an exception to stop the installer from completing");

清除缓存重新访问页面,如果你看到页面中抛出了上面添加的异常就说明我们安装资源配置没有问题。
Magento抛出异常
接下来我们在安装脚本中删除出抛出异常代码段,修改脚本内容如下:

<?php $installer = $this; $installer->startSetup();
$installer->addEntityType('complexworld_eavblogpost',array(
	//entity_model is the URI you'd pass into a Mage::getModel() call
	'entity_model' => 'complexworld/eavblogpost',
	//table refers to the resource URI complexworld/eavblogpost
    //<complexworld_resource>...<eavblogpost>
    'table' => 'complexworld/eavblogpost',
	));
$installer->endSetup();

这里我们在安装脚本中调用了addEntityType方法,该方法可以传递实体类型以及一系参数来设置默认值。运行此脚本后会在eav_attribute_group, eav_attribute_set和eav_entity)type表中产生新的行。
重新访问页面会出现如下报错:

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'magento.eavblog_posts' doesn't exist, query was: SELECT 

<code>eavblog_posts</code>.* FROM <code>eavblog_posts</code> WHERE (entity_id =1)

注:如果你学习了之前的章节,就会知道版本号存放在core_resource一表中,为保证安装脚本正常运行,可能需要经常执行如下操作:

 delete from core_resource where code= 'complexworld_setup';

前面我们已经把实体类型告诉给Magento了,下面我们需要创建一个MySQL表格用于存储实体的值,并且还要进行配置来让系统知道所创建的这些表。
EAV安装资源中有一个名为createEntityTables的方法,它会自动设置我们所需的数据表,并向系统添加一些配置行,下面就在安装脚本中添加如下代码:

$installer->createEntityTables(
	$this->getTable('complexworld/eavblogpost')
);

createEntityTables方法中可传入两个参数,第一个是基础表的名称,第二是一系列选项。这里我们用到了安装资源的getTable方法来从配置中抓出表名,也就是前面设置的eavblog_posts。本例中我们省去了第二个参数,通常只要进行更高级的开发时才会使用这一选项。再次访问页面,会发现创建了以下表格:

mysql> show tables like 'eavblog_posts%';
+--------------------------------+
| Tables_in_magento (eavblog_posts%) |
+--------------------------------+
| eavblog_posts                  |
| eavblog_posts_char             |
| eavblog_posts_datetime         |
| eavblog_posts_decimal          |
| eavblog_posts_int              |
| eavblog_posts_text             |
| eavblog_posts_varchar          |
+--------------------------------+
7 rows in set (0.01 sec)

此外在eav_attribute_set表中还是新增一行:

mysql> select * from eav_attribute_set order by attribute_set_id DESC LIMIT 1 \G

*************************** 1. row ***************************
  attribute_set_id: 10
    entity_type_id: 10
attribute_set_name: Default
        sort_order: 2
1 row in set (0.08 sec)

再次访问http://localhost/mgt/complexworld,不再出现报错。最后我们需要告诉EAV模型我们需要哪些属性,这与在普通数据表中添加列是等价的。同样,我们还将通过安装资源来进行操作,这次用到的方法名为addAttribute。先在安装脚本中添加如下代码来为Eavblogpost添加一个属性:

$this->addAttribute('complexworld_eavblogpost', 'title', array(
        //the EAV attribute type, NOT a MySQL varchar
        'type'              => 'varchar',
        'label'             => 'Title',
        'input'             => 'text',
        'class'             => '',
        'backend'           => '',
        'frontend'          => '',
        'source'            => '',
        'required'          => true,
        'user_defined'      => true,
        'default'           => '',
        'unique'            => false,
    ));

addAttribute方法中的第一个参数是实体类型code,它需要与addEntityType相符,如果想要查看Magento中还有哪些其它实体,可查询eav_entity_type表中的entity_type_code一列;第二个参数是属性code,在同一个实体中该名称不可重复;第三个参数是一个数组,用于描述属性,为了简化我们这里只定义了一个属性,实际上是可以同时添加多个的。
值得一提的是以下这一行

'type' => 'varchar'

它定义属性所包含的值的类型,前面我们提到以下这些表

eavblog_posts_char
eavblog_posts_datetime
eavblog_posts_decimal
eavblog_posts_int
eavblog_posts_text
eavblog_posts_varchar

这些并不是指MySQL中列的属性而是EAV属性类型,其名称(如decimal, varchar等)即表示其中所存储的数据。前面数组中所有这些属性都是可以的,如果我们不予以指定Magento就会使用默认值。这些默认值由Mage_Eav_Model_Entity_Setup类中的_prepareValues所定义,详情如下:

protected function _prepareValues($attr)
{
$data = array(
    'backend_model'   => $this->_getValue($attr, 'backend'),
    'backend_type'    => $this->_getValue($attr, 'type', 'varchar'),
    'backend_table'   => $this->_getValue($attr, 'table'),
    'frontend_model'  => $this->_getValue($attr, 'frontend'),
    'frontend_input'  => $this->_getValue($attr, 'input', 'text'),
    'frontend_label'  => $this->_getValue($attr, 'label'),
    'frontend_class'  => $this->_getValue($attr, 'frontend_class'),
    'source_model'    => $this->_getValue($attr, 'source'),
    'is_required'     => $this->_getValue($attr, 'required', 1),
    'is_user_defined' => $this->_getValue($attr, 'user_defined', 0),
    'default_value'   => $this->_getValue($attr, 'default'),
    'is_unique'       => $this->_getValue($attr, 'unique', 0),
    'note'            => $this->_getValue($attr, 'note'),
    'is_global'       => $this->_getValue($attr, 'global', 1),
    );
    return $data;

 }

该方法中的_getValue的第二个值对应addAttribute中的键,第三个就是默认值,所以Magento默认会认为你在添加一个varchar属性。
接下来我们为博客内容和发表日期添加属性,最终的脚本内容如下:

<?php $installer = $this; $installer->startSetup();
$installer->addEntityType('complexworld_eavblogpost',array(
	//entity_model is the URI you'd pass into a Mage::getModel() call
	'entity_model' => 'complexworld/eavblogpost',
	//table refers to the resource URI complexworld/eavblogpost
    //<complexworld_resource>...<eavblogpost>
    'table' => 'complexworld/eavblogpost',
	));
$installer->createEntityTables(
	$this->getTable('complexworld/eavblogpost')
);

$this->addAttribute('complexworld_eavblogpost', 'title', array(
        //the EAV attribute type, NOT a MySQL varchar
        'type'  => 'varchar',
        'label'  => 'Title',
        'input' => 'text',
        'class'  => '',
        'backend'  => '',
        'frontend' => '',
        'source' => '',
        'required' => true,
        'user_defined' => true,
        'default' => '',
        'unique' => false,
    ));
$this->addAttribute('complexworld_eavblogpost','content',array(
	'type' => 'text',
	'label' => 'Content',
	'input' => 'textarea',
	));

$this->addAttribute('complexworld_eavblogpost','data',array(
	'type' => 'datetime',
	'label' => 'Post Date',
	'input' => 'datetime',
	'required' => false,
	));
$installer->endSetup();

现在我们再最后刷新一次,安装脚本将会运行,调用addAttribute方法后:

  1. 在eav_entity_type表中会增加一个complexworld_eavblogpost的实体类型
  2. 在eav_attribute表中会增加一个title属性
  3. 在eav_attribute表中会增加一个content属性
  4. 在eav_attribute表中会增加一个date属性
  5. eav_entity_attribute表中会增加一行

显然这个博客模型很不完善,但让我添加一行并循环取出内容来进行测试,在IndexController.php中添加如下Action

public function populateEntriesAction(){
	for($i=0; $i<10; $i++){
 		$weblog2 = Mage::getModel('complexworld/eavblogpost');
 		$weblog2->setTitle('This is a test '.$i);
		$weblog2->setContent('This is test content '.$i);
		$weblog2->setDate(now());
		$weblog2->save();
	}

	echo 'Done';
}

public function showCollectionAction(){
	$weblog2 = Mage::getModel('complexworld/eavblogpost');
	$entries = $weblog2->getCollection()
		->addAttributeToSelect('title')
		->addAttributeToSelect('content');
	$entries->load();
	foreach ($entries as $entry) {
		// var_dump($entry->getData());
		echo '<h2>'.$entry->getTitle().'</h2>';
		echo '<p>Date: '.$entry->getDate().'</p>';
		echo '<p>'.$entry->getContent().'</p>';
	}
	echo '<br>Done<br>';
}

我们访问http://localhost/magento/complexworld/index/populateEntries
然后到数据库中查询一下新添加的10行数据
SELECT * FROM eavblog_posts ORDER BY entity_id DESC;
eavblog_post表
以及在eavblog_posts_varchar表中新增的10行记录:
SELECT * FROM eavblog_posts_varchar ORDER BY value_id DESC;
eavblog_posts_varchar表
请注意eavblog_posts_varchar与eavblog_posts这两张表通过entity_id进行连接
最后我们通过访问http://localhost/magento/complexworld/index/showCollection来把数据提取出来
这时会出现如下报错

Warning: include(Alanhou\Complexworld\Model\Resource\Eavblogpost\Collection.php): failed to open stream: No such file or directory  in D:\xampp\htdocs\mgt\lib\Varien\Autoload.php on line 94

离胜利还差一步,我们这里没有为集合对象添加类,方法和添加一个常规的模型资源相似,创建

Alanhou/Complexworld/Model/Resource/Eavblogpost/Collection.php

<?php
class Alanhou_Complexworld_Model_Resource_Eavblogpost_Collection extends Mage_Eav_Model_Entity_Collection_Abstract{ 	protected function _construct(){
 		$this->_init('complexworld/eavblogpost');
	}
}

这是一个初始化模型标准的_construct方法,重新载入页面,我们就可以看到所有的标题和内容输出,但是没有日期值。
可能你会注意到集合加载有些不一样

$entries = $weblog2->getCollection()
	->addAttributeToSelect('title')
	->addAttributeToSelect('conent');

因为查询EAV中的数据会用到大量的SQL,我们需要指定模型需要哪些属性,这样系统只查询那些需要的数据。如果不在乎性能上的损耗,可以使用如下方法获取所有属性

$entries = $weblog2->getCollection()->addAttributeToSelect('*');

当然关于EAV还有更多的内容,就本例而言,还可以进行如下的探讨

  1. EAV属性不仅限于datetime, decimal, int, text和varchar,我们还可以创建其它类来为更多的属性建模,这也是attribute_model实体属性的作用
  2. 集合过滤:EAV集合的过滤会有些麻烦,尤其是针对我们上面提到的非普通属性,需要在加载前在集合中使用addAttributeToFilter
  3. Magento中的EAV层级:Magento使用基本EAV模型创建与店铺功能相关的层级结构,这个结构有有助于减少EAV模型生成的查询的数量

EAV模型可以说是Magento系统开发过程中会碰到的最复杂的部分,随着逐步的使用就会慢慢地适应其用法。
http://localhost/magento/complexworld/index/showCollection显示结果:showCollection
注:在本例中除了需经常删除core_resource中的对应行外,还可能会需要删除eavblog_posts相关表格,可采用如下语句:

SET FOREIGN_KEY_CHECKS=0; 
drop table  eavblog_posts;
drop table  eavblog_posts_char;
drop table  eavblog_posts_datetime;
drop table  eavblog_posts_decimal;
drop table  eavblog_posts_int;
drop table  eavblog_posts_text;
drop table  eavblog_posts_varchar;

Magento开发系列之六 安装、升级脚本

在任何高效的软件开发项目中,保持开发库和线上库同步都会是一件棘手的事情。Magento通过在系统中创建带有版本号的资源迁移脚本来让这种开发过程更加的顺畅。前面有关ORM的章节我们创建了一个weblog模型,那时我们使用CREATE TABLE语句直接在数据库中创建表格,今天通过对模块的资源配置(Setup Resource)来创建数据表。我们还会在模块中创建升级脚本来完成对已创建模型的升级。主要有以下几步:

  1. 在配置文件中添加Setup资源
  2. 创建资源类文件
  3. 创建安装脚本
  4. 创建升级脚本

我们还是在上一节创建的weblog模块中进行修改,在config.xml的<global />标签内添加以下代码:

<resources>
	<weblog_setup>
		<setup>
			<module>Alanhou_Weblog</module>
			<class>Alanhou_Weblog_Model_Resource_Setup</class>
		</setup>
	</weblog_setup>
</resources>

<weblog_setup>标签用于标识这个安装资源,通常也建议使用modelname_setup的命名机制。<module>Alanhou_Weblog</module>的内容应为模块的命名空间和模块名Packagename_Modulename。<class>标签中包含的是我们将要为Setup资源创建的类名,对于普通的setup脚本可以不自建类,但现在这么做会给以后的开发带来更大的灵活性。

添加完上面的代码后清除缓存并加载Magento中的任意页,会出现如下提示:

Warning: include(Alanhou\Weblog\Model\Resource\Setup.php): failed to open stream: No such file or directory

创建app/code/local/Alanhou/Weblog/Model/Resource/Setup.php并加入如下代码:

<?php
class Alanhou_Weblog_Model_Resource_Setup extends Mage_Core_Model_Resource_Setup{

}

重新访问任意页,前面的报错会消失页面正常显示。

下面我们来创建安装脚本,这个脚本中将包含初始化模块时所需的CREATE TABLE及其它SQL语句。首先让我们来看一下config.xml文件

<config>
	<modules>
		<Alanhou_Weblog>
			<version>0.1.0</version>
		</Alanhou_Weblog>
	</modules>
</config>

这段代码在所有的config.xml文件中都需要添加,主要用于确定模块及版本号。我们的安装脚本就需要用到版本号,本例将假设初使版本号为0.1.0。创建app/code/local/Alanhou/Weblog/sql/weblog_setup/mysql4-install-0.1.0.php文件并加入如下代码:

<?php
echo "Running this upgrade: ".get_class($this)."\n<br />\n";
die("Exit for now");

目录名weblog_setup需要与conifg.xml中的标签<weblog_setup />相同,而文件名称中的0.1.0则应与模块的版本号相同,刷新缓存并访问任意页面会得到如下提示:

Running this upgrade: Alanhou_Weblog_Model_Resource_Setup
Exit for now

注:如果看不到如上提示可能是由于之前访问过页面,执行如下语句再重新访问即可:

delete from core_resource where code = 'weblog_setup';

上面的提示说明升级脚本确实运行了,后面我们会加入SQL升级脚本,不过现在还是先了解下安装机制。删除die()语句,再次访问会在页面的头部出现Running this upgrade: Alanhou_Weblog_Model_Resource_Setup,重复再刷新一下提示就会消失而页面也显示正常。

Magento的安装资源让我们可以轻易地将安装或升级脚本放到服务器上,然后系统自动运行这些脚本,这样系统中所有的数据库迁移脚本都会保持连续性。

使用熟悉的数据库客户端输入如下语句查询core_table数据表:

mysql> select * from core_resource;
+-------------------------+----------------+----------------+
| code                    | version        | data_version   |
+-------------------------+----------------+----------------+
| adminnotification_setup | 1.6.0.0        | 1.6.0.0        |
| admin_setup             | 1.6.1.1        | 1.6.1.1        |
| api2_setup              | 1.0.0.0        | 1.0.0.0        |
| api_setup               | 1.6.0.1        | 1.6.0.1        |
| backup_setup            | 1.6.0.0        | 1.6.0.0        |
| bundle_setup            | 1.6.0.0.1      | 1.6.0.0.1      |
| captcha_setup           | 1.7.0.0.0      | 1.7.0.0.0      |
| catalogindex_setup      | 1.6.0.0        | 1.6.0.0        |
| cataloginventory_setup  | 1.6.0.0.2      | 1.6.0.0.2      |
| catalogrule_setup       | 1.6.0.3        | 1.6.0.3        |
| catalogsearch_setup     | 1.8.2.0        | 1.8.2.0        |
| catalog_setup           | 1.6.0.0.19.1.2 | 1.6.0.0.19.1.2 |
| checkout_setup          | 1.6.0.0        | 1.6.0.0        |
| cms_setup               | 1.6.0.0.2      | 1.6.0.0.2      |
| compiler_setup          | 1.6.0.0        | 1.6.0.0        |
| complexworld_setup      | 0.1.0          | 0.1.0          |
| contacts_setup          | 1.6.0.0        | 1.6.0.0        |
| core_setup              | 1.6.0.6        | 1.6.0.6        |
| cron_setup              | 1.6.0.0        | 1.6.0.0        |
| customer_setup          | 1.6.2.0.4      | 1.6.2.0.4      |
| dataflow_setup          | 1.6.0.0        | 1.6.0.0        |
| directory_setup         | 1.6.0.3        | 1.6.0.3        |
| downloadable_setup      | 1.6.0.0.2      | 1.6.0.0.2      |
| eav_setup               | 1.6.0.1        | 1.6.0.1        |
| giftmessage_setup       | 1.6.0.0        | 1.6.0.0        |
| googleanalytics_setup   | 1.6.0.0        | 1.6.0.0        |
| importexport_setup      | 1.6.0.2        | 1.6.0.2        |
| index_setup             | 1.6.0.0        | 1.6.0.0        |
| log_setup               | 1.6.1.1        | 1.6.1.1        |
| moneybookers_setup      | 1.6.0.0        | 1.6.0.0        |
| newsletter_setup        | 1.6.0.2        | 1.6.0.2        |
| oauth_setup             | 1.0.0.0        | 1.0.0.0        |
| paygate_setup           | 1.6.0.0        | 1.6.0.0        |
| payment_setup           | 1.6.0.0        | 1.6.0.0        |
| paypaluk_setup          | 1.6.0.0        | 1.6.0.0        |
| paypal_setup            | 1.6.0.6        | 1.6.0.6        |
| persistent_setup        | 1.0.0.0        | 1.0.0.0        |
| poll_setup              | 1.6.0.1        | 1.6.0.1        |
| productalert_setup      | 1.6.0.0        | 1.6.0.0        |
| rating_setup            | 1.6.0.1        | 1.6.0.1        |
| reports_setup           | 1.6.0.0.1      | 1.6.0.0.1      |
| review_setup            | 1.6.0.0        | 1.6.0.0        |
| salesrule_setup         | 1.6.0.3        | 1.6.0.3        |
| sales_setup             | 1.6.0.9        | 1.6.0.9        |
| sendfriend_setup        | 1.6.0.1        | 1.6.0.1        |
| shipping_setup          | 1.6.0.0        | 1.6.0.0        |
| sitemap_setup           | 1.6.0.0        | 1.6.0.0        |
| tag_setup               | 1.6.0.0        | 1.6.0.0        |
| tax_setup               | 1.6.0.4        | 1.6.0.4        |
| usa_setup               | 1.6.0.3        | 1.6.0.3        |
| weblog_setup            | 0.1.0          | 0.1.0          |
| weee_setup              | 1.6.0.0        | 1.6.0.0        |
| widget_setup            | 1.6.0.0        | 1.6.0.0        |
| wishlist_setup          | 1.6.0.0        | 1.6.0.0        |
+-------------------------+----------------+----------------+
54 rows in set (0.00 sec)

这张表中包含所有已安装的模块及其版本号,在靠近下面的部分可以看到我们的模块

| weblog_setup            | 0.1.0          | 0.1.0          |

正是因为有这条记录,我们在第二次重新访问页面后安装脚本才没有运行,如果想要重新执行安装或升级脚本,比如在开发时想要查看效果,可以删除掉这条记录。那么现在我们就先删除掉这条记录并添加一条SQL语句来创建数据表,删除记录的代码为:

DELETE FROM core_resource WHERE code = 'weblog_setup';

另外我们还要先删除在ORM一节中所创建的blog_post一表,执行如下语句

DROP TABLE blog_posts;

然后在安装脚本(mysql4-install-0.1.0.php)中添加如下代码:

$installer =$this;
$installer-&gt;startSetup();
$installer-&gt;run(&quot;
	CREATE TABLE <code>{$installer-&gt;getTable('weblog/blogpost')}</code>(
		<code>blogpost_id</code> int(11) NOT NULL auto_increment,
		<code>title</code> text,
		<code>post</code> text,
		<code>date</code> datetime default NULL,
		<code>timestamp</code> timestamp NOT NULL default CURRENT_TIMESTAMP,
		PRIMARY KEY(<code>blogpost_id</code>)
		)ENGINE=InnoDB DEFAULT CHARSET=utf8;
	INSERT INTO <code>{$installer-&gt;getTable('weblog/blogpost')}</code> VALUES
	(1,'My New Title','This is a blog post','2015-8-09 00:00:00','2015-8-10 23:12:30'
		)
&quot;);
$installer-&gt;endSetup();

删除缓存重新加载任意页就会创建blog_posts数据表并插入一行内容。

接下来我们就一起来分析一下上面代码的含义

$installer =$this;

这句代码中的this指的是什么呢?每个安装脚本都在Setup资源类的环境中运行,自然$this就指向这个类的实例化对象。习惯上我们都会像本例中那样在安装脚本中为$this取一个别名$installer,没有特殊原因建议你在开发过程中也照做。

$installer->startSetup();
//...
$installer->endSetup();

紧接着的就是上面的代码段,如果查看app/code/core/Mage/Core/Model/Resource/Setup.php中的Mage_Core_Model_Setup类(也就是我们这个安装脚本所继承的类),会发现这些方法会进行了一些基本的SQL操作

public function startSetup()
    {
        $this->getConnection()->startSetup();
        return $this;
    }

    /**
     * Prepare database after install/upgrade
     *
     * @return Mage_Core_Model_Resource_Setup
     */
    public function endSetup()
    {
        $this->getConnection()->endSetup();
        return $this;
    }

查看Varien_Db_Adapter_Pdo_Mysql(lib/Varien/Db/Adapter/Pdo/Mysql.php)就可以找到上面用到的startSetup()和endSetup()方法

 public function startSetup()
    {
        $this->raw_query("SET SQL_MODE=''");
        $this->raw_query("SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0");
        $this->raw_query("SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'");

        return $this;
    }

    /**
     * Run additional environment after setup
     *
     * @return Varien_Db_Adapter_Pdo_Mysql
     */
    public function endSetup()
    {
        $this->raw_query("SET SQL_MODE=IFNULL(@OLD_SQL_MODE,'')");
        $this->raw_query("SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS=0, 0, 1)");

        return $this;
    }

另外安装脚本中清空调用了run方法

$installer->run(...);

该方法中传入了一个用于创建数据表的SQL语句,可传入多条SQL语句,只需使用分号进行分隔,可能大家还注意到了下面这条语句:

$installer->getTable('weblog/blogpost')

这个getTable方法中传入的是模型URI,并获取表名。虽然这里可以直接写入表名,但为避免因他人修改配置文件中的表名而导致脚本无法运行,建议养成使用这个方法的好习惯。Mage_Core_Model_Resource_Setup类中包含许多像这样有用的helper方法,最好的学习方法是去研究Magento中core模块的安装脚本。

从1.6开始,Magento就开始支持MySQL以外的数据库,既然我们的安装脚本中包含原生SQL语句,那有可能在其它的数据库系统比如MSSQL中无法执行。这也是为什么我们在文件名中添加了mysql4-作为前缀。

为确保安装脚本保持在多个数据库系统中兼容,Magento提供了一个DDL(Data Definition Language)数据表对象,以下我们对脚本进行修改来保证脚本能在所有Magento支持的关系型数据库中运行。

依然编辑app/code/local/Alanhou/Weblog/sql/weblog_setup/mysql4-install-0.1.0.php文件:

$installer = $this;
$installer->startSetup();
$table = $installer->getConnection()->newTable($installer->getTable('weblog/blogpost'))
->addColumn('blogpost_id',Varien_Db_Ddl_Table::TYPE_INTEGER,null,array(
	'unsigned'=>true,
	'nullable'=>false,
	'primary'=>true,
	'identity'=>true,
	),'Blogpost ID')
->addColumn('title',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(
	'nullable'=>false,
	),'Blogpost Title')
->addColumn('post',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(
	'nullable'=>true,
	),'Blogpost Body')
->addColumn('date',Varien_Db_Ddl_Table::TYPE_DATETIME,null,array(
	),'Blogpost Date')
->addColumn('timestamp',Varien_Db_Ddl_Table::TYPE_TIMESTAMP,null,array(
	),'Timestamp')
->setComment('Alanhou weblog/blogpost entity table');
$installer->getConnection()->createTable($table);

$installer->endSetup();

可以看到这里我们没有使用原生SQL语句,使用以上DDL形式的脚本可以保证咱们的模块在与各种数据库保持兼容。

现在我们已经掌握了如何通过脚本来创建数据表,可是如果我们想要对已有模块的数据表结构进行修改又该怎么做呢?Magento的安装资源允许我们通过版本号来自动运行脚本升级模块。一旦在Magento中运行了安装脚本,除非删除core_resource表中对应的记录,否则这个脚本就再也不会被运行。通过创建类似于安装脚本的升级脚本,我们就可以进行适当的升级操作。

创建app/code/local/Alanhou/Weblog/sql/weblog_setup/upgrade-0.1.0-0.2.0.php文件并加入以下代码:

<?php
echo "Testing our upgrade script(upgrade-0.1.0-0.2.0.php) and halting execution to avoid updating the system version number
";
die();

升级脚本和安装脚本放在同一个文件夹内,文件名前面为upgrade,后面为升级前的版本号和升级后的版本后,中间用”-“来进行分隔。如果我们清除缓存重新访问页面这个脚本并不会运行,需要修改config.xml中的版本号都会运行升级脚本:

<modules>
	<Alanhou_Weblog>
		<version>0.2.0</version>
	</Alanhou_Weblog>
</modules>

在这里我们的升级脚本也可以命名为mysql4-upgrade-0.1.0-0.2.0.php,当然这就表明了脚本针对的是MySQL。

在完善升级脚本前,还要一件需要注意的事情,先在同一个文件夹中创建app/code/local/Alanhou/Weblog/sql/weblog_setup/upgrade-0.1.0-01.5.php并加入代码:

<?php
echo "Testing our upgrade script(upgrade-0.1.0-0.1.5.php) and NOT halting execution
";

 

重新访问页面会同时出现两条提示

Testing our upgrade script(upgrade-0.1.0-0.1.5.php) and NOT halting execution
Testing our upgrade script(upgrade-0.1.0-0.2.0.php) and halting execution to avoid updating the system version number

这是因为当Magento发现模块的版本号发生变时,它会运行所有比当前版本号高的脚本,虽然我们并没有在模块配置文件中添加0.1.5,但该脚本依然会被执行。脚本的执行顺序是按版本号从低到高,此时再查看core_resource会得到如下结果:

mysql> select * from core_resource where code = 'weblog_setup';
+--------------+---------+--------------+
| code         | version | data_version |
+--------------+---------+--------------+
| weblog_setup | 0.1.5   | 0.1.5        |
+--------------+---------+--------------+
1 row in set (0.00 sec)

可以看到版本号已经变成0.1.5了,这说明0.1.0到0.1.5的升级已经完成,并不是执行的0.1.0到0.2.0的升级。那么下面我们就来修改0.1.0-0.2.0的升级脚本:

<?php
 $installer = $this;
$installer->startSetup();
$installer->getConnection()
->changeColumn($installer->getTable('weblog/blogpost'),'post',
	'post',array(
		'type'=>Varien_Db_Ddl_Table::TYPE_TEXT,
		'nullable'=>false,
		'comment'=>'Blogpost Body'
		)
	);
$installer->endSetup();
die("You'll see why this is here in a second");

 

重新刷新页面发现升级脚本并没有执行,post列依然允许使用空值,更重要的是die()语句并没有使用程序跳出运行,到底发生了什么呢?我们来一起捋一下前面的操作:

  1. 起初weblog_setup资源的版本号是0.1.0
  2. 我们将模块升级到0.2.0
  3. Magento发现了升级模块,并且有两个升级脚本(0.1.0到0.1.5和0.1.0到0.2.0)
  4. Magento将两个脚本放到队列中运行
  5. Magento运行了0.1.0到0.1.5的脚本
  6. 现在weblog_setup资源的版本号是0.1.5
  7. 运行0.1.0到0.2.0升级脚本,执行被中断
  8. 在进行下一页面加载时,Magento发现weblog_setup的版本号是0.1.5且未发现可升级的脚本,因为两个脚本都是0.1.0的升级脚本

正确的做法是把脚本名称改做upgrade-0.1.0-0.1.5.php和upgrade-0.1.5-1.2.0.php

这样Magento就会一次加载中同时运行这两个脚本,可以通过更新core_resource表来重新进行升级:

UPDATE core_resource SET version = '0.1.0', data_version = '0.1.0' WHERE code = 'weblog_setup';

删除掉upgrade-0.1.5-0.2.0.php再重新加载页面,两个升级脚本就会同时运行,这时再去查看blog_posts表中的post列已经不允许为空了。

blog_posts表

所以在多人开发同一个Magento项目时就需要格外注意升级版本的问题,避免会出现错误。

 

Magento开发系列之五 模型和ORM基础

模型层级的应用在MVC框架中是一个非常重要的部分,它包含应用程序所需的数据。在Magento中,Model起的作用甚至还要更大,因为其中还包含业务逻辑,在其它的MVC框架中,业务逻辑通常是放在Controller或Helper方法中的。

注:在调试过程中如果没有出现我们所提到的报错,请开启报错,步骤如下:
a> 打开根目录下的index.php文件并取消如下注释:

#ini_set('display_errors', 1);

b> 打开.htaccess文件并在最后添加

SetEnv MAGE_IS_DEVELOPER_MODE “true”

MVC模型

首先我们来看看PHP传统的MVC模型,如果说MVC的定义不够清楚,那么模型的定义就更加模糊了。在PHP开发者大规模采用MVC框架前,数据是通过原生的SQL语句或SQL抽象层来获取的。这时开发者需要书写SQL语句并考虑为哪些对象建模。

如今很多人已不太愿意去书写原生SQL语句了,但很多PHP框架依然是在大量使用SQL。Model是提供抽象层的对象,但事实上程序员还是书写SQL语句或者通过抽象方法来读写数据以调用SQL。

也有一些框架避开SQL的使用而采用ORM(Object Relational Mapping)来完成相关操作。这种方法开发者通过对象设定属性,在调用对象中的save方法时,数据会被自动写入数据库。

我们再来看看Magento的模型,Magento是支持ORM的,同时Zend框架中的SQL抽象也可用。Magento中大多数的数据通过其内置的模型或自开发的模型来处理。Magento中Model的概念也是超级灵活、高度抽象的。

Magento中大多数的模型可以被划分为两类,一种是类似ActiveRecord的一个对象一张表的模型,另一种就是大名鼎鼎的EAV(Entity Attribute Value)模型。每个模型都有一个模型集合,集合(Collection)是用于包含一系列Magento模型实例的PHP对象。Magento开发团队在PHP标准库接口中加入了IteratorAggregate和Countable来让每个Model类型获取它自己的集合类型。如果你不太了解PHP标准库,可以把模型集合看作带有方法的数组。

Magento模型中没有代码去连接数据库,而是通过模型资源类来与数据库服务器进行交互(读或写adapter对象)。通过分离逻辑模型与数据库交互代码,理论上就可以在保持模型不变的情况下写一个新的资源类来操作不同类型的数据库。

首先我们要创建一个基本的Magento模型,按照传统我们来做一个weblog博客建模,步骤如下:

  1. 创建一个Weblog模块
  2. 为我们的Model创建一个数据表
  3. 在配置文件中添加一个Blogpost模型
  4. 在配置文件中为Blogpost模型添加模型资源信息
  5. 在配置文件中为Blogpost模型添加一个读适配器(Adapter)
  6. 在配置文件中为Blogpost模型添加一个写适配器
  7. 为Blogpost模型添加一个PHP类文件
  8. 为Blogpost资源模型添加一个PHP类文件
  9. 实例化该模型

那下面我们就按这个步骤来进行操作吧,通过前面的学习你应该对创建Weblog模块已经相当熟悉了,不过考虑到有些朋友没有从头开始看,这里再赘述一下,首先创建如下目录

app/code/local/Alanhou/Weblog/Block
app/code/local/Alanhou/Weblog/controllers
app/code/local/Alanhou/Weblog/etc
app/code/local/Alanhou/Weblog/Helper
app/code/local/Alanhou/Weblog/Model
app/code/local/Alanhou/Weblog/sql

创建app/code/local/Alanhou/Weblog/etc/config.xml文件并加入如下代码

<config>
	<modules>
		<Alanhou_Weblog>
			<version>0.1.0</version>
		</Alanhou_Weblog>
	</modules>
</config>

接着创建app/etc/modules/Alanhou_Weblog.xml文件并加入如下代码

<config>
	<modules>
		<Alanhou_Weblog>
			<active>true</active>
			<codePool>local</codePool>
		</Alanhou_Weblog>
	</modules>
</config>

执行以下步骤

  1. 清除缓存
  2. 在后台中,点击System > Configuration > Advanced > Advanced
  3. 在Disable modules output面板中查看Alanhou_Weblog是否出现

模块添加成功后我们来创建一个Action方法名为testModel的index控制器,首先在config.xml里添加router即加入如下代码:

	<frontend>
		<routers>
			<weblog>
				<use>standard</use>
				<args>
					<module>Alanhou_Weblog</module>
					<frontName>weblog</frontName>
				</args>
			</weblog>
		</routers>
	</frontend>

在app/code/local/Alanhou/Weblog/controllers/IndexController.php中加入如下代码:

<?php
class Alanhou_Weblog_IndexController extends Mage_Core_Controller_Front_Action{
	public function testModelAction(){
		echo 'Setup!';
	}
}

清除缓存并访问http://localhost/magento/weblog/index/testModel页面上会输出Setup!

Magento拥有一套可自动创建和修改数据库的系统,不过现在我们先手动为这个模型创建数据表,在phpMyAdmin或任何数据库客户端中进入数据库执行如下语句创建数据表blog_posts

CREATE TABLE <code>blog_posts</code> (
  <code>blogpost_id</code> int(11) NOT NULL auto_increment,
  <code>title</code> text,
  <code>post</code> text,
  <code>date</code> datetime default NULL,
  <code>timestamp</code> timestamp NOT NULL default CURRENT_TIMESTAMP,
  PRIMARY KEY  (<code>blogpost_id</code>)
)

接着执行如下语句手动插入一条数据

INSERT INTO <code>blog_posts</code> VALUES (1,'My New Title','This is a blog post','2010-08-08 00:00:00','2010-08-08 23:12:30');

blog_posts数据表

配置模型文件我们需要在配置文件中

  1. 在模块中开启Model
  2. 在模块中开启Model资源
  3. 为Model资源添加一个entity数据表配置

在实例化一个模型时需要进行类似下面这样的调用

$model = Mage::getModel('weblog/blogpost');

在传入的URI中前半部分是Model分组名(weblog),按照惯例它是我们模块名的小写形式,如果想更加保险些,可以加上命名空间,同样也需要使用小写;第二部分则是模块名称的小写形式(blogpost)。
在config.xml的config标签内添加如下代码

<global>
	<models>
		<weblog>
			<class>Alanhou_Weblog_Model</class>
			<resourceModel>weblog_resource</resourceModel>
		</weblog>
	</models>
</global>

外部的<weblog />标签是组名,和模块名相同,<class  />标签中内容为weblog分组中所有模型的基础名称,或称为类前缀。<resourceModel />标签指定weblog分组模型所使用的资源模型,还有更多的内容,但这里我们只要知道这个分组名接resource即可。

我们的操作还没完成,不过先清除缓存并通过如下代码尝试实例化blogpost模型:

public function testModelAction(){
	// echo 'Setup!';
	$blogpost = Mage::getModel('weblog/blogpost');
	echo get_class($blogpost);
}

重新加载页面就会出现类似下面的报错

Warning: include(Alanhou\Weblog\Model\Blogpost.php): failed to open stream: No such file or directory

可以看到这里尝试实例一个Mage_Weblog_Model_Blogpost类,Magento通过__autoload来包含这个模型,但无法找到文件,下面我们来创建app/code/local/Alanhou/Weblog/Model/Blogpost.php并加入如下代码:

<?php class Alanhou_Weblog_Model_Blogpost extends Mage_Core_Model_Abstract{
   	protected function _construct(){
   		$this->_init('weblog/blogpost');
	}
}

重新加载页面,提示信息应该会变成刚刚创建的类名,即Alanhou_Weblog_Model_Blogpost

Alanhou_Weblog_Model_Blogpost

所有与数据库交互的基础模型都继承Mage_Core_Model_Abstract类,这个抽象类会强制应用一个_construct方法(注意:这不是构造方法__construct);该方法会调用与Mage::getModel调用URI相符类的_init方法。

上面我们创建好了模型,下一步就是建立模型资源,模型资源中的代码将会操作数据库,本例中我们在配置文件中包含了

<resourceModel>weblog_resource</resourceModel>

<resourceModel />中的值会被用来实例化Model资源类,虽然这里没有调用该类,但任何weblog分组中的模型要和数据库进行交互时,都会调用如下方法来获取模型资源

Mage::getResourceModel('weblog/blogpost');

再强调一下,weblog是分组名,blogpost是模型名称,Mage::getResourceModel方法会使用weblog/blogpost来查看全局配置并获取<resourceModel>标签中的值(本例中即为weblog_resource)。接下来模型类会实例化如下URI:

weblog_resource/blogpost

因而,资源模型和常规模型一样都在config.xml中同处配置,因此,我们在config.xml中进行如下修改

<global>
    <!-- ... -->
    <models>
        <!-- ... -->
        <weblog_resource>
            <class>Alanhou_Weblog_Model_Resource</class>
        </weblog_resource>
    </models>
</global>

这里我们从resourceModel标签中获取了值weblog_resource并作为标签添加到配置文件中,class标签中的值通过如下格式获取:

Packagename_Modulename_Model_Resource

下面我们修改testModel方法并重置加载页面

public function testModelAction(){
	$params = $this->getRequest()->getParams();
	$blogpost = Mage::getModel('weblog/blogpost');
	echo ("Loading the blogpost with an ID of ".$params['id']);
	$blogpost->load($params['id']);
	$data = $blogpost->getData();
	var_dump($data);
}

加载http://localhost/magento/weblog/index/testModel/id/1页面,会出现类似如下报错:

Warning: include(Alanhou\Weblog\Model\Resource\Blogpost.php): failed to open stream: No such file or directory

因此我们需要为模型添加一个资源类,每个模型都有自己的资源类,下面我就创建app/code/local/Alanhou/Weblog/Model/Resource/Blogpost.php文件并加入如下代码:

<?php class Alanhou_Weblog_Model_Resource_Blogpost extends Mage_Core_Model_Resource_Db_Abstract{
 	protected function _construct(){
 		$this->_init('weblog/blogpost','blogpost_id');
	}
}

同样的_init方法中前半段即个URL用于指定模型,而后半部分可以是任意可唯一标识数据表列的字段,大多数情况下我们会使用主键,刷新缓存,重新加载页面,会出现如下报错:

Can't retrieve entity config: weblog/blogpost

在为weblog/blogpost建模时,我们在告诉Magento模块分组是weblog,Entity是blogpost,例中继承了Mage_Core_Model_Resource_Db_Abstract,因而entity对应的是一张数据表,我们前面创建的数据表名为blog_post,所以要对config.xml进行如下修改来添加entity(models标签内):

<weblog_resource>
	<class>Alanhou_Weblog_Model_Resource</class>
	<entities>
		<blogpost>

<table>blog_posts</table>

		</blogpost>
	</entities>
</weblog_resource>

这样就成功添加了<entities />代码段也就为模型指定了数据表,清除缓存重新加载页面,如果一切正常的话将会在浏览器中输出如下数组:

Loading the blogpost with an ID of 1
array(5) {
["blogpost_id"]=> string(1) "1"
["title"]=> string(12) "My New Title"
["post"]=> string(19) "This is a blog post"
["date"]=> string(19) "2010-08-08 00:00:00"
["timestamp"]=> string(19) "2010-08-08 23:12:30" }

这样就成功的从数据库中读取了数据并完成了Model的配置。

Magento中所有的模型都继承自Varien_Object类,该类是Magento系统库的一部分,但不属于任何core模块,可以在lib/Varien/Object.php中找到这个对象。模型将数据存储在一个类型为protected的_data数组中,Varien_object类提供了一些可获取数据的方法,比如前面用到的getData()将返回一个带有键值对的数组,也可以传一个键值来获取指定字段:

$model->getData();
$model->getData('title');

该类中还有一个getOrigData方法,它将返回对象起初被实例化时的模型数据:

$model->getOrigData();
$model->getOrigData('title');

Vairen_Object还通过PHP的魔术方法__call组成了一些特殊方法,可以通过以get, set, unset或has开头的方法来get, set, unset或检查一个属性是否存在,这些关键词后采用驼峰法即首字母大写:

$model->getBlogpostId();
$model->setBlogpostId(25);
$model->unsetBlogpostId();
if($model->hasBlogpostId()){...}

因此,建议所有数据表中的列名采用小写或下划线分隔单词的方式命名。

Magento中的增删改查

一提到数据库免不了会用到增删改查(英文称为CRUD,即Create, Read, Update, Delete),这些都是数据库中的基本功能,Magento的模型通过load, save和delete方法来实现这些功能。前面我们已经用到了load方法,在传入一个参数时,load方法会返回模型资源里指定列与传入值相匹配的记录。

$blogpost->load(1);

save方法可以对数据库进行INSERT插入新记录和UPDATE更新已有记录的操作,在我们的Index控制器添加如下方法:

public function createNewPostAction(){
	$blogpost = Mage::getModel('weblog/blogpost');
	$blogpost->setTitle('Code Post!');
	$blogpost->setPost('This post was created from code!');
	$blogpost->save();
	echo 'post with ID '.$blogpost->getId().' created';
}

访问http://localhost/magento/weblog/index/createNewPost页面,些时在数据表中就会新增一行内容
blog_post_insert

我们同样在Index控制器中再添加一个方法用于编辑数据表内容:

public function editFirstPostAction(){
	$blogpost = Mage::getModel('weblog/blogpost');
	$blogpost->load(1);
	$blogpost->setTitle("The First Post");
	$blogpost->save();
	echo 'post edited';
}

访问http://localhost/magento/weblog/index/editFirstPost完成对数据表的修改

通过代码领回数据表

同样地我们也可以创建一个方法来删除行:

public function deleteFirstPostAction(){
	$blogpost = Mage::getModel('weblog/blogpost');
	$blogpost->load(1);
	$blogpost->delete();
	echo 'post removed';
}

访问http://localhost/magento/weblog/index/deleteFirstPost即可完成对行的删除。

一个模型固然非常有用,但有时我们想要获取一系列的模型,每个模型类型都有一个特殊的集合对象,这些对象采用PHP的IteratorAggregate和Countable接口,也就是说可以传递给count函数并在foreach函数中使用。

关于集合我们会在后面进行全面的探讨,这里先讨论一下基本设置和使用。在Index控制器中添加如下方法

public function showAllBlogPostsAction(){
	$posts = Mage::getModel('weblog/blogpost')->getCollection();
	foreach($posts as $blogpost){
		echo '<h3>'.$blogpost->getTitle().'</h3>';
		echo nl2br($blogpost->getPost());
	}
}

访问http://localhost/magento/weblog/index/showAllBlogPosts页面,会提示如下报错:

Warning: include(Alanhou\Weblog\Model\Resource\Blogpost\Collection.php): failed to open stream: No such file or directory

这表示我们还需要添加一个类文件来定义Blogpost集合,每个模型都有一个protected类型的属性_resourceCollectionName,它包含一个用于标识集合的URI

protected '_resourceCollectionName' => string 'weblog/blogpost_collection'

默认情况下它与我们用于标识资源模型的URI相同,只是在最后面添加一段_collection。Magento把Collection看作资源的一部分,所以以上URI组合成如下类名

Alanhou_Weblog_Model_Resource_Blogpost_Collection

接下来在app/code/local/Alanhou/Weblog/Model/Resource/Blogpost/Collection.php中添加如下类

<?php class Alanhou_Weblog_Model_Resource_Blogpost_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract{
  	protected function _construct(){
  		$this->_init('weblog/blogpost');
	}
}

和其它类一样,我们需要使用传入weblog.blogpost来init我们的集合。重新访问http://localhost/magento/weblog/index/showAllBlogPosts就会得到文章信息。

通过代码遍历出post内容

Magento开发系列之四 布局、块和模板

刚刚接触Magento开发的人经常会搞不清布局(Layout)和视图(View)之间的分别。那么本节就来剖析Magento的布局/块,以及它们在Magento的MVC架构中所起到的作用。

和很多主流的MVC系统不同,Magento的Action控制器不向视图传递数据对象,并且通常也不会去设定视图对象中的属性值。而是由视图组件直接引用系统模型来获取所需显示的信息。

采用这种设计方法直接导致视图被分割成两个部分:块文件(Blocks)和模板文件(Templates),其中块是PHP对象,模板是混合了HTML和PHP的原生PHP文件,后缀名采用.phtml。每个块文件对应着一个模板文件,在模块文件中通过$this关键词来引用块文件中的对象。

先看看代码,打开app/design/frontend/base/default/template/catalog/product/list.phtml文件,会看到如下代码

<?php     $_productCollection=$this->getLoadedProductCollection();
    $_helper = $this->helper('catalog/output');
?>
<?php if(!$_productCollection->count()): ?>

<?php echo $this->__('There are no products matching the selection.') ?>

<?php else: ?>

其中getLoadedProductCollection方法来自块文件的Mage_Catalog_Block_Product_List类,具体位置在app/code/core/Mage/Catalog/Block/Product/List.php

public function getLoadedProductCollection()
    {
        return $this->_getProductCollection();
    }

块文件中的_getProductCollection会实例化模型并读取其中的数据,并将结果返回给模板文件。

块文件内置

块和模板文件真正的强大之处在于getChildHtml方法的使用,使用这个方法可以包含一级块、模板文件内的二级块、模板文件。页面上的HTML布局正是通过块文件不断地调用内嵌的块文件来创建的。下面我们就来看一看app/design/frontend/base/default/template/page/1column.phtml这个文件:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<head>
<?php echo $this->getChildHtml('head') ?>
</head>
<body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
<?php echo $this->getChildHtml('after_body_start') ?>

<div class="wrapper">
    <?php echo $this->getChildHtml('global_notices') ?>

<div class="page">
        <?php echo $this->getChildHtml('header') ?>

<div class="main-container col1-layout">

<div class="main">
                <?php echo $this->getChildHtml('breadcrumbs') ?>

<div class="col-main">
                    <?php echo $this->getChildHtml('global_messages') ?>
                    <?php echo $this->getChildHtml('content') ?>
                </div>

            </div>

        </div>

        <?php echo $this->getChildHtml('footer') ?>
        <?php echo $this->getChildHtml('global_cookie_notice') ?>
        <?php echo $this->getChildHtml('before_body_end') ?>
    </div>

</div>

<?php echo $this->getAbsoluteFooter() ?>
</body>
</html>

模板文件本身并不长,但通过多次调用$this->getChildHtml()方法包含并显示了多个块文件,这些块文件又会使用getChildHtml方法去调用其它块文件。

布局文件

这么看起来块文件和模板文件确实不错,那么如何让Magento知道在页面中使用哪一个块文件?先从哪一个块文件开始处理?又如何在getChildHtml中指定一个块文件呢?毕竟里面的参数看起来不太像是个块文件的名称。

这就需要讲到布局对象了,布局对象是一个指定了页面上将包含块文件的XML对象,它同时也指定了先处理哪一个块文件。在上节中我们在Action方法中使用了echo指令输出了一些内容,本节我们将会为Helloworld模块创建一个简单的HTML模板文件。首先创建一个app/design/frontend/base/default/layout/local.xml文件,加入如下代码

<layout version='0.1.0'>
	<default>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml" />
	</default>
</layout>

然后再创建一个app/design/frontend/base/default/template/alanhou/helloworld/simple_page.phtml文件并添加如下代码:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<title>Hello World</title>

<style type="text/css">
		body{
			background-color:#f00;
		}
	</style>

</head>
<body>

</body>
</html>

完成了上面的操作后,最后需要通过Action控制器来调用整个布局,那么我们需要对之前创建的indexAction方法进行如下调整:

public function indexAction(){
		//echo 'Hello World!';
		$this->loadLayout();
		$this->renderLayout();
}

清除缓存重新加载http://localhost/mgt/helloworld/index/index,就会显示刚刚在simple_page.phtml里所添加的内容,也就是整个屏幕都显示红色的背景。

你一定很好奇这背后到底发生了什么呢?下面就让我们一起来揭开这神秘的面纱。为方便观察首先安装一下Layoutviewer模块:

链接: http://pan.baidu.com/s/1qHTd8 密码: 2dya

只需解压缩然后拷贝到app目录中即可完成安装,这个模块类似于我们这前面小节里提到的Configviewer。安装完成后请访问http://localhost/magento/helloworld/index/index?showLayout=page

Magento Layout文件

输出内容即为page请求的layout.xml,它由<block />, <reference /> 和<remove />等标签组成,当我们在调用loadLayout方法时,Magento实际完成的操作有:

  1. 生成Layout布局XML文件
  2. 实例化各<block />标签中的Block类,通过标签属性查找类并作为全局配置路径保存到布局对象的_blocks数组中,在该数组中以标签名作为数据的键
  3. 如若标签中包含输出属性,其值就会被添加到布局对象的_output数组中

然后在Action控制器中调用renderLayout方法时,Magento会遍历_output数组中的所有Block,将输出属性的值作为一个回调方法,这通常都是toHtml,也就是从块模板文件开始处理。接下我们会谈谈Block是怎么被实例化的,layout文件又是如何生成并完成输出的?

Block的实例化

在生成的布局XML文件中,包含type属性的实际上是一个类群URI,比如

<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml"/>
<block type="page/template_links" name="top.links" as="topLinks"/>

这些URI都引用全局配置文件中的一个地方,URI的前半部分(上例中都是page)会用于在全局配置文件中查询page的类名,而第二部分(分别是html和template_links)会被加到前面查询到的类名最后并形成Magento将要实例化的类。

以page/html为例,Magento会首先去依次查找全局配置文件中的global > blocks > page节点(参见Magento开发系列之二 配置文件)

Magento页面块

查找到的结果即是上图中的

<page>
<class>Mage_Page_Block</class>
</page>

这里我们就得到了类名的前一部分Mage_Page_Block,然后将第二部分添加到后面就构成了将要实例化的类名Mage_Page_Block_Html。假如我们创建了一个和现有block同名的块文件,新的block会替换原来的实例,我们在前面创建的local.xml就做了这样的替换

<layout version='0.1.0'>
	<default>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml" />
	</default>
</layout>

名为root的block就被我们的创建的block所替换,指向了一个新phtml模板文件。

Reference标签

标签会将会将内部包含的所有XML声明以指定的名称放到现在的block中,它内部包含的节点会被作为所引用的父级block的子block

<layout version=“0.1.0″>
    <default>
        <block type=“page/html” name=“root” output=“toHtml” template=“page/2columns-left.phtml”>
            <!– … sub blocks … –>
        </block>
    </default>
</layout>

放在不同的layout文件中:

<layout version=“0.1.0″>
    <default>
        <reference name=“root”>
            <!– … another sub block … –>
            <block type=“page/someothertype” name=“some.other.block.name” template=“path/to/some/other/template” />
        </reference>
    </default>
</layout>

即便root block是在一个单独的布局XML文件中声明,新添加的block还是作为一个子block。Magento先创建一个名为root的page/html块,之后如果再遇到有对root的引用(reference),就会将新的块some.other.block.name作为root的子block。

Layout文件的生成过程

现在我们对于Layout XML文件已经有了更进一步的了解,但这个XML文件是从哪里来的呢?要解释这个问题就需要引入两个新的概念:指针(Handle)和安装包布局(Package Layout)。

Magento中的每个页面请求都会生成一些指针,我们前面创建的Layoutview模块将有助于形象化的展示这些指针,只需要访问http://localhost/magento/helloworld/index/index?showLayout=handles就会看到类似下面的结果(根据个人配置会略有差别)

Handles For This Request

1. default
2. STORE_default
3. THEME_frontend_rwd_default
4. helloworld_index_index
5. customer_logged_out

以上输出的每一个都是指针,指针可以在Magento系统中的很多地方进行设置,这里我们着重讨论的是default和helloworld_index_index这两个。default指针在每个Magento的请求中都会出现,而helloworld_index_index是拼接了Router名称(helloworld)、Action控制器名称(index)和Action控制器方法名(index)所形成的一个字符串。这也就是说Action控制器中的每个方法都会对应一个指针。

前面我们也讲过在Magento中Action控制器及方法的默认值都是index,所以访问http://localhost/magento/helloworld/?showLayout=handles会输出同样的内容。

那么Package Layout又是什么东东呢?它有点类似于全局配置文件,具体体现为一个包含所有可能出现的布局配置的一个大型XML文件,依然用到Layoutviewer这个模块,这次我们访问http://localhost/magento/helloworld/index/index?showLayout=package,浏览器中会输出一长段XML,这就是安装包布局文件,这个XML文件是通过将当前主题(或称为安装包)中所有的XML布局文件进行组合而得。对于default主题,这些布局文件在app/design/frontend/base/default/layout/中

全局文件中的<frontend><layout><updates /> 和<adminhtml><layout><updates /> 代码段会包含不同区域需要加载的所有文件的节点,配置文件将所有的文件拼接完之后,Magento会添加最后一个xml文件,即local.xml。因而我们可以在这个文件中添加一些自定义代码。

安装包布局文件中会包含一些我们已然熟知的标签,如<block />和<reference />,但都会出现在<default />和<catalogsearch_advanced_index />等标签内。这些标签是指针标签,某一请求的布局文件是通过抓取匹配请求中各指针安装包布局中的各个代码段来生成的。所以上例中,我们的布局文件是由抓取以下代码段中的标签生成的:

1. default
2. STORE_default
3. THEME_frontend_rwd_default
4. helloworld_index_index
5. customer_logged_out

在安装包布局文件中还有另一个需要关注的标签,标签可以包含其它指针标签,比如

<customer_account_index>
    <!– … –>
    <update handle=“customer_account”/>
    <!– … –>
</customer_account_index>

上面代码的意思是在请求中包含customer_account_index指针时,应包含<customer_account />指针中的<block />

具体实践

讲了这么多理论,还是回到前面的例子:

<layout version='0.1.0'>
	<default>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml" />
	</default>
</layout>

这段代码就是我们所添加的local.xml文件,它的主要作用是用一个新的Block来重载root标签。通过将这个block放到指针中将使得系统中的任何一次页面请求都会进行这一重载,这显然不是我们想要的。

访问Magento站点中的任意一页,要么返回一个空白页,要么会显示红色的背景页,那下面我们就对local.xml文件进行修改,让我们的代码只对hello world页面生效。你可能已经猜到,只需要修改指针名称为helloworld_index_index即可

<layout version='0.1.0'>
	<helloworld_index_index>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml" />
	</helloworld_index_index>
</layout>

清除缓存,再重新加载一下其它页面会发现都已恢复正常。现在这段代码只对index这个Action方法有效,如果我们想要对goodye方法也生效的话,就需修改修改该方法

public function goodbyeAction(){
	// echo 'Goodbye World!';
	$this->loadLayout();
	$this->renderLayout();
}

可是这时如何你访问http://localhost/magento/helloworld/index/goodbye会发现并没有什么变化,这是因为我们还没添加一个包含完整action名称(helloworld_index_goodbye)的指针,让我们使用update标签来将local.xml修改为

<layout version='0.1.0'>
	<helloworld_index_index>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml" />
	</helloworld_index_index>
	<helloworld_index_goodbye>
		<update handle="helloworld_index_index" />
	</helloworld_index_goodbye>
</layout>

这时清除缓存再加载一下页面会出现同样的红色背景页面。

输出和getChildHtml

标准配置中,输出从名为root的Block开始(因为它包含一个输出属性),在本例中我们使用如下代码对root进行了重载

template="alanhou/helloworld/simple_page.phtml"

模板引用自当前主题的根文件夹,这里为app/design/frontend/base/default,所以我们需要深入分析一下我们的自定义页面,大多数Magento的模板文件都存储在app/design/frontend/base/default/template中,结合这一段得到如下路径

app/design/frontend/base/default/templates/alanhou/helloworld/simple_page.phtml

仅有红色背景页面不免显得过于单调,下面我们就为页面添加更多的内容,修改local.xml中对应的指针如下

<helloworld_index_index>
	<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml">
		<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml" />
	</block>
</helloworld_index_index>

我们在root中又内嵌了一个Block,这是Magento所自带的用户注册表单,通过这一修改,内嵌Block就可以在simple_page.phtml中通过getChildHtml方法来获取到了。在simple_page.phtml的body标签内加入相关内容:

<body>
	<?php echo $this->getChildHtml('customer_form_register'); ?>
</body>

清除缓存再重新访问http://localhost/magento/helloworld/index/index页面就会在红色背景上输出注册表单了

用户注册表单

Magento还有一个名为top.links的Block,我们在simple_page.phtml进行添加

<body>

<h1>Links</h1>

	<?php echo $this->getChildHtml('top.links'); ?>
</body>

重新加载页面,会发现只有<h1>Links</h1>进行了输出,但是top.links却没有输出。这是因为我们并没有在local.xml进行添加,getChildHtml方法只能包含在布局文件指定为子Block的块。通过这样做可以在只实例化所需用到的Block,开发者就可以根据不同应用场景为Block设置不同的模板文件。

那么我们在local.xml中添加top.links

<helloworld_index_index>
		<block type="page/html" name="root" output="toHtml" template="alanhou/helloworld/simple_page.phtml">
			<block type="page/template_links" name="top.links" />
			<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml" />
		 </block>
	</helloworld_index_index>

再重新载入页面就会出现top.links模块了

添加登录链接

在本节的最后还需要再介绍一个重要的概念,<action />标签。使用<action />标签允许我们调用block类中公有方法,因此我们可以不用替换root block的模板,而只通过调用setTemplate方法就可以了,下面我们将local.xml的内容修改如下:

<layout version="0.1.0">
	<helloworld_index_index>
		<reference name="root">
			<action method="setTemplate">
				<template>alanhou/helloworld/simple_page.phtml</template>
			</action>
			<block type="page/template_links" name="top.links" />
			<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml" />
		</reference>
	</helloworld_index_index>
</layout>

以上XML布局文件会先设置模板属性为root block,然后添加两个子block。清除缓存重新访问http://localhost/magento/helloworld/index/index会得到一模一样的结果。使用<action />的好处在于使用之前创建的同一个block实际不会影响到其它的父子block关联,

action方法的变量需要内置在单独的标签子节点中,节点的名称影响不大,但顺序非常重要。上例中我们完全可以将action段代码写成下面这样而丝毫不影响输出

<action method=“setTemplate”>
    <some_new_template>alanhou/helloworld/simple_page.phtml</some_new_template>
</action>

这也说明action的变量节点名完全可以自由定义。

Magento开发系列之三 控制器

MVC结构可以追溯到Smalltalk编程语言和Xerox Parc时代,从那时起很多系统都自称采用了MVC结构。这些系统又都与其它有着些许差别,不过总体而言都会将获取数据、业务逻辑和用户界面的代码进行分离。PHP中常见的MVC框架如下图所示:

常见PHP的MVC结构

  1. 一个称为前台控制器的PHP文件获取到URL
  2. 该PHP文件检测URL并获取控制器名和Action名(这一步常被称为routing)
  3. 实例化所获取到的控制器
  4. 与获取的Action名相匹配的方法将会被调用
  5. 这个Action方法会根据请求的变量实例化/调用model中的方法
  6. 该Action方法准备好数据结构然后传送给view
  7. 视图根据从控制器那里获取的数据结构生成HTML

这种模式显然比早期的一个页面对应一个PHP文件要先进了很多,但软件工程师们在工作中会经常抱怨:

  • 前台控制器在全局命名空间中工作
  • 配置的规则只能实现少量的模块化
  • URL routing不太灵活
  • 即便系统允许重载默认方法,如果不进行大规模重构,这种模式会很难添加新的模型、视图或控制器。

基于此,Magento团队创建了更为抽象的MVC模式

Magento的MVC结构

  1. 由一个PHP文件获取到URL
  2. 该PHP文件实例化一个Magento应用
  3. Magento应用实例化一个前台控制器对象
  4. 前台控制器实例化一到多个Router对象(通过全局配置文件指定)
  5. Router检测请求的URL是否匹配
  6. 如果发现到匹配,会获取到Action控制器和Action
  7. 实例化Action控制器并调用与Action名称相匹配的方法
  8. Action方法根据请求实例化模型及调用方法
  9. 该Action控制器实例化Layout对象
  10. Layout对象根据请求对象和系统属性(也称作指针)创建一系列Block对象
  11. Layout还会在某些Block对象(启动内层渲染)上调用输出方法
  12. 每个Block都有一个对应的模板文件,Block中包含PHP逻辑,模板中包含HTML和PHP输出代码
  13. Block从模型中调取数据,也就是说Action控制器并不会传输数据结构。

我们会逐步揭开层层面纱,不过本节我们主要讨论前台控制器 > Router > Action控制器几部分。

这么多理论,想必大家多年的失眠症都已经治愈了…(⊙_⊙;)… 还是老规矩,下面用实例来进行说明,主要有以下几步:

  1. 创建一个Hello World模块
  2. 使用Router配置该模块
  3. 为Router创建Action控制器

首先为Helloworld模块创建目录:

app/code/local/Alanhou/Helloworld/Block
app/code/local/Alanhou/Helloworld/controllers
app/code/local/Alanhou/Helloworld/etc
app/code/local/Alanhou/Helloworld/Helper
app/code/local/Alanhou/Helloworld/Model
app/code/local/Alanhou/Helloworld/sql

到这里大家估计已经是轻车熟路了,先创建app/code/local/Alanhou/Helloworld/etc/config.xml并加入如下代码:

<config>
	<modules>
		<Alanhou_Helloworld>
			<version>0.1.0</version>
		</Alanhou_Helloworld>
	</modules>
</config>

紧接着创建app/etc/modules/Alanhou_Helloworld.xml文件来激活这个模块,加入如下代码:

<config>
	<modules>
		<Alanhou_Helloworld>
			<active>true</active>
			<codePool>local</codePool>
		</Alanhou_Helloworld>
	</modules>
</config>

使用老方法确认一下模块是否配置成功

过上面基本的框架已经搭建好了,执行以下步骤

  1. 清除缓存
  2. 在后台中,点击System > Configuration > Advanced > Advanced
  3. 在Disable modules output面板中查看Alanhou_Helloworld是否出现

Alanhou_Helloworld模块

 

完成了模块的创建,下面我们就要进行Router的配置了,Router会将URL解释为Action控制器和方法,和传统PHP中的MVC系统不同,在Magento中需求明确地在全部配置中定义Router,打开config.xml,添加一段代码,修改为

<config>
	<modules>
		<Alanhou_Helloworld>
			<version>0.1.0</version>
		</Alanhou_Helloworld>
	</modules>
	<frontend>
		<routers>
			<helloworld>
				<use>standard</use>
				<args>
					<module>Alanhou_Helloworld</module>
					<frontName>helloworld</frontName>
				</args>
			</helloworld>
		</routers>
	</frontend>
</config>

标签起来越越多的,让我们来逐一介绍

<frontend>是做什么的?

<frontend>代表Magento中的区域,可以把区域看作一个Magento应用,frontend区域就是面向客户的前台,也称为shopping cart应用。admin区域是管理后台应用而install区域是在第一次安装Magento时用到的应用。

不论是一个Router还是多用到的标签都是复数的<routers>

<frontName>又是做什么的?

在Router解析URL时,会进行如下的分离

http://example.com/frontName/actionControllerName/actionMethod/

所以通过将<frontName>定义为helloworld,我们实际上是在告诉Magento系统对应的URL是http://example.com/helloworld/*

很多开发者在刚开始的时候容易把frontName和前台控制器对象搞混,实际上frontName只和routing有关

<helloworld>标签有什么作用?
这个标签是模块名称的小写形式,这个模块名称是Helloworld,所以标签名就是helloworld,这实际上定义的是Router名称。

你可能发现frontName和模块名也是相同的,这是一种习惯,但并非强制要求。在自己写的模块中,拼接模块名和命名空间也许更好,这样可以避免一些不必要的名称重复。

<module>Alanhou_Helloworld</module>有什么作用?

这个module标签需要是模块的全称,包含命名空间,系统通过这个标签来定义控制器文件

接下来我们就进入最后一步,为我们的Router创建Action控制器。首先创建一个文件app/code/local/Alanhou/Helloworld/controllers/IndexController.php并添加如下代码:

<php
class Alanhou_Helloworld_IndexController extends Mage_Core_Controller_Front_Action{
	public function indexAction(){
		echo 'Hello World!';
	}
}

清除缓存,然后在浏览器中访问http://localhost/magento/helloworld/index/index页面,就会发现屏幕上输出了Hello World!

 

Action控制器放在模块内controllers的文件夹内,其名称首先取自config.xml,本例中config.xml中有一段

后面紧接着Action控制器的名称(Alanhou_Helloworld_Index),再在最后添加Controller,这样就构成了Alanhou_Helloworld_IndexController。所有的Action控制器都继承Mage_Core_Controller_Front_Action。

那么URL中的index/index又代表什么意思呢?前面我们提到过Magento的URL是按照下面这样解析的

http://example.com/frontName/actionControllerName/actionMethod/

http://example.com/frontName/actionControllerName/actionMethod/

假设我们访问http://example.com/checkout/cart/add,Magento会去全局配置文件中查找Mage_Checkout模块,名称为Mage_Checkout_CartController的Action控制器以及该控制器中的addAction方法

下面使用非默认的名称来创建一个方法,在IndexController.php中添加如下方法:

public function goodbyeAction(){
		echo 'Goodbye World!';
	}

这时访问http://localhost/magento/helloworld/index/goodbye就会输出Goodbye World!

由于这里我们继承了Mage_Core_Controller_Front_Action类,就会自然地获取一些该类中的方法,比如URL中多余的元素会被自动解析为键值对,在Action控制器中添加如下方法:

public function paramsAction(){
	echo '

<dl>';
	foreach($this->getRequest()->getParams() as $key=>$value){
		echo '

<dt><strong>Param: </strong>'.$key.'</dt>


';
		echo '

<dt><strong>Value: </strong>'.$value.'</dt>


';
	}
	echo '</dl>


';
}

然后访问http://localhost/magento/helloworld/index/params?foo=bar&baz=eof就会输出如下结果

Magento键值对

最后我们再来创建一个响应http://localhost/magento/helloworld/messages/goodbye的文件,所以我们需要创建一个app/code/local/Alanhou/Helloworld/controllers/MessagesController.php文件。在其出添加一个名为Alanhou_Helloworld_MessagesController的Action控制器并添加一个goodbyeAction方法,具体代码如下:

<?php
class Alanhou_Helloworld_MessagesController extends Mage_Core_Controller_Front_Action{
	public function goodbyeAction(){
		echo 'Another Goodbye!';
	}
}

访问http://localhost/magento/helloworld/messages/goodbye就会输出Another Goodbye!

以上就是如何在Magento添加MVC中的Controller,虽然比常见的MVC框架要复杂一些,但是具有超强的灵活性。

Magento开发系列之二 配置文件

config文件可以说是Magento的发动机,它从整体上包含了几乎一切模块、模型、类、模板文件等。很多PHP开发者不太适应这种高度的抽象,在开发中也给他们带来了不少的困扰和麻烦,但一旦掌握这种对系统文件的重载机制将带来空前的灵活性。

本节我们将创建一个用于在浏览器中查看系统配置的模块,请尝试跟着我们一起做,这样有助于你更进一步的了解Magento以及主要的名称和用法。

构建模块目录结构

这里我们会创建一个Magento模块,模块由一组php和xml文件组成,用于继承系统类添加新功能或重载系统方法。比如可以是为追踪销售信息添加新的数据模型、修改已有类的方法或添加全新的功能等等。

值得一提的是Magento系统中通常都是基于同一个模块,如果查看app/code/core/Mage的话,会发现里面包含很多的文件夹,这些都是由Magento团队所创建的模块,也正是默认情况下前台所用到的所有模块。

对于自建的模块通常会放在app/code/local/Packagename内,其中Packagename作为唯一标识你所开发代码的命名空间。作为一个不成文的规定,通常建议使用公司的名称,比如app/code/local/Google,在本系列的例子中我们将使用Alanhou,并使用Configviewer作为模块名称,所以首先请创建如下目录结构:

app/code/local/Alanhou/Configviewer/Block
app/code/local/Alanhou/Configviewer/controllers
app/code/local/Alanhou/Configviewer/etc
app/code/local/Alanhou/Configviewer/Helper
app/code/local/Alanhou/Configviewer/Model
app/code/local/Alanhou/Configviewer/sql

我们在本节的模块中不会用到所有这些文件夹,但创建这些文件夹有助于对整个模块结构的了解。下一步我们会创建两个文件

app/code/local/Alanhou/Configviewer/etc/config.xml
app/etc/modules/Alanhou_Configviewer.xml

第二个文件按照Packagename_Modulename.xml的规则来进行命名,在config.xml文件中放入如下代码:

<config>
    <modules>
        <Alanhou_Configviewer>
            <version>0.1.0</version>
        </Alanhou_Configviewer>
    </modules>
</config>

然后在Alanhou_Configviewer.xml中添加如下代码

<config>
    <modules>
        <Alanhou_Configviewer>
            <active>true</active>
            <codePool>local</codePool>
        </Alanhou_Configviewer>
    </modules>
</config>

如果你现在对上面的代码还是一头雾水的话,大可不必担心,后面会有详细的介绍。通过上面基本的框架已经搭建好了,执行以下步骤

  1. 清除缓存
  2. 在后台中,点击System > Configuration > Advanced > Advanced
  3. 在Disable modules output面板中查看Alanhou_Configviewer是否出现

如果看到Alanhou_Configviewer,那么恭喜你已经成功创建第一个模块。

Alanhou_Configviewer

创建模块配置文件

你可能已经看到胜利的曙光了,不过截至目前这个模块还没有什么实质意义。这一模块我们计划完成的工作有

  1. 检查showConfig变量字段是否存在
  2. 如果showConfig存在,显示Magento的配置并中断其它的执行
  3. 检测是否存在其它的字符变量,showConfigFormat可以指定以纯文本或xml进行输出

首先我们将在config.xml文件中添加<global>代码块

<config>
	<modules>
		<Alanhou_Configviewer>
			<version>0.1.0</version>
		</Alanhou_Configviewer>
	</modules>
	<global>
		<events>
			<controller_front_init_routers>
				<observers>
					<Alanhou_configviewer_model_observer>
						<type>singleton</type>
						<class>Alanhou_Configviewer_Model_Observer</class>
						<method>checkForConfigRequest</method>
					</Alanhou_configviewer_model_observer>
				</observers>
			</controller_front_init_routers>
		</events>
	</global>
</config>

然后创建一个app/code/local/Alanhou/Configviewer/Model/Observer.php文件,加入如下代码:

<?php
    class Alanhou_Configviewer_Model_Observer{
        const FLAG_SHOW_CONFIG	 = 'showConfig';
        const FLAG_SHOW_CONFIG_FORMAT = 'showConfigFormat'; 	
	private $request;
 	public function checkForConfigRequest($observer){ 
	    $this->request = $observer->getEvent()->getData('front')->getRequest();
            if($this->request->{self::FLAG_SHOW_CONFIG}==='true'){
	        $this->setHeader();
		$this->outputConfig();
	    }
        }

	private function setHeader(){
	    $format = isset($this->request->{self::FLAG_SHOW_CONFIG_FORMAT}) ? $this->request->{self::FLAG_SHOW_CONFIG_FORMAT} : 'xml';
	    switch($format){
		case 'text':
		    header("Content-Type: text/plain");
		    break;
		default:
		    header("Content-Type: text/xml");
		}
	}

	private function outputConfig(){
	    die(Mage::app()->getConfig()->getNode()->asXML());
	}
    }

然后访问http://localhost/magento/?showConfig=true,就会出现像下面这样的一大串XML代码

Magento系统配置文件

代码中主要包含Magento的状态,其中列出了所有的模块、模型、类、事件监听器等内容。

例如在如上的XML输出中搜索Alanhou_Configviewer_Model_Observer就可以找到对应的类。Magento中所有模块的config.xml文件都会被解析并包含到全局设置中。

那这些有什么实际意义呢,你现在看到这些内容可能会被吓到,但配置文件对于理解Magento来说相当之重要。你创建的所有模块都会被加到这个配置文件中,并且你在任何时候调用系统core中的功能时,都会到这个配置文件中去进行查看。

举个例子,熟悉MVC的开发者一定实例化过类似这样的helper类

$helper_sales = new HelperSales();

而Magento对于PHP的类声明进行了抽象化,所以上面的代码在Magento中会像这样

$helper_sales = Mage::helper('sales');

用通俗的话来讲,这个静态helper方法会完成如下动作

  1. 在Config中查找<helpers />代码段
  2. 在<helpers />代码段中查找<sales />代码段
  3. 在<sales />代码段中查找<class />代码段
  4. 将斜杠后的内容添加到上一步所查找到的内容后(本例采用默认值data)
  5. 实例化上一步中所查找到的类,即Mage_Sales_Helper_Data

看上去这样有些繁杂,但通过在配置文件中查找类,有助于我们在不修改core代码的情况下重载Magento的系统方法。而且相当的灵活,可以对部分系统方法进行继承修改。

Magento开发系列之一 基础知识

代码模块化

Magento采用Model-View-Controller(MVC)架构,Controller, Model都会放在单独的文件夹里,文件会根据功能进行分组,这种分组在Magento中称为模块(module)。

Magento MVC结构

在Magento中通常一个模块会包含 Controllers, Models, Helpers, Blocks等目录,比如app/code/core/Mage/Checkout模板下的文件夹结构:

Magento Checkout模块
想要修改或继承Magento的代码时,不应在系统core代码上进行修改,而应在local或community代码池内新建一个模块

app/code/local/Modulename

代码包(Package,或称为命名空间 – Namespace)用于区分开发代码的公司和组织,进而避免在进行代码分享时出现相互覆盖的情况。新建好模块后,还应在app/etc/modules内添加一个XML文件用于告知Magento你新建模块的位置。这个XML文件可用于指定单一模块,命名格式为Packagename_Modulename.xml,也可用于指定命名空间内的多个模块,命名格式为Packagename_All.xml,比如在app/etc/modules内在Mage_All.xml文件。但是并不推荐在单个文件中指定多个模块,因为这样就失去了分开定义模块的意义。

基于配置的MVC

Magento是一套基于配置文件(configuration-based)的MVC系统,有别于传统的基于惯例(convention-based)的MVC系统。在基于惯例的MVC中,添加一个控制器或模型,只需创建一个文件或类,系统会自动运行。

Magento的MVC结构

而在像Magento这样的基于配置文件的MVC系统中,除了添加新文件或类外,还需要告知系统所创建的类或类群的名称,就是通过每个模块中的config.xml文件。
比如要在自己写的模块中使用一个模型,就需要在config.xml中添加一些代码告诉Magento你要使用模型以及基类的名称

<models>
     <packagename>
          <class>Packagename_Modulename_Model</class>
    <packagename>
</models>

Helpers, Blocks, Routes for your Controllers, Event Handlers等也同理,几乎在Magento系统中添加任何东西都需要在配置文件中做类似修改。

控制器

在任何PHP系统中,主入口文件仍然是PHP文件,因而Magento的主入口文件也不例外,具体的说就是index.php这个文件。但绝不要修改index.php文件,在MVC系统中,index.php用于

  1. 检测URL地址
  2. 基于既定规则,将URL解析成控制器类和Action方法(这步称为Routing)
  3. 实例化控制器类并调用Action方法(这步称为dispatching)

也就是说在Magento或其它的MVC系统入口是控制器文件中的一个方法,那么http://example.com/catalog/category/view/id/25这个URL会被解析为

Front Name: catalog

URL中的第一段称为前台名称(front name),这会告知Magento在哪个模块去查找控制器,本例中前台名称就是catalog,对应的路径是app/code/core/Mage/Catalog

Controller Name:category

紧接着的部分告知Magento应使用哪个控制器,每个模块中都有一个controllers文件夹用于存放该模块的控制器文件,本例中对应的文件是

app/code/core/Mage/Catalog/controllers/CategoryController.php

其中的内容类似:

class Mage_Catalog_CategoryController extends Mage_Core_Controller_Front_Action
{
}

所有Magento前台的应用都继承Mage_Core_Controller_Front_Action这个类

Action Name:view

URL的第三部分为动作名,本例中的view用于创建动作方法,具体对应viewAction

class Mage_Catalog_CategoryController extends Mage_Core_Controller_Front_Action
{
    public function viewAction()
    {
        //main entry point
    }
}

熟悉Zend框架的朋友对于这种命名规则一定也不会陌生。

Paramater/Value – id/25

在动作名后面的部分会被作为GET方法传入的键值对,本例中的id/25代表GET变量名为id,值为25。前面也提到如果要模块使用控制器的话,需在config文件中进行添加,下面就是在Catalog模块中应用控制器的代码

<frontend>
    <routers>
        <catalog>
            <use>standard</use>
            <args>
                <module>Mage_Catalog</module>
                <frontName>catalog</frontName>
            </args>
        </catalog>
    </routers>
</frontend

可能你现在还不理解上面各标签的具体意义,不过不用担心,后面会详细的说明。注意catalog这段是在URL中链接到指定模块的前台名。通常Magento的core模块中会使用与模块名相同的前台名称,但并非强制要这样做。

 多路由(Routers)

前面所说的routing是针对Magento的cart应用(常称为前台frontend),当Magento在URL未发现有关的Controller/Action时,会再次尝试一套针对Admin应用的Routing规则,如果再次失败,则会使用一个名为Mage_Cms_IndexController的特殊控制器。
CMS控制器查找Magento的内容管理系统,来确定是否有什么内容可以加载,如果还找不到,则会返回404页面。

基于环境的URI模型载入

我们已经了解到Action方法的入口,下一步就是实例化类进行具体操作了,Magento有一套特别的方法来实例化Models, Helpers和Blocks,也就是全局的Mage类中的静态工厂方法,例如:

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

catalog/product称作一个类群名称,也常被称作URI,前面部分catalog用于查找类位于哪个模块中,后面部分product用于指定要加载的类,所以本例中两段代码都会被解析到app/code/core/Mage/Catalog这个模块,也就是类名称会以Mage_Catalog开头。然后Product会被加到类名称的最后

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

Mage::helper('catalog/product');
Mage_Catalog_Helper_Product

这些规则由模块中的config文件来限定,在自己创建模块时,会使用你自己的类群来执行Mage::getModel(‘myspecialprefix/modelname’);

实例化时不强制使用类群名称,但后面我们会讲到这样做会有诸多好处。

Magento的模型

和大多数框架一样,Magento提供对象关系映射(ORM)系统。ORM把你从书写复杂的SQL语句中解放出来,仅通过PHP代码就可以操作数据库,比如:

$model = Mage::getModel('catalog/product')->load(27);
$price = $model->getPrice();
$price += 5;
$model->setPrice($price)->setSku('SK83293432');
$model->save();

 

上例中我们调用了getPrice和setPrice方法,但在Mage_Catalog_Model_Product类中并没有这些方法,这是因为ORM使用了PHP中的魔术方法__call来实现getters和setters。调用$product->getPrice()方法会get模型属性price,调用$product->setPrice()方法会set模型属性price。以上的分析建立在没有名称为getPrice或setPrice的方法的基础上,如果有这两个方法就不会执行魔术方法。感兴趣的朋友可以查看一下Varien_Object类,所有的模型都继承这个类。

如果想要获取所有的模型数据,可以调用$product->getData()方法,将会返回一个包含所有属性的数组。同时也可以连接多个set方法:

$model->setPrice($price)->setSku('SK83293432');

那是因为每个set方法返回一个模型的实例,这种使用在Magento的代码中随处可见。Magento的ORM还可以通过一个Collections接口来查询多个对象,以下代码将获取一个包含成本价为$5的产品集合

$products_collection = Mage::getModel('catalog/product')
->getCollection()
->addAttributeToSelect('*')
->addFieldToFilter('price','5.00');

这里同样用到了连接,Collections使用PHP的标准库来实例化包含属性数组的对象

foreach($products_collection as $product)
{
    echo $product->getName();
}

你可能会奇怪addAttributeToSelect方法有什么作用,Magento有两类Model对象,一种是传统的一个对象对应一张数据表的模型,在实例化这种模型时,会选取所有的属性。另一种则是Entity Attribute Value (EAV) 模型,EAV模型中数据散布在数据库中的不同表格内,这样产品属性会非常灵活,每次添加一个属性都无需修改schema。在创建EAV对象集合时,Magento会查询有限的列,所以需要使用addAttributeToSelect来获取指定的列,或者通过addAttributeToSelect(*)来查询所有的列。

Helpers

Magento的Helper类包含操作对象和变量的工具方法,比如:

$helper = Mage::helper('catalog');

你可能已经注意到这里并没有包含类群的第二部分,每个模块都一个默认的Data帮助类,所以上面的代码相当于

$helper = Mage::helper('catalog/data');

通常Helpers继承Mage_Core_Helper_Abstract类,也就默认获取到几个有用的方法

$translated_output = $helper->__('Magento is Great'); //gettext style translations
if($helper->isModuleOutputEnabled()): //is output for this module on or off?

 Layouts

上面我们谈到了Controllers, Models以及Helpers,在一个典型的PHP MVC系统中,在操作完模型后,就会

  • 为view设定一些变量
  • 系统会加载默认的外部HTML布局
  • 系统加载外部布局内的view

不过在查看典型的Magento的控制器时,并没有发现任何如下内容

/**
 * View product gallery action
 */
public function galleryAction()
{
    if (!$this->_initProduct()) {
        if (isset($_GET['store']) && !$this->getResponse()->isRedirect()) {
            $this->_redirect('');
        } elseif (!$this->getResponse()->isRedirect()) {
            $this->_forward('noRoute');
        }
        return;
    }
    $this->loadLayout();
    $this->renderLayout();
}

取而代之的是以下两个调用

$this->loadLayout();
$this->renderLayout();

从这我们已经可以看出Magento中的V有别于通常所见到的的MVC,需要通过代码指明渲染布局。布局本身也是有区别的,Magento的布局是一个包含Block对象的集合。每个Block对象会去渲染一段特定的HTML,每个Block对象是PHP代码的混合,包含.phtml模板文件中的PHP代码。Blocks对象用于与Magento系统进行交互获取Models中的数据,而phtml模板文件会生成页面所需的html代码。
例如页面头部Block app/code/core/Mage/Page/Block/Html/Head.php使用到了page/html/head.phtml文件。
也可以这么认为,Block类是一个小型的控制器,.phtml就是MVC中的view。

$this->loadLayout();
$this->renderLayout();

默认情况下调用以上代码时Magento会载入一个网站结构框架的Layout,结构框架中会包含html, head和body标签以及单列或多列的Layout,另外还会有一些导航所用的内容Block以及默认欢迎信息等。
结构和内容是在Layout系统中人为指定的,Block并不能在程序上判定这是一个结构还是内容,但有助理解上的区分。
在Layout中添加内容,需要告诉Magento

&quot;Hey, Magento, add these additional Blocks under the &quot;content&quot; Block of the skeleton&quot;

&quot;Hey, Magento, add these additional Blocks under the &quot;left column&quot; Block of the skeleton&quot;

程序上可以在控制器action中使用

public function indexAction()
{
    $this->loadLayout();
    $block = $this->getLayout()->createBlock('adminhtml/system_account_edit')
    $this->getLayout()->getBlock('content')->append($block);
    $this->renderLayout();
}

但是通常(至少在前端应用中如此)会在Layout系统中使用XML文件实现。主题中的Layout XML文件可在控制器的基础上删除通常会被渲染的Blocks或者在框架中添加Blocks。例如下面的Layout XML文件:

<catalog_category_default>
    <reference name=&quot;left&quot;>
        <block type=&quot;catalog/navigation&quot; name=&quot;catalog.leftnav&quot; after=&quot;currency&quot; template=&quot;catalog/navigation/left.phtml&quot;/>
    </reference>
</catalog_category_default>

上面代码的意思是在catalog模块category控制器内的default Action,将left结构Block中插入一个catalog/navigation Block,作用的模板是catalog/navigation/left.phtml。关于Blocks的最后一个重要的事情,你会在模板中看到类似下面的代码:

$this->getChildHtml('order_items')

这是Block渲染内嵌Block的方法,但仅有在子Block在Layout XML文件中作为内嵌Block,该Block才能渲染这个子Block,也就是说left.phtml中的$this->getChildHtml()会返回空,但如果代码是下面这样:

<catalog_category_default>
    <reference name=&quot;left&quot;>
        <block type=&quot;catalog/navigation&quot; name=&quot;catalog.leftnav&quot; after=&quot;currency&quot; template=&quot;catalog/navigation/left.phtml&quot;>
            <block type=&quot;core/template&quot; name=&quot;foobar&quot; template=&quot;foo/baz/bar.phtml&quot;/>
        </block>
    </reference>
</catalog_category_default>

在catalog/navigation Block中,就可以调用

$this->getChildHtml('foobar');

 Observers

Magento和所有良好的面向对象系统一样,为终端用户提供了Event/Observer。在页面请求中发生一些动作(如保存模型,用户登录)时,Magento会发出一个事件信号。

那么在创建自己的模块时,就可以监听这些事件,比如要在客户登录时获取邮箱,就应在config.xml进行相关配置来监听customer_login事件

<events>
    <customer_login>
        <observers>
            <unique_name>
                <type>singleton</type>
                <class>mymodule/observer</class>
                <method>iSpyWithMyLittleEye</method>
            </unique_name>
        </observers>
    </customer_login>
</events>

然后写一段在客户登录时运行的代码:

class Packagename_Mymodule_Model_Observer
{
    public function iSpyWithMyLittleEye($observer)
    {
        $data = $observer->getData();
        //code to check observer data for out user,
        //and take some action goes here
    }
}

类重载

最后,Magento系统允许将core模块中的Model, Helper和Block类替换成自己的代码,这有些类似于Ruby或Python中的Duck Typing或Monkey Patching。
为便于理解,这里举个例子。产品的模型类是Mage_Catalog_Model_Product,在调用下面这段代码时就会创建Mage_Catalog_Model_Product对象

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

Magento的类重载系统允许你和系统间进行下面的对话

&quot;Hey, whenever anyone asks for a catalog/product, instead of giving them a Mage_Catalog_Model_Product,
give them a Packagename_Modulename_Model_Foobazproduct instead&quot;.

如果你愿意,可以在Packagename_Modulename_Model_Foobazproduct类中继承原来的产品类

class Packagename_Modulename_Model_Foobazproduct extends Mage_Catalog_Model_Product
{
}

这允许你修改该类中的任何方法,同时保存其它已有方法的功能

class Packagename_Modulename_Model_Foobazproduct extends Mage_Catalog_Model_Product
{
 public function validate()
 {
     //add custom validation functionality here
     return $this;
 }

}

你可能已经猜到,这种重载(或者说重写)是在config.xml文件中实现的

<models>
    <!-- does the override for catalog/product-->
    <catalog>
        <rewrite>
            <product>Packagename_Modulename_Model_Foobazproduct</product>
        </rewrite>
    </catalog>
</models>

这是需要提一下的是,你所写的模块中的某一个类重载另一个模块中的某一个类时,并不会重载整个模块。这就保证了在对某些方法进行修改时不必担心该模块中其它的内容。

Magento添加email退订功能

在Magento中默认提供了订阅邮件的代码,但退订则需要二次开发或安装插件,本帖旨在探讨邮件订阅和尤其是退订的相关代码实现方式。

使用系统所提供的方法

订阅和退订操作的数据表是newsletter_subscriber,对应的数据列为subscriber_status,值有1,2和3对应的意思如下:

1: Subscribed 2: Status Not Active 3: Unsubscribed

以下某邮箱退订后在数据表中的显示:

Magento数据表newsletter_subscriber

Magento系统中提供了对应的方法来处理退订和订阅,示例代码如下:

<?php
require_once 'app/Mage.php';
Mage::app();
/* To Subscribe */
Mage::getModel('newsletter/subscriber')->subscribe('name@domain.com');
/* To Unsubscribe */
Mage::getModel('newsletter/subscriber')->loadByEmail('name@domain.com')->unsubscribe();
?php>

直接对数据库进行操作

本方法通过在template目录下添加代码来处理退订,比如我们在app/design/frontend/default/模板名/template下建立一个unsub文件夹,并在unsub下创建一个unsubscribe.phtml文件来处理退订,这里为了简便,我们采用GET的方式来提交邮箱,您可以配合表单应用POST方式进行提交。示例代码如下:

<?php
require_once 'app/Mage.php';
Mage::app();
if($_GET['unsub']==1 && $email = $_GET['email']){ //获取退订状态以及提交的email
    //从数据库里读取订阅状态信息
    $read = Mage::getSingleton("core/resource")->getConnection('core_read');
    $sql = "select subscriber_status from newsletter_subscriber where subscriber_email = '".$email."'";
    $result = $read->fetchAll($sql);  //可使用fetchRow查找一条
    if(!$result or $result[0]['subscriber_status'] != 1){
        die('Sorry, we can not find your record or you have already unsubscribed');
    }else{
        //更改订阅状态
	$write = Mage::getSingleton("core/resource")->getConnection('core_write');
        $sql = "update newsletter_subscriber set subscriber_status = 3 where subscriber_email = '".$email."'";
        $write->query($sql);
        }
}else{
    die('Sorry, we can not find your record');
}
?>

以上代码添加完毕后,在CMS > Pages下添加一个页面用于处理退订及退订完成后显示的页面,并在其中添加如下代码:

 {{block type="core/template" name="unsub" template="unsub/unsubscribe.phtml"}}
<h2>Your email address has been successfully removed from our newsletter list.</h2>

其中name以及template的值对应前面创建的目录及文件名。

Magento二次开发知识帖

操作数据库

Magento连接数据库执行查询

<?php
$connection = Mage::getSingleton('core/resource')->getConnection('core_read');
$select = $connection->select()
->from('catalog_product_entity', array('*')) // 选择查询所有字段,如果只想查询一个字段,把*改成字段名
->where('entity_id=?',1);               // where id =1
//->group('name');         // group by name
$rowsArray = $connection->fetchAll($select); // 返回所有行
$rowArray =$connection->fetchRow($select);   //返回行
var_dump($rowArray);
echo $rowArray[sku];
?>

执行时需要include或require一下app/Mage.php文件,若出现类似”Fatal error: Call to a member function getModelInstance() on a non-object in D:\xampp\htdocs\magento\app\Mage.php on line 463“报错,请尝试添加一句

$app = Mage::app('default');

Magento对数据库执行增删改查

以下直接提供让magento操作支持直接操作数据库修改方法,  查:

    $read = Mage::getSingleton(&quot;core/resource”)-&gt;getConnection('core_read');
    $sql = &quot;select * from <code>abc</code>”;
    $result = $read-&gt;fetchAll($sql);  //fetchRow查找一条

增,删,改
    $write = Mage::getSingleton(&quot;core/resource&quot;)-&gt;getConnection('core_write');
     $sql = &quot;insert into abc (name)values('hello')”;
     $write-&gt;query($sql);

另一种写法:
增
     $write = Mage::getSingleton(&quot;core/resource”)-&gt;getConnection('core_write');
     $table = Mage::getSingleton('core/resource')-&gt;getTableName('abc');
     $write-&gt;insert($table,array('name'=&gt;'hello'));

改
     $write = Mage::getSingleton(&quot;core/resource”)-&gt;getConnection('core_write');
    $table = Mage::getSingleton('core/resource')-&gt;getTableName('abc');
    $write-&gt;update($table,array('name'=&gt;'abc'),array('id=?'=&gt;3));

删
        $write = Mage::getSingleton(&quot;core/resource”)-&gt;getConnection('core_write');
    $table = Mage::getSingleton('core/resource')-&gt;getTableName('abc');
    $write-&gt;delete($table,array('id=?'=&gt;3));

查
     $read = Mage::getSingleton(&quot;core/resource”)-&gt;getConnection('core_read');
    $table = Mage::getSingleton('core/resource')-&gt;getTableName('abc');
        $result = $read-&gt;select()-&gt;from(array('main_table'=&gt;$table))-&gt;where('main_table.id=?',3)-&gt;limit(1);
        $products=$read-&gt;fetchAll($result);

判断用户登录状态

Mage::getSingleton( 'customer/session' )->isLoggedIn();
//已登录可获取客户相关信息,如姓名,email
$customer = Mage::getSingleton('customer/session')->getCustomer();
echo $customer->getName();
echo $customer->getEmail();

CMS代码

邮件订阅

{{block type="newsletter/subscribe" template="newsletter/subscribe.phtml"}}

自定义代码

{{block type="core/template" name="mycode" template="mycode/myfile.phtml"}}