Magento开发系列之十二 默认系统配置

本节并没有太多新内容,更多的是对前面有关系统后台配置的补充。在我们创建新的系统配置路径时,Magento并没有存储默认值,甚至对于一些系统默认配置也是如此,这点可以通过查看core_config_data表来进行验证。

mysql> select * from core_config_data;
+-----------+---------+---------+----------------------------+--------+
| config_id | scope   | scope_id| path                       | value  |
+-----------+---------+---------+----------------------------+--------+
|         1 | default |        0| general/region/display_all | 1      |
... ...

这张表仅存储在后台或基它程序中明确设置的值,而如果请求一个没有进行这个设置的系统配置值的话,Magento会到全局配置文件中去查看默认值。虽然不要求这么做,但为自添加配置变量设置一个默认值是一个不错的习惯。这样做很简单,也防止在获取到空值时产生一些意想不到的效果。

上面提到默认值存放在全局配置文件中,这可能与很多的想法大相径庭,因为大家可能会认为这默认值会存储在用于配置后台的system.xml文件中。为什么要这么做其实大可不必深究,我们可以理解为配置文件中存储常用的值,而stystem.xml中存储着用于修改这些值的界面的配置。

在模块配置文件config.xml里添加一个<default />代码块(如app/code/local/Packagename/Modulename/etc/config.xml)

<config>
    <!-- ... -->
    <default>
    </default>
    <!-- ... -->
</config>

这就是我们用于存储默认值的最上级节点,接下来将配置路径转化成XML格式的树形节点,比如我们为以下配置路径设置默认值:

design/header/welcome

那么在config.xml的代码就会是这样:

<config>
    <!-- ... -->
    <default>
        <design>

<header>
                <welcome>Default welcome msg!</welcome>
            </header>

        </design>
    </default>
    <!-- ... -->
</config>

应用了这个配置后,请求design/header/welcome时,如果没有设定值,就会返回”Default welcome msg!”。这个例子是基本系统默认的配置,我们业看看design/header/welcome的真实配置(文件地址:app/code/core/Mage/Page/etc/config.xml):

<default>
    <design>
        <head translate="default_description" module="page">
            <default_title>Magento Commerce</default_title>
            <default_description>Default Description</default_description>
            <default_keywords>Magento, Varien, E-commerce</default_keywords>
            <default_robots>*</default_robots>
            <default_media_type>text/html</default_media_type>
            <default_charset>utf-8</default_charset>
        </head>

<header translate="welcome" module="page">
            <logo_src>images/logo.gif</logo_src>
            <logo_alt>Magento Commerce</logo_alt>
            <logo_src_small>images/logo.gif</logo_src_small>
            <welcome>Default welcome msg!</welcome>
        </header>

<footer translate="copyright" module="page">
            <copyright>&amp;copy; 2015 Magento Demo Store. All Rights Reserved.</copyright>
        </footer>

    </design>
    <system>
        <media_storage_configuration>
            <allowed_resources>
                <site_favicons>favicon</site_favicons>
            </allowed_resources>
        </media_storage_configuration>
    </system>
</default>

这是所有design/*的默认配置,和我们例子中不同的时这里有一个translate属性:

<head translate="default_description" module="page">

translate和module属性告诉系统哪些节点需要被translate,以及使用哪个模块的Data Helper来进行这一操作。在上例中,welcome节点将被转化为:

Mage::helper('page')->__(...);

前面我们也提到过,如果调用helper类时URI没有传入第二部分,会默认使用data,也就是说下面两句代码是一样的:

Mage::helper('page')->__(...);
Mage::helper('page/data')->__(...);

如果想要转化多个子节点,可以在名称之间用逗号进行分隔,如:

<example translate="foo,baz,bar" module="page">

写在后面

本系列更新到此结束,一共十二篇,大多数代码并非出自笔者之手,而是来自Magento资深大师Alan Storm,笔者只是用自己的理解丰富了一下内容,以有助于国内Magento学习者更为有效地掌握Magento开发相关知识。本系列的终结只是一个阶段性的符号,Magento作为一套强大的电商系统还有很值得探讨和学习的地方,需要大家共同努力去研究和分享,让我们一起努力吧!

Magento开发系列之十一 数据重载和升级

在Magento经常被鼓吹也常被滥用的功能就是重载core中的系统代码,而另一个开发者经常讨论的话题就是升级以及重载对升级的阻碍作用。本节我们就来一起看看重载给版本切换所带来的不便。

需要强调下我们这里的是修改Maento中core的业务逻辑,对于phtml模板文件的修改是非常普遍的。

不论是在Magento中还是其它系统中对升级最不友好的肯定是直接修改源代码,比如想要修改产品模型时,就直接编辑了如下文件:

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

一旦这么做,就直接修改Magento中的基础代码,因而在进行升级时就需要进行逐个文件的合并,这通常都会出问题。此外这种修改还可能返回系统无法识别的数据或者是得到计划外的数据。虽然我们不建议去修改源代码,但还是有很多开发者在刚开始时会去这么做。在开发新项目时可以从官网下载一份全新的系统文件,然后比对下lib和app/code/core文件夹内的代码,看看核心代码有没有被修改。

Magento或者说PHP会在下面的目录中搜索类文件

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

由于这个原因以及PHP构造时包含的顺序,在core/code/local中复制一个core文件系统就会先包含这个文件,所以如果想要修改产品模型的功能,就可以进行如下复制:

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

如果这里定义的是类而不是core文件,就无需包含核心文件,这也就避免了合并文件的麻烦,并且所有修改文件都会放在同一个目录结构中。这样虽然比直接修改核心源代码要好些,但还是有可能会修改掉一些重要方法,比如前面说到的产品模型中的getName方法

/**
     * Get product name
     *
     * @return string
     */
    public function getName()
    {
        return $this->_getData('name');
    }

在对这个方法进行重载时,我们有可能不小心添加了代码导致重载后返回空

/**
* LOCAL OVERRIDE! Get product name
*
* @return string
*/
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
}

系统的其它部分可能会使用到这个方法并等待该方法返回一个字串,而我们的修改则会导致接下来的执行被中断。这里如果返回对象情况还会更糟,因为调用一个空方法会导致一个致命错误(Fatal)。

下面我们来看看同一模型中的validate方法

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;
}

在修改时我们有可能不小心删除了dispatch事件

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

这时系统中其它依靠这一事件的部分都会停止运转。此外,在升级时同样存在着危险,如果升级中更新了类,而我们仍将使用旧的过时了的方法。也就是说我们还是需要在升级时进行手动的代码合并。

Magento中的类重载和系统重写依赖于创建模型、Helper和Block时使用的工厂模式,比如在执行如下代码时:

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

实际是在告诉Magento去查找catalog/product用到的类并进行实例化。接下来,Magento会付出查看系统配置文件,并在config.xml中查看针对catalog/product应该使用哪个类,然后Magento就会对类进行实例化并返回一个模型。

而当我们在Magento中重载一个类时,实际上修改了配置文件,在进行上述操作时,就会告诉系统:如果要实例化catalog/product模型的话,请不要使用core中的类,而使用我们定义的类Myp_Mym_Model_Product

同时,在定义我们自己的类时,需要继承原来的类

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
}

这样,我们的新类就会包含系统类中的功能,同时也就避免了在升级时还需要合并文件的功能,也不会在升级时包含过时的方法。但是在修改方法时还是存在问题,我们还是以getName和validate方法为例,我们还是会忘记返回值或返回错误的值,甚至是忘记添加方法中重要功能的代码块:

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
    }      
}

重载重写系统无法在这个层面上形成保护,但也提供了一些避免的方式。同于我们是继承了原来的类,因而可以在构造时使用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;
    }      
}

通过调用原来的方法,就可以确保该进行的操作都会完成,并且也减少了返回非系统要求值的可能性。当然只能是减少,而非彻底消除,开发者还是需要负责在自建代码中返回和原方法相同的对象或其它值。也就是,即使应用了重载系统,系统还是可能会由于误操作导致崩溃。

鉴于此,我们在进行开时,应使重载最小化,重载时可以在最后添加如下代码:

return parent::originalMethod();

如果重载时要求先运行原方法,可以使用如下方法:

public function someMethod()
{
    $original_return = Mage::getModel('mymodule/immutable')
    ->setValue('this is a test');
 
    //my custom code here
    return $original_return->getValue();
}

mymodule/immutable指向如下类

class Alanhou_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;
    }
}

这样并不能保证$original_return不会被我们自己或其它人重写,但确实会在修改后更容易发现。如果在重载类的方法最后没有使用$original_return->getValue或parent::method,那么在debug时就很容易发现问题。

还有的时候我们会希望修改core方法中的返回值,在有这种需求时,定义一个新方法来调用原方法并在主题中调用新方法往往更为安全:

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;
    }
}

这样可以保证原方法的一些功能依然有用,原来的返回类型保持不变,而我们也可以在系统中的某些地方添加自己的逻辑。

Magento开发系列之十 Varien数据集合

早期PHP开发者如果想要将一组相关的变量放在一起,只能通过数组,虽然和C语言的内存地址数组同名,PHP的数组更多的是一个对象加序列索引的数组行为组成的通用字库。在其它语言中都有多种数据结构,每种结构在存储、速度和语法上有不同的优势。PHP语言的逻辑是减去这些选择而仅使用一种有用的数据结构即可。从PHP 5开始通过提供内置类和接口来让开发者允许创建自己的数据结构。

$array = new ArrayObject();
class MyCollection extends ArrayObject{}
$collection = new MyCollection();
$collection[] = 'bar';

一些软件工程师肯定还不满足于此,因为没有获取到底层实现的细节,但已经获得包含特定功能的方法来创建类似数组的对象。也可以设定一定的安全规则来只允许某些类型的对象进行集合,Magento中包含一些这种集合,事实上每个遵循Magento接口的模型对象都会获得一个集合类型。要成为有效的Magento开发者,了解这些集合的工作机制也非常重要。本节我们就来看看Magento的集合,创建一个可以自由添加代码的控制器action。

第一步需要创建一些新的对象

$thing_1 = new Varien_Object();
$thing_1->setName('Richard');
$thing_1->setAge(24);

$thing_2 = new Varien_Object();
$thing_2->setName('Jane');
$thing_2->setAge(12);

$thing_3 = new Varien_Object();
$thing_3->setName('Spot');
$thing_3->setLastName('The Dog');
$thing_3->setAge(7);

Varien_Object类定义了所有Magento模型所继承的类,这面向对象的系统中这很常见,它可以保证可以轻易地为每个对象添加方法、功能而无需编辑每个类文件。每个继承Varien_Object的对象都会获得魔术方法getter和setter,这样就可以通过get和set来获取并设置数据。比如,

var_dump($thing_1->getName());

如果不知道要获取的属性名,可以将所有数据取出到一个数组中,如

var_dump($thing_3->getData());

上面的代码将返回下面这样的数组:

array
'name' => string 'Spot' (length=4)
'last_name' => string 'The Dog' (length=7)
'age' => int 7

你可能会注意到last_name这个属性,中间是由下划线来划分的,如果想要使用getter和setter魔术方法,需要采用驼峰法来进行命名:

$thing_1->setLastName('Smith');

在较新版本的Magento中,可以使用数组形式的中括号来取得属性值:

var_dump($thing_3["last_name"]);

这是PHP5中强大的功能,现在我们创建了一些对象,让我们一起来加入到集合中吧,要记得集合类似数组,但是是由PHP开发人员定义的:

$collection_of_things = new Varien_Data_Collection();
$collection_of_things
->addItem($thing_1)
->addItem($thing_2)
->addItem($thing_3);

Magento中的数据集合通常都继承自Varien_Data_Collection,所有Varien_Data_Collection中的方法都可以被调用,对于集合我们可以用foreach函数来进行遍历

foreach($collection_of_things as $thing)
{
    var_dump($thing->getData());
}

还可以通过以下方法取出集合中的第一项和最后一项:

var_dump($collection_of_things->getFirstItem());
var_dump($collection_of_things->getLastItem()->getData());

如果想要以XML形式获取集合中的数据,可以使用如下方法

var_dump( $collection_of_things->toXml() );

那如果只要取出指定的属性呢?

var_dump($collection_of_things->getColumnValues('name'));

Magento开发团队甚至还添加了过滤功能:

var_dump($collection_of_things->getItemsByColumnValue('name','Spot'));

为什么要特别地讲这些东西呢?因为所有的内置数据集合都继承自这个对象,也就是说可以对产品集合等进行同样的操作,比如:

public function testAction()
{
    $collection_of_products = Mage::getModel('catalog/product')->getCollection();
    var_dump($collection_of_products->getFirstItem()->getData());
}

Magento中大多数的模型对象都有一个getCollection方法,它可以实例化集合并返回所有系统中支持的对象类型。数据集合包含很多决定使用index还是缓存的复杂逻辑,还有针对EAV实体的一些逻辑。在同一个集合中连续多次调用同一方法可能会产生一些不可控的效果,因此下面所有的例子都是放在一个action方法中进行的,建议在独自测试时也这么做。另外var_dump对于对象和集合相当的有用,因为通常针对大规模递归的对象时,它也会很智能的显示出对象的结构。产品集合还有其它的一些Magento集合,还继承了Varien_Data_Collection_Db,这样就可以使用很有有用的方法,比如要查看集合的select语句:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	var_dump($collection_of_products->getSelect());
}

输出结果如下:

object(Varien_Db_Select)#86 (5) {
["_bind":protected]=> array(0) { }
["_adapter":protected]=> object(Magento_Db_Adapter_Pdo_Mysql)

看上去一头雾水,由于Magento使用了Zend数据抽象层,select也是对象,我们修改代码如下:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	// var_dump($collection_of_products->getSelect());
	var_dump((string) $collection_of_products->getSelect());
}

这次的输出结果可能是如下的这种简单SQL:

string(49) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code>&quot;

也有可能是更为复杂的语句:

string 'SELECT <code>e</code>.*, <code>price_index</code>.<code>price</code>, <code>price_index</code>.<code>final_price</code>, IF(<code>price_index</code>.<code>tier_price</code>, LEAST(<code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>tier_price</code>), <code>price_index</code>.<code>min_price</code>) AS <code>minimal_price</code>, <code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>max_price</code>, <code>price_index</code>.<code>tier_price</code> FROM <code>catalog_product_entity</code> AS <code>e</code>
INNER JOIN <code>catalog_product_index_price</code> AS <code>price_index</code> ON price_index.entity_id = e.entity_id AND price_index.website_id = '1' AND price_index.customer_group_id = 0'

这种区别主要取决于所select的属性,以及前所提到的index和cache的问题。如果学习过前面的章陈明,就知道Magento的很多模型包括产品模型使用EAV系统,默认情况下EAV集合不会包含所有对象属性,可以通过使用addAttributeToSelect方法来添加所有属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*');

也可以只添加一个属性

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('meta_title');

或者多个属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('meta_title')->addAttributeToSelect('price');

很多刚接触Magento ORM系统的PHP开发者总是搞不清楚该在什么时候进行数据库调用。在书写SQL语句甚至是使用基础的ORM系统时,SQL调用都在实体化对象当时发生。

$model = new Customer();
//SQL Calls being made to Populate the Object
echo 'Done'; //execution continues

但在Magento中并不是这样,它使用了延时加载(Lazy Loading)的概念。简单地说,延时加载是指只有在客户端程序需要获取数据时才会执行SQL调用,也就是如果进行如下操作:

$collection_of_products = Mage::getModel('catalog/product')->getCollection();

Magento并没有操作数据库,因此你可以在后面再添加属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection();
$collection_of_products->addAttributeToSelect('meta_title');

这里完全不用担心Magento会在每次添加属性时都去进行数据库查询,这些查询只有到了需要获取集合中的数据时才会进行。

数据库集合中最重要的一个方法是addFieldToFilter,这就添加了WHERE语句,可以使用自己的SKU也可以安装测试数据来进行如下测试:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	$collection_of_products->addFieldToFilter('sku','n2610');
	echo "Our collection now has ". count($collection_of_products). ' item(s)';
	var_dump($collection_of_products->getFirstItem()->getData());
}

addFieldToFilter中的第一个参数就是我们想要过滤的属性,而第二个则是查询的值。本例中我们添加了sku作为过滤,值为n2610。第二个参数也可以用于指定想要过滤的类型,这个有些复杂,不过值得更深入地研究一下,所以默认情况下:

$collection_of_products->addFieldToFilter('sku','n2610'); 

相当于

WHERE sku = "n2610"

接下来运行一下如下语句:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku','n2610')->getSelect());
}

将会得到如下结果:

string(77) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> = 'n2610')&quot;

注意一旦我们使用了EAV属性,就会变得更为复杂,下面添加一个属性:

var_dump((string) Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*')->addFieldToFilter('meta_title','my title')->getSelect());

输出会得到下面这样复杂的SQL语句:

string(670) &quot;SELECT <code>e</code>.*, IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) AS <code>meta_title</code> FROM <code>catalog_product_entity</code> AS <code>e</code> INNER JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title_default</code> ON (<code>at_meta_title_default</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title_default</code>.<code>attribute_id</code> = '82') AND <code>at_meta_title_default</code>.<code>store_id</code> = 0 LEFT JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title</code> ON (<code>at_meta_title</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title</code>.<code>attribute_id</code> = '82') AND (<code>at_meta_title</code>.<code>store_id</code> = 1) WHERE (IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) = 'my title')&quot;

那如果我们不只想要在查询中使用等号呢?比如大于、小于、不等于等等。addFieldToFilter中的第二个参数可以解决这一问题,在这里可以不传入一个字符串,而是传入一个数组。比如我们进行如下修改:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array('eq'=>'n2610'))->getSelect());
}

其中的过滤即为:

addFieldToFilter('sku',array('eq'=>'n2610'))

可以看出,第二个参数是一个数组,键为eq,表示equals(等于),而值为n2610,即为想要过滤的值。Magento中有很多半英文的过滤符号,和perl中的基本一致,下面是一些过滤方式及其对应的SQL语句:

array("eq"=>'n2610')
WHERE (e.sku = 'n2610')
 
array("neq"=>'n2610')
WHERE (e.sku != 'n2610')
 
array("like"=>'n2610')
WHERE (e.sku like 'n2610')
 
array("nlike"=>'n2610')
WHERE (e.sku not like 'n2610')
 
array("is"=>'n2610')
WHERE (e.sku is 'n2610')
 
array("in"=>array('n2610'))
WHERE (e.sku in ('n2610'))
 
array("nin"=>array('n2610'))
WHERE (e.sku not in ('n2610'))
 
array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)
 
array("null"=>'n2610')
WHERE (e.sku is NULL)
 
array("gt"=>'n2610')
WHERE (e.sku > 'n2610')
 
array("lt"=>'n2610')
WHERE (e.sku &lt; 'n2610')
 
array("gteq"=>'n2610')
WHERE (e.sku >= 'n2610')
 
array("moreq"=>'n2610') //a weird, second way to do greater than equal
WHERE (e.sku >= 'n2610')
 
array("lteq"=>'n2610')
WHERE (e.sku &lt;= 'n2610')
 
array("finset"=>array('n2610'))
WHERE (find_in_set('n2610',e.sku))
 
array('from'=>'10','to'=>'20')
WHERE e.sku >= '10' and e.sku &lt;= '20

基本上一看就能够明白,这里挑出几个特别的解释一下

in, nin

in和nin条件语句中可以传入一组值,这里值的部分也可以使用数组。

array("in"=>array('n2610','ABC123')
WHERE (e.sku in ('n2610','ABC123'))

notnull, null

NULL类型在SQL中非常特别,不能通过等号运算符来过滤,通常使用null或notnull即可进行过滤,后面传入的值会被忽略掉

array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)

from – to

这是另外一个各其它语句不一样的格式,它传入的不是只有一对键值的数组,而是拥有两对键值,一个用于from,另一个用于to。从键名就可以猜它是用于限定一个范围的:

public function testAction
{
        var_dump( (string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('price',array('from'=>'10','to'=>'20'))->getSelect());                      
}

将会生成如下的SQL语句:

WHERE (_table_price.value >= '10' and _table_price.value <= '20')

最后我们来看看布尔值操作符,我们很少会只过滤一个属性,Magento自然早已考虑到这一情况。可以通过调用多个addFiledToFilter生成AND查询

function testAction()
{
        echo( (string) Mage::getModel('catalog/product') ->getCollection()->addFieldToFilter('sku',array('like'=>'a%'))->addFieldToFilter('sku',array('like'=>'b%'))->getSelect());
}

将生成如下SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> LIKE 'a%') AND (<code>e</code>.<code>sku</code> LIKE 'b%')

可以发现以上WHERE语句中有一个AND,当然,这条语句不会返回任何,因为sku不可能同时以a和b开头。那这种情况下我们应用使用OR语句,要这么做需要在addFieldToFilter的第二个参数处传一个过滤数组,通过定义变量来做为数组可能更为直观:

$filter_a = array('like'=>'a%');
$filter_b = array('like'=>'b%');

那么最终我们将使用如下方法:

public function testAction()
{
        $filter_a = array('like'=>'a%');
        $filter_b = array('like'=>'b%');
        echo((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array($filter_a,$filter_b))->getSelect());
}

这次我们将得到如下的SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (((<code>e</code>.<code>sku</code> LIKE 'a%') OR (<code>e</code>.<code>sku</code> LIKE 'b%')))

 

Magento开发系列之九 后台开发进阶

前面一节我们讨论了后台系统配置,本节我们将进行更深入的探讨,前面讲到了field内可以用到的一些标签:

<fields>
    <!-- ... --->
    <fieldname translate="label">
        <label>Field Name</label>
        <frontend_type>text</frontend_type>
        <sort_order>2</sort_order>
        <show_in_default>1</show_in_default>
        <show_in_website>1</show_in_website>
        <show_in_store>0</show_in_store>
    </fieldname>
    <!-- ... --->
</fields>

本节将对这些标签进行更深度的剖析,首先查看<label /><comment />标签,<label />标签中的内容将出现在文本框左侧,而<comment />标签则出现在文本框的下方

<fields>
	<demo_text>
		<label>This is a label</label>
		<frontend_type>text</frontend_type>
		<comment>While this is a comment</comment>
		<show_in_default>1</show_in_default>
		<show_in_website>1</show_in_website>
		<show_in_store>1</show_in_store>
	</demo_text>
</fields>

<labe />和<comment />标签

<show_in_default />, <show_in_website />,和<show_in_store />

这几个标签内都使用布尔型的0或1,用于定义在defautl视图、website视图或store视图中是否显示所添加的field。同时通过这一配置也可以控制选项在哪个视图里是可编辑的,比如可以设定在store视图里不可用:

<show_in_store>0</show_in_store>

当然通过编辑还可以在store社图级别设定或获取其中的值,但我们不推荐这么去做。

<sort_order />

<sort_order />中传入的是数值,用于决定在分组中field的排序,数值越大,则会出现在越下面。

<frontend_type />

<frontend_type />用于定义在配置中field的类型,所支持的类型有:

  1. allowspecific
  2. export
  3. image
  4. import
  5. label
  6. multiselect
  7. obscure
  8. password
  9. select
  10. text
  11. textarea
  12. time

这些值会按照工厂模式实例如下格式的类:

Varien_Data_Form_Element_Type

其中的Type对应的就是<frontend_type />中的值,这个动作是在Varien_Data_Form_Abstract(lib/Varien/Data/Form/Abstract.php)类中addField方法内进行的

class Varien_Data_Form_Abstract extends Varien_Object
{
... ...
public function addField($elementId, $type, $config, $after=false)
    {
        if (isset($this->_types[$type])) {
            $className = $this->_types[$type];
        }
        else {
            $className = 'Varien_Data_Form_Element_'.ucfirst(strtolower($type));
        }
        $element = new $className($config);
        $element->setId($elementId);
        $this->addElement($element, $after);
        return $element;
    }
... ...
}

<frontend_class />

<frontend_class />不是一个类群的名称,该标签可用于修改给field生成的表单元素标签的class属性,也就是说可以通过这个配置可以为表单属性添加一个CSS的类。比如:

<frontend_type>select</frontend_type>
<frontend_class>free-method</frontend_class>

会在结果页面变成:

<select class="free-method">

<validate />

<validate />这个标签看上去会让人误解,它只是在标签中添加一个CSS类

<frontend_type>text</frontend_type>
<validate>validate-email</validate>

上述代码会在结果页面变成:

<input class="validate-email">

Magento中还有更多的配置,这个CSS会在客户端触发验证机制,比如上面的代码会调用javascript来对表单的输入内容进行email验证。如果验证失败的话,就无法完成配置表单的提交。可以在以下文件中查看验证规则:

js/prototype/validation.js

email验证规则如下:

['validate-email', 'Please enter a valid email address. For example johndoe@domain.com.', function (v) {
                return Validation.get('IsEmpty').test(v) || /^([a-z0-9,!\#\$%&amp;'\*\+\/=\?\^_<code>\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9,!\#\$%&amp;'\*\+\/=\?\^_</code>\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*@([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*\.(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,})$/i.test(v)
            }],

这是一条功能很强大的正则表达式。

<can_be_empty />

<can_be_empty />标签针对多选框,也就是在提交时允许不选择任何选项。在设置can_be_empty为真时,实际上是在系统配置页面添加了一个hidden的input标签(文件地址:/lib/Varien/Data/Form/Element/Multiselect.php):

if ($this->getCanBeEmpty() && empty($this->_data['disabled'])) {
	$html .= '<input type="hidden" name="' . parent::getName() . '" value="" />';
}

这样在执行的时候就可以允许不予选择。

<depends />

<depends />标签用于设置仅在本组内其它配置项为指定值时显示某一配置项。比如,PayPal Express系统配置中有有如下的定义:

<specificcountry translate="label">
    <label>Countries Payment Applicable From</label>
    <frontend_type>multiselect</frontend_type>
    <sort_order>110</sort_order>
    <source_model>adminhtml/system_config_source_country</source_model>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
    <depends><allowspecific>1</allowspecific></depends>
</specificcountry>

上述代码中有一段

<depends><allowspecific>1</allowspecific></depends>

<allowspecific />在以下代码中定义:

<allowspecific translate="label">
   <label>Payment Applicable From</label>
   <frontend_type>select</frontend_type>
   <sort_order>100</sort_order>    <source_model>adminhtml/system_config_source_payment_allspecificcountries</source_model>
   <show_in_default>1</show_in_default>
   <show_in_website>1</show_in_website>
   <show_in_store>0</show_in_store>
</allowspecific>

如果<allowspecific>选项中的值为1,就会显示<specificcountry>版块,这个是通过后台的JS来即时实现的。虽然所有的表单项都可以使用onchange事件,但在Magento中只针对父级选项是select时才使用这一功能。

<source_model />

<source_model />标签通过以UR/类群名称格式指定模型类来设置field的默认选项,它可以与select和multi-select一起使用。除了标准的类群命名,还可以采用扩展形式:

module/modelname::methodName

系统会使用getModel(‘module/modulename’)来实例化模型,然后调用methodName来获取值-标签对来作为数据源。假如没有添加methodName,默认会调用toOptionArray方法。

<frontend_model />

默认情况下,Magento的表单元素使用Block类来进行处理

Mage_Adminhtml_Block_System_Config_Form_Field

但是如果想要自己指定系统配置选项的处理文件时,可以通过<frontend_model />标签以URI/类群名称的方式来指定另一个block类。比如针对adminnotification组中的<last_update />项可以进行如下配置:

<last_update translate="label">
    <label>Last update</label>
    <frontend_type>label</frontend_type>
    <frontend_model>adminhtml/system_config_form_field_notification</frontend_model>
    <sort_order>3</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>0</show_in_website>
    <show_in_store>0</show_in_store>
</last_update>

这里就指定了system_config_form_field_notification来进行处理,对应的类就是app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Notification.php文件中的如下类

class Mage_Adminhtml_Block_System_Config_Form_Field_Notification extends Mage_Adminhtml_Block_System_Config_Form_Field
{
    protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
    {
        $element->setValue(Mage::app()->loadCache('admin_notifications_lastcheck'));
        $format = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
        return Mage::app()->getLocale()->date(intval($element->getValue()))->toString($format);
    }
}

这里通过重载_getElementHtml方法确保所输入日期会以相同格式进行显示。

<backend_model />

一旦在Magento中进行表单提交,数据就会被存储起来,对于系统配置项,通常是由以下的模型类来进行处理的

 Mage_Core_Model_Config_Data

但在开发中经常会想要使用其它的后台模型,同样地,可以通过<backend_model />标签以URI/类群名称的方式来指定另一个模型类。通常这样做不是因为想要修改数据存储地址,而是想在field保存时进行一些其它的操作。通过自建模型继承Mage_Core_Model_Config_Data类并在模型中定义_beforeSave和_afterSave方法,可以在配置值发生更改时进行其它的操作。

以tablerate组内的import项为例

<import translate="label">
    <label>Import</label>
    <frontend_type>import</frontend_type>
    <backend_model>adminhtml/system_config_backend_shipping_tablerate</backend_model>
    <sort_order>6</sort_order>
    <show_in_default>0</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</import>

类群名adminhtml/system_config_backend_shipping_tablerate表示app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Shipping/Tablerate.php文件中下面这个类

class Mage_Adminhtml_Model_System_Config_Backend_Shipping_Tablerate extends Mage_Core_Model_Config_Data
{
    public function _afterSave()
    {
        Mage::getResourceModel('shipping/carrier_tablerate')->uploadAndImport($this);
    }
}

这里通过在模型保存后调用的_afterSave方法,将刚刚上传的文件信息来更新shipping/carrier_tablerate模型。

<upload_dir />和<base_url />

这两个标签都用于拥有<backend_model>adminhtml/system_config_backend_image</backend_model>后台模型的<frontend_type>image</frontend_type>项。用于指定上传图片存储地址以及在<img>标签中调取图片基础的网址路径。

我们先来说说<upload_dir />标签

<upload_dir config="system/filesystem/media" scope_info="1">sales/store/logo</upload_dir>

上面的代码指定了三个内容

  1. 上传图片的基本路径
  2. 相对基本路径这个图片项上传的路径
  3. 当前配置域是否添加到路径之中

上传图片基本路径通过config属性指定,即为system/filesystem/media,这个指定的是系统配置路径,也就是说图片上传的路径不是system/filesystem/media,而是安装Magento后系统system/filesystem/media配置值,即{{root_dir}}/media。

一旦找到了上传图片的基本路径,需要在其后添加子目录来指定图片传的地址,这是通过<upload_dir />中的值来指定的,这里是sales/store/logo,添加完成后整个路径即为:

/path/to/magento/install/media/sales/store/logo

如果scope属性值为1,则当前配置域会转变为路径,假设将图片上传到default域中,上传路径即为:

/path/to/magento/install/media/sales/store/logo/default

而如果在某一个store中上传图片的话,就会得到类似下面这样的路径:

/path/to/magento/install/media/sales/store/logo/store/5

在上传图片时,只有路径中的作用域(scope)和图片名称会保存到配置中,也就是说我们需要指定图片的基础url

<base_url type="media" scope_info="1">sales/store/logo</base_url>

scope_info和文本节点与<upload_dir />相同,<base_url />的不同之处在于它设置的是图片网址的基础地址。你可能已经猜到,基础路径通过type属性来进行设置,其值会通过全局Mage对象传入getBaseUrl方法来设定图片的基础路径。上例中如得到如下这样的调用:

Mage::getBaseUrl('media')

实际代码可以查看在app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Image.php中的如下类:

class Mage_Adminhtml_Block_System_Config_Form_Field_Image extends Varien_Data_Form_Element_Image
{

    /**
     * Get image preview url
     *
     * @return string
     */
    protected function _getUrl()
    {
        $url = parent::_getUrl();

        $config = $this->getFieldConfig();
        /* @var $config Varien_Simplexml_Element */
        if (!empty($config->base_url)) {
            $el = $config->descend('base_url');
            $urlType = empty($el['type']) ? 'link' : (string)$el['type'];
            $url = Mage::getBaseUrl($urlType) . (string)$config->base_url . '/' . $url;
        }

        return $url;
    }

}

需要注意的是这个基础地址并不是在Magento中各处用于生成配置过图片的完整路径,应该只是作用于后台配置中的图片预览。

 

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框架要复杂一些,但是具有超强的灵活性。