代码模块化
Magento采用Model-View-Controller(MVC)架构,Controller, Model都会放在单独的文件夹里,文件会根据功能进行分组,这种分组在Magento中称为模块(module)。
在Magento中通常一个模块会包含 Controllers, Models, Helpers, Blocks等目录,比如app/code/core/Mage/Checkout模板下的文件夹结构:
想要修改或继承Magento的代码时,不应在系统core代码上进行修改,而应在local或community代码池内新建一个模块
app/code/local/Modulename
代码包(Package,或称为命名空间 – Namespace)用于区分开发代码的公司和组织,进而避免在进行代码分享时出现相互覆盖的情况。新建好模块后,还应在app/etc/modules内添加一个XML文件用于告知Magento你新建模块的位置。这个XML文件可用于指定单一模块,命名格式为Packagename_Modulename.xml,也可用于指定命名空间内的多个模块,命名格式为Packagename_All.xml,比如在app/etc/modules内在Mage_All.xml文件。但是并不推荐在单个文件中指定多个模块,因为这样就失去了分开定义模块的意义。
基于配置的MVC
Magento是一套基于配置文件(configuration-based)的MVC系统,有别于传统的基于惯例(convention-based)的MVC系统。在基于惯例的MVC中,添加一个控制器或模型,只需创建一个文件或类,系统会自动运行。
而在像Magento这样的基于配置文件的MVC系统中,除了添加新文件或类外,还需要告知系统所创建的类或类群的名称,就是通过每个模块中的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用于
- 检测URL地址
- 基于既定规则,将URL解析成控制器类和Action方法(这步称为Routing)
- 实例化控制器类并调用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
可能你现在还不理解上面各标签的具体意义,不过不用担心,后面会详细的说明。注意
多路由(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
"Hey, Magento, add these additional Blocks under the "content" Block of the skeleton"
或
"Hey, Magento, add these additional Blocks under the "left column" Block of the skeleton"
程序上可以在控制器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="left"> <block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml"/> </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="left"> <block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml"> <block type="core/template" name="foobar" template="foo/baz/bar.phtml"/> </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的类重载系统允许你和系统间进行下面的对话
"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".
如果你愿意,可以在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>
这是需要提一下的是,你所写的模块中的某一个类重载另一个模块中的某一个类时,并不会重载整个模块。这就保证了在对某些方法进行修改时不必担心该模块中其它的内容。