本博客包含多个文档和书籍的翻译,但有能力者推荐阅读英文原版

Magento开发系列之一 基础知识

Coding Alan 4年前 (2015-08-05) 825次浏览 0个评论

代码模块化

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

Magento开发系列之一 基础知识

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

Magento开发系列之一 基础知识
想要修改或继承Magento的代码时,不应在系统core代码上进行修改,而应在local或community代码池内新建一个模块

app/code/local/Modulename

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

基于配置的MVC

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

Magento开发系列之一 基础知识

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

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

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

控制器

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

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

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

Front Name: catalog

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

Controller Name:category

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

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

其中的内容类似:

class Mage_Catalog_CategoryController extends Mage_Core_Controller_Front_Action
{
}

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

Action Name:view

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

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

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

Paramater/Value – id/25

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

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

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

 多路由(Routers)

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

基于环境的URI模型载入

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

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

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

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

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

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

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

Magento的模型

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

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

 

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

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

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

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

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

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

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

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

Helpers

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

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

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

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

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

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

 Layouts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

$this->getChildHtml('order_items')

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

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

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

$this->getChildHtml('foobar');

 Observers

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

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

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

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

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

类重载

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

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

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

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

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

class Packagename_Modulename_Model_Foobazproduct extends Mage_Catalog_Model_Product
{
}

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

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

}

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

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

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

喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址