magento

模型层的实现是任何一个MVC框架的重要组成部分。它用来实现应用程序的数据,并且大部分应用程序在没有数据的情况下都是一堆废柴。相对于其他PHP MVC框架,Magento模型在系统中扮演了一个更为重要的角色,因为它包含了通常应用于控制器和助手方法中的业务逻辑。

传统的PHP MVC模型

如果说MVC架构的定义有些模糊,那么模型的定义就更为模糊了。早在MVC模式被PHP开发者普遍接受之前,数据的交互通常是使用原始的SQL语句或者SQL抽象进行。开发者必须很多数据库查询语句,而不用考虑在模型化哪个对象。

此处省略三段关于传统PHP MVC模型层以及ORM的探讨,直接进入正题。

Magento 模型

毫无疑问Magento实现了ORM模式。尽管Zend Framework的SQL抽象层能够正常使用,大部分的数据交互依然是通过内置的Magento模型,以及用户自己构建的模型完成。Magento系统拥有一个高度灵活,高度抽象的模型层。

Magento模型解剖

绝大部分Magento模型可以被分为两类。基础的,ActiveRecord,或者说是“一张表,一个对象”的模型;另外一种是Entity Attribute Value(EAV)模型。每个模型都包含一个模型收集(Model Collection)。收集(Collections)是用来同时操作多个Magento模型实例的对象。Magento团队通过实现PHP的IteratorAggregate标准库接口和Countable,从而允许每个模型类型拥有自己的收集类型。如果你对PHP标准库不是很熟悉,可以将模型收集想象成拥有方法可以使用的数组。

Magento模型不包含任何连接数据库的代码。取而代之,每个模型使用两个modelResource类(一个读取,一个写入),它们通过read and write adapter objects与数据库进行交互。通过解耦模型与数据库交互代码,理论上可以通过构建新的资源类来满足任意不同的数据库平台,并且保持模型的完整性。

创建一个基础的Magento模型

下面我们开始创建一个基础的Magento模型,我们以简单的weblog博客为例,构建一个模型,总的分为以下几步。

  • 创建“Weblog”模块
  • 为模型创建一张表,模型命名为Blogpost
  • 添加模型信息到配置文件
  • 添加模型资源信息到配置文件
  • 添加Read Adapter信息到配置文件
  • 添加Write Adapter信息到配置文件
  • 为Blogpost模型添加PHP类文件
  • foo
  • 初始化模型

创建 Weblog 模块

通过之前几章的学习,创建一个新的空模块应该没有问题啦,这里我们跳过这些细节,假设你已经创建了一个名为Weblog的空模块。完成之后,我们为Index控制器设置路由规则。这里依然假设我们的Package命名为Magentotutorial。

在Magentotutorial/Weblog/etc/config.xml文件中,加入一下路由规则,

01

02

03

04

05

06

07

08

09

10

11
<frontend>
    <routers>
        <weblog>
            <use>standard</use>
            <args>
                <module>Magentotutorial_Weblog</module>
                <frontName>weblog</frontName>
             </args>
        </weblog>
    </routers>
</frontend>

然后添加以下代码到Index控制器中,该文件位于Magentotutorial/Weblog/controller/IndexController.php。

01

02

03

04

05
class Magentotutorial_Weblog_IndexController extends Mage_Core_Controller_Front_Action {
    public function testModelAction() {
        echo 'Setup!';
    }
}

清空Magento缓存,根据你的安装路径,访问类似下面的地址,

http://example.com/weblog/index/testModel

创建数据库表

Magento系统能够自动创建和更改数据库模式,这里为了演示,我们先手动为模型创建一个表。使用命令行或你最喜欢的MySQL GUI工具,创建下表,

01

02

03

04

05

06

07

08
CREATE TABLE `blog_posts` (
`blogpost_id` int(11) NOT NULL auto_increment,
`title` text,
`post` text,
`date` datetime default NULL,
`timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP,
PRIMARY KEY  (`blogpost_id`)
)

然后填充一些数据到表中,

01
INSERT INTO `blog_posts` VALUES (1,'My New Title','This is a blog post','2010-07-01 00:00:00','2010-07-02 23:12:30');

创建模型及其配置文件

创建Weblog的模型及其配置文件需要以下五步完成,

  • 在模块中启用模型
  • 在模块中启用模型资源(Model Resources)
  • 在模型资源中添加实体“entity”,对于简单的模型来说,该实体即表名
  • 为模型资源指定读取适配器(Read Adapter)
  • 为模型资源指定写入适配器(Writer adapter)

在Magento中实例化一个模型,可以使用如下语法,

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

getmodel()方法里的URI的第一部分叫做模型组名(Model Group Name)。考虑到Magento为类使用__autoload方法,所以该模型组名必须是模块的小写形式。该URI的第二部分是你的模型名的小写形式。

接着,我们开始添加模型的配置代码到模块的config.xml文件中。

01

02

03

04

05

06

07

08

09

10

11

12

13

14
<global>
    <!-- ... -->
    <models>
        <weblog>
            <class>Magentotutorial_Weblog_Model</class>
            <!--
                need to create our own resource, cant just
                use core_mysql4
            -->
            <resourceModel>weblog_mysql4</resourceModel>
        </weblog>
    </models>
    <!-- ... -->
</global>

最外层的<weblog />标签是模型组名,应该匹配模块名。<class />中的值是weblog组中所有的模型都拥有的BASE名。<resourceModel />标签指定weblog组中的模型应该使用哪种模型资源,这里我们先记得它是由模型组名加“mysql4”。

现在让我们清理下Magento缓存,尝试下实例化这个blogpost模型。在testModelAction()中,添加如下代码。

01

02

03

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

刷新页面之后,你会看到系统抛出了异常,大概如下,

include(Magentotutorial/Weblog/Model/Blogpost.php) [function.include]: failed to open stream: No such file or directory

由于在上面那段代码中,试图引用‘weblog/blogpost’模型,Magento会实例化下面这个类,

Magentotutorial_Weblog_Model_Blogpost

但是此时我们还没有创建这个文件。所以系统会抛出上面的异常。下面我们来创建该类,文件路径位于,

File: app/code/local/Magentotutorial/Weblog/Model/Blogpost.php
01

02

03

04

05

06

07
class Magentotutorial_Weblog_Model_Blogpost extends Mage_Core_Model_Abstract
{
    protected function _construct()
    {
        $this->_init('weblog/blogpost');
    }
}

刷新页面之后,异常就被该类名所取代了。所有的基础模型都必须扩展Mage_Core_Model_Abstract类。这个抽象类强制你必须实现一个名为_construct的方法。此方法会调用该类的_init方法,并需要传递在getModel()方法中的参数。

全局配置和模型资源

到此为止,我们已经成功设置了自定义的模型。接着,我们需要设置它的模型资源。模型资源包含与数据库交互的代码。在上一小节中,我们在配置文件中添加了如下代码,

01
<resourceModel>weblog_mysql4</resourceModel>

在<resourceModel />中的值会实例化一个模型资源类。尽管你从不需要手动调用它,当任何在weblog组中的模型需要与数据库交互时,Magento会调用以下方法获取模型资源,

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

重申一次,weblog是模型组名,blogpost是模型名。Mage::getResourceModel方法使用weblog/blogpost URI来检查全局配置文件,并获取<resourceModel>中的值(在这里,是weblog_mysql4)。然后,下列URI地址的模型类将会被实例化。

weblog_mysql4/blogpost

资源模型的配置与模型的配置在XML配置文件中的相同节点呢,下面我们在<models>节点中添加下列代码,

01

02

03

04

05

06

07

08

09
<global>
    <!-- ... -->
    <models>
        <!-- ... -->
        <weblog_mysql4>
            <class>Magentotutorial_Weblog_Model_Mysql4</class>
        </weblog_mysql4>
    </models>
</global>

这里设置的<weblog_mysql4 />标签,就是刚刚在<resourceModel />标签中设置的值。<class />节点中的值是使用的资源模型的基础命名,它的命名方式大概如下

Packagename_Modulename_Model_Mysql4

现在,我们成功配置了资源模型,来试着从模型数据中读取一些信息吧。稍稍添加一些代码到testModelAction()方法中。

01

02

03

04

05

06

07

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

清空Magento缓存,在浏览器中打开如下地址,

http://example.com/weblog/index/testModel/id/1

好吧,又一次看到系统抛出了异常,大概如下,

Warning: include(Magentotutorial/Weblog/Model/Mysql4/Blogpost.php) [function.include]: failed to open stream: No such file ….

上面我们提到过,当与数据库交互时,会实例化资源模型类,这里系统提示我们需要为该模型添加一个模型资源类。(译者注:本文提到过,Magento的模型本身与数据库连接及交互是相互独立的,所以在模型没有与数据库交互之前,例如在本篇第一次使用getModel()方法时,系统不会抛出关于实例化模型以外的异常。)每个模型都有模型资源类,添加该类到下列路径的文件中,

File: app/code/local/Magentotutorial/Weblog/Model/Mysql4/Blogpost.php
01

02

03

04

05

06
class Magentotutorial_Weblog_Model_Mysql4_Blogpost extends Mage_Core_Model_Mysql4_Abstract{
    protected function _construct()
    {
        $this->_init('weblog/blogpost', 'blogpost_id');
    }
}

可以看到,_init方法的第一个参数依旧是模型组名/模型名。参数二是数据库字段,可以是任意唯一字段,大多数情况下,参数二可以指定为主键。清空缓存,刷新页面,页面中会显示如下内容,

Loading the blogpost with an ID of 1

array

empty

没有异常?可是也没有正常读取到数据!接着该做些什么呢?每一个模型组都拥有一个读取适配器和写入适配器。Magento允许模型使用默认的适配器,也可以使用开发者自己开发的适配器。无论使用哪一种,我们需要告诉Magento系统关于适配器的配置。这里,我们在配置文件中添加一个新的tag节点,<resources />到<global />节点中。

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15
<global>
    <!-- ... -->
    <resources>
        <weblog_write>
            <connection>
                <use>core_write</use>
            </connection>
        </weblog_write>
        <weblog_read>
            <connection>
                <use>core_read</use>
            </connection>
        </weblog_read>
    </resources>
</global>

这里我们在<resources />中添加了两个子节点。一个用来写入,另一个用来读取。标签命名(<weblog_write />和<weblog_read />)根据上面定义的模型组名。完成改配置文件之后,清空Magento缓存,再次刷新页面,然后…

Can’t retrieve entity config: weblog/blogpost

又一次出现异常了!一起理清下思路,在使用模型URI weblog/blogpost时,Magento系统被告知我们想使用模型组weblog,以及blogpost实体。在扩展Mage_Core_Model_Mysql4_Abstract的简单模型中,实体相对应一张表。这里,该表即我们上面创建的blog_post表,添加该实体到配置文件中。

01

02

03

04

05

06

07

08

09

10

11
<models>
    <!-- ... --->
    <weblog_mysql4>
        <class>Magentotutorial_Weblog_Model_Mysql4</class>
        <entities>
            <blogpost>
                <table>blog_posts</table>
            </blogpost>
        </entities>
    </weblog_mysql4>
</models>

在配置文件中的resource模型节点中,添加新的<entities />节点。现在,在配置文件中终于出现了以刚才创建的表名命名的节点,从而为该模型指定相关的数据库表。

清空Magento缓存,刷新页面,OK…

Loading the blogpost with an ID of 2

Loading the blogpost with an ID of 1

array
‘blogpost_id’ => string ’1′ (length=1)
‘title’ => string ‘My New Title’ (length=12)
‘post’ => string ‘This is a blog post’ (length=19)
‘date’ => string ’2009-07-01 00:00:00′ (length=19)
‘timestamp’ => string ’2009-07-02 16:12:30′ (length=19)

好啦!经过这么长一个过程我们终于成功从数据库中读取到了数据,更为重要的是,我们完成了一个崭新的Magento模型的配置!

基础的Magento模型操作

Magento模型都继承自Varien_Object类。该类是Magento系统核心库中的一部分,而非Magento核心模块。可以在下列路径找到该对象。

lib/Varien/Object.php

Magento模型将数据保存在一个protected的_data属性中。Varien_Object类提供给我们很多方法,可以使用这些方法读取这些数据。你已经使用过了getData()方法,该方法返回一个包含字段/值的数组。你也可以通过传递字段名作为该方法的参数来获取相应字段的值

01

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

还有一个getOrigData方法,which will return the Model data as it was when the object was initially populated, (working with the protected _origData method).这段就不翻译了。

01

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

Varien_Object类通过PHP的魔术方法__call实现了一些特殊的方法。你可以通过get,set,unset以及has加上驼峰命名的字段名的方式,获取、设置、unset及查看任意存在的字段值。

01

02

03

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

正因为如此,你可能会以小写字母及下划线来命名数据库字段。不过,最近版本的Magento已经舍弃了这种语法,转而实现PHP的数组连接(ArrayAccess)接口。

01

02

03
$id = $model->['blogpost_id'];
$model->['blogpost_id'] = 25;
//etc...

That said, you’re likely to see both techniques used throughout the Magento code base, as well as third party extensions.这段意思应该是说你可以在Magento或第三方扩展中看到上面两种语法格式。

Magento的CRUD操作

Magento模型通过load(),sava(),delete()方法,提供基础的Create,Read,Update和Delete功能。在上面的控制器方法中,我们已经使用了load()方法。当传递一个参数到load()方法中,该方法会返回与该参数相对应的id字段(在模型资源中设置)的一条记录。

01
$blogpost->load(1);

save()方法允许你插入新数据到模型中,或更新已经存在的数据。添加如下代码到控制器中。

01

02

03

04

05

06

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

然后在浏览器中访问以下地址,

http://example.com/weblog/index/createNewPost

这时你会看到数据库表中新增了一条数据,然后在控制器中加入编辑功能。

01

02

03

04

05

06

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

最后,加入下列代码,实现删除功能。

01

02

03

04

05

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

Magento的模型收集 Model Collections

对于单独一个模型的操作固然很有用,但是多数时候,我们会同时操作多个模型。比返回多个模型的一个多维嵌套数组更好的是,在Magento中,每个模型类型都有一个唯一的收集对象与其关联。这些对象实现了PHP IteratorAggregate和Countable接口,这意味着它们可以被传递到count函数,并使用for each结构循环出数据。

我们将在第八章具体介绍Magento的收集机制,现在我们先简要介绍下它的设置和使用。添加如下代码到控制器中,然后再浏览器中访问该地址。

01

02

03

04

05

06

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

访问如下地址,

http://example.com/weblog/index/showAllBlogPosts

然后,是的,系统再一次抛出异常。

Warning: include(Magentotutorial/Weblog/Model/Mysql4/Blogpost/Collection.php) [function.include]: failed to open stream

看下上面的PHP代码,你就应该对系统抛出异常不会感到太惊讶了吧?我们需要添加一个类来定义Blogpost的模型收集。每个模型资源拥有一个_resourceCollectionName保护属性,它包含了用来识别收集的URI。

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

默认的,该URI也用来识别模型资源,以字符串”_collection”结尾。Magento将收集归为模型资源的一部分,所以该URI转换为类名之后如下,

Magentotutorial_Weblog_Model_Mysql4_Blogpost_Collection

添加下面的模型收集类到如下路径,

File: app/code/local/Magentotutorial/Weblog/Model/Mysql4/Blogpost/Collection.php
01

02

03

04

05

06
class Magentotutorial_Weblog_Model_Mysql4_Blogpost_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {
    protected function _construct()
    {
        $this->_init('weblog/blogpost');
    }
}

和其他类一样,我们需要使用该模型的URI(weblog/blogpsot)来_init模型收集。最后,在浏览器中访问模型收集的地址,就能成功返回文章的数据信息了。

Magento核心模型

恭喜你,到这里,说明你已经完成了Magento模型的配置。在后面的教程中,我们会更深入的讲解Magento的高级EAV模型。

还要提到一点,上面文章中,我们说到所有的Magento模型都继承自Mage_Core_Model_Abstract类。这并不是100%正确。因为有些模型直接继承自Varien_Object。当然,这都不会影响到任何你创建的模型,说明这些只是为了让开发者能够更好的理解Magento的代码。

magento

我们接着研究Magento。根据我们第二章讲的Magento MVC的架构,我们接下来应该讲模型(Model),但是我们跳过模型先来看布局和块。和一些流行的PHP MVC架构不同的是,Magento的执行控制器不直接将数据传给试图,相反的视图将直接引用模型,从模型取数据。这样的设计就导致了视图被拆分成两部 分,块(Block)和模板(Template)。块是PHP对象,而模板是原始PHP文件,混合了XHTML和PHP代码(也就是把PHP作为模板语言 来使用了)。每一个块都和一个唯一的模板文件绑定。在模板文件phtml中,“$this”就是指该模板文件对应的快对象。

让我们来看一个例子

File: app/design/frontend/base/default/template/catalog/product/list.phtml
你将看到如下代码

  1. <?php $_productCollection=$this->getLoadedProductCollection() ?>
  2. <?php if(!$_productCollection->count()): ?>
  3. <p class="note-msg"><?php echo $this->__('There are no products matching the selection.') ?></p>
  4. <?php else: ?>

这里“getLoadedProductCollection”方法可以在这个模板的块对象“Mage_Catalog_Block_Product_List”中找到

File: app/code/core/Mage/Catalog/Block/Product/List.php
...
public function getLoadedProductCollection()
{
return $this->_getProductCollection();
}
...

块的“_getProductCollection”方法会实例化模型,并读取数据然后返回给模板。

嵌套块
Magento把视图分离成块和模板的真正强大之处在于“getChildHtml”方法。这个方法可以让你实现在块中嵌套块的功能。顶层的块调用第二层的块,然后是第三层……这就是Magento如何输出HTML的。让我们来看一下单列的顶层模板

File: app/design/frontend/base/default/template/page/1column.phtm

  1. <?php
  2. /**
  3. * Template for Mage_Page_Block_Html
  4. */
  5. ?>
  6. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  7. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
  8. <head>
  9. <?php echo $this->getChildHtml('head') ?>
  10. </head>
  11. <body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
  12. <?php echo $this->getChildHtml('after_body_start') ?>
  13. <div class="wrapper">
  14. <?php echo $this->getChildHtml('global_notices') ?>
  15. <div class="page">
  16. <?php echo $this->getChildHtml('header') ?>
  17. <div class="main-container col1-layout">
  18. <div class="main">
  19. <?php echo $this->getChildHtml('breadcrumbs') ?>
  20. <div class="col-main">
  21. <?php echo $this->getChildHtml('global_messages') ?>
  22. <?php echo $this->getChildHtml('content') ?>
  23. </div>
  24. </div>
  25. </div>
  26. <?php echo $this->getChildHtml('footer') ?>
  27. <?php echo $this->getChildHtml('before_body_end') ?>
  28. </div>
  29. </div>
  30. <?php echo $this->getAbsoluteFooter() ?>
  31. </body>
  32. </html>

我们可以看到这个模板里面很多地调用了“$this->getChildHtml(…)”。每次调用都会引入另外一个块的HTML内容,直到最底层的块。

布局对象
看到这里,你可能有这样的疑问
Magento怎么知道在一个页面上要用那些块?
Magento怎么知道哪一个块是顶层块?
“$this->getChildHtml(…)”里面的参数是什么意思?块的名字吗?
Magento引入了布局对象(Layout Object)来解决上面的那些问题。布局对象(或者说布局文件)就是一个XML文件,定义了一个页面包含了哪些块,并且定义了哪个块是顶层块。

在第二章的时候我们在执行方法(Action Method)里面直接输出了HTML内容。现在我们要为我们的Hello World模块创建一个简单的HTML模板。首先我们要创建如下文件

app/design/frontend/default/default/layout/local.xml
包含以下内容

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <layout version="0.1.0">
  3. <helloworld_index_index>
  4. <reference name="root">
  5. <block type="page/html" name="root" output="toHtml" template="helloworld/simple_page.phtml"/>
  6. </reference>
  7. </helloworld_index_index>
  8. </layout>

再创建如下文件

app/design/frontend/default/default/template/helloworld/simple_page.phtml
包含以下内容

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  2. "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3. <html xmlns="http://www.w3.org/1999/xhtml">
  4. <head>
  5. <title>Untitled</title>
  6. <style type="text/css">
  7. body {
  8. background-color:#f00;
  9. }
  10. </style>
  11. </head>
  12. <body>
  13. <h4>Links</h4>
  14. <?php echo $this->getChildHtml('top.links'); ?>
  15. <?php echo $this->getChildHtml('customer_form_register'); ?>
  16. </body>
  17. </html>

最后,我们要在执行控制器里面调用布局文件,开始输出HTML。修改执行方法如下

  1. public function indexAction() {
  2. //remove our previous echo
  3. //echo 'Hello Index!';
  4. $this->loadLayout();
  5. $this->renderLayout();
  6. }

清空Magento缓存,访问URL “http://exmaple.com/helloworld/index/index”。你应该看到一个纯红色背景的页面。这个页面的源代码应该和我们创建的文件“simple_page.phtml”一模一样。

究竟是怎么回事呢?
也许你看到这里一头雾水,没关系,我们来慢慢解释。首先你得安装一个 Layout Viewer 模块,这和我们第一章讲的 Config Viewer 模块很相似,都是查看Magento的内部信息。安装完这个模块之后【注:你需要参照第一章的内容,为这个模块创建“app/etc/modules /App_Layoutviewer.xml”】,打开如下URL

http://example.com/helloworld/index/index?showLayout=page
你看到的是你正在请求的页面的布局文件。它是由block,reference和remove组成的。当你在执行方法中调用“loadLayout”时,Magento会做如下处理
生成这个布局文件
为每一个block和reference标签实例化一个块对象。块对象的类名是通过标签的name属性来查找的。这些块对象被存储在布局对象的_blocks数组中
如果block标签包含了output属性,那么这个块的名字和output属性的值会被添加到布局对象的_output数组中
然后,当你在执行方法中调用“renderLayout”方法时,Magento会遍历_output数组中所有的块名字,从_blocks数组中获得该 名字的块,并调用块对象中使用output属性的值作为名字的函数。这个函数往往是“toHtml”。这个output属性也告诉Magento这里就是 输出HTML的起点,也就是顶层块。【注:直接阅读Layout类的代码应该比较容易理解这里的逻辑

  1. File: app/code/core/Mage/Core/Model/Layout.php
  2. public function getOutput()
  3. {
  4. $out = '';
  5. if (!emptyempty($this->_output)) {
  6. foreach ($this->_output as $callback) {
  7. $out .= $this->getBlock($callback[0])->$callback[1]();
  8. }
  9. }
  10. return $out;
  11. }

从这里我们也可以看出,一个页面的布局文件可以拥有多个顶层块。】

下面我们要讲解块对象是如何被实例化的,这个布局文件时如何被生成的,最后我们将动手做一个例子来实践这一章讲的内容。

实例化块对象
在布局文件中,block和reference标签有一个“type”属性,这个属性其实是一个URI

<block type="page/html" ...
<block type="page/template_links"...
Magento就是通过这个URI是用来查找块对应的类名。这个URI分为两部分,第一部分“page”是用来在全局配置中查找一个基本类名,第二部分“html”或者“template_link”将被添加到基本类名后面生成一个具体的将被实例化的类名。

我们以“page/html”为例。首先Magento在全局配置中找到节点

/global/blocks/page
有以下内容

<page>
<class>
Mage_Page_Block
</class>
</page>
这里我们拿到了一个基本类名“Mage_Page_Block”,然后添加URI的第二部分“html”到基本类名后面,我们就得到最终的块对象的类名 “Mage_Page_Block_Html”。块的类名在Magento中被称为“分组类名”(Grouped Class Names),这些类都用相似的方法被实例化。我们将在以后的章节中详细介绍这个概念。

block和reference的区别
我们上面提到block和reference都会实例化块对象,那么它们究竟有什么区别呢? reference在布局文件中是用来表示替换一个已经存在的块,举个例子

<block type="page/html" name="root" output="toHtml" template="page/2columns-left.phtml">
<!-- ... sub blocks ... -->
</block>
<!-- ... -->
<reference name="root">
<block type="page/someothertype" name="root" template="path/to/some/other/template" />
<!-- ... sub blocks ... -->
</reference>
Magento首先创建了一个名叫“root”的块。然后,它有发现了一个引用(reference)的名字也叫“root”,Magento会把原来那个“root”块替换成reference标签里面的那个快。

再来看看我们之前创建那个local.xml

  1. <layout version="0.1.0">
  2. <default>
  3. <reference name="root">
  4. <block type="page/html" name="root" output="toHtml" template="helloworld/simple_page.phtml" />
  5. </reference>
  6. </default>
  7. </layout>

在这里,块“root”被我们用reference替换了,指向了一个不同的模板文件。

布局文件是如何生成的
现在我们对布局文件已经有所了解了,但是这个布局文件是那里来的呢?要回答这个问题,我们得引入Magento中的另外两个概念,操作(Handle)和包布局(Package Layout)。

操作
Magento会为每一个页面请求生成几个不同的操作。我们的Layout View模块可以显示这些处理器

http://example.com/helloworld/index/index?showLayout=handles
你应该看到类似如下列表的列表(和你的配置有关)

Handles For This Request
1. default
2. STORE_default
3. THEME_frontend_default_gap
4. helloworld_index_index
5. customer_logged_out
它们每一个都是一个操作的名字。我们可以在Magento系统的不同的地方配置操作。在这里我们需要关注两个操作 “default” 和 “helloworld_index_index”。“default”处理器是Magento的默认处理器,参与每一个请求的处理。 “helloworld_index_index”处理器的名字是frontname “helloworld”加上执行控制器的名字“index”再加上执行方法的名字“index”。这说明执行控制器的每一个执行方法都有一个相应的操 作。

我们说过“index”是Magento默认的执行控制器和执行方法的名字,所以以下请求的操作名字也是“helloworld_index_index”。

http://example.com/helloworld/?showLayout=handles

包布局
包布局和我们以前讲过的全局配置有些相似。它是一个巨大的XML文档包含了Magento所有的布局配置。我们可以通过以Layout View模块来查看包布局,请求一下URL

http://example.com/helloworld/index/index?showLayout=package
你可能要等一会儿才能看到输出,因为文件很大。如果你的浏览器在渲染XML的时候卡死了,建议你换成text格式的

http://example.com/helloworld/index/index?showLayout=package&amp;showLayoutFormat=text
假设你选择的是XML格式输出,那么你应该看到一个巨大的XML文件,这就是包布局。这个文件是Magento动态生成的,合并当前主题(theme)下面所有的布局文件。如果你用的是默认安装的话,这些布局文件在以下目录

app/design/frontend/base/default/layout/
其实在全局配置中,有一个updates节点下面定义了所有将被装载的布局文件

<layout>
<updates>
<core>
<file>core.xml</file>
</core>
<page>
<file>page.xml</file>
</page>
...
</updates>
</layout>
当这些文件被装载以后,Magento还会装载最后一个布局文件,helloworld.xml,也就是我们之前新建的那个文件。我们可以通过这个文件来定制Magento的布局。

结合操作和包布局
在包布局文件中,我们可以看到一些熟悉的标签block,reference等等,但是他们都包含在一下这些标签中

<default />
<catalogsearch_advanced_index />
etc...
这些就是操作标签。对于每个特定的请求来说,针对这个请求的布局文件是由包布局中所有和这个请求相关的操作标签组成的。比如我们上面的例子,和请求相关的操作标签如下

<default />
<store_bare_us />
<theme_frontend_default_default />
<helloworld_index_index />
<customer_logged_out />
所以,针对请求

http://example.com/helloworld/index/index
布局文件就是包布局中上面这些标签的内容组合。在包布局文件中,还有一个标签值得我们注意。我们可以通过这个标签引入另外一个操作标签。比如

<customer_account_index>
<!-- ... -->
<update handle="customer_account"/>
!-- ... -->
</customer_account_index>
这段代码的意思是,如果一个请求包含了“customer_acount_index”操作,那么这个请求的布局文件也应该包含“customer_account”操作标签下面的block和reference。

更新我们的例子
好了,理论讲完了,让我们来修改我们的例子,把这一章的内容实践一下。我们重新来看local.xml

<layout version="0.1.0">
<default>
<reference name="root">
<block type="page/html" name="root" output="toHtml" template="helloworld/simple_page.phtml" />
</reference>
</default>
</layout>
我们用一个引用(reference)覆盖了名为“root”的块。然后定义了一个新的块,指向了一个不同的模板文件。我们把这个引用放在default 操作标签下面,那就说明这个Layout将对所有的请求有效。如果你访问Magento自带的一些页面,你会发现它们要么是空白,要么就是和我们 “hello world”例子的红色背景,但这并不是我们想要的效果。我们来修改一下local.xml,让我们的模板仅对“helloworld”的请求有效。

<layout version="0.1.0">
<helloworld_index_index>
<reference name="root">
<block type="page/html" name="root" output="toHtml" template="helloworld/simple_page.phtml" />
</reference>
</helloworld_index_index>
</layout>
我们把操作标签换成了“helloworld_index_index”。清空Magento缓存,重新访问Magento的各个页面,你应该发现都恢复了正常,但是针对”hello world”模块的请求页面还是我们自定义的那个。

目前我们只实现了一个“index”执行函数,现在我们来实现“goodbye”执行函数。修改我们的执行控制器代码如下

public function goodbyeAction() {
$this->loadLayout();
$this->renderLayout();
}
但是你访问一下页面的时候你还是会看到Magento的默认布局

http://example.com/helloworld/index/goodbye
那是因为我们没有为这个请求定义布局。我们需要在local.xml中添加“helloworld_index_goodbye”标签。由于“index”请求和“goodbye”请求我们要套用的布局是一样的,所以我们将用标签来重用已有的配置

<layout version="0.1.0">
<!-- ... -->
<helloworld_index_goodbye>
<update handle="helloworld_index_index" />
</helloworld_index_goodbye>
</layout>
清空Magento缓存,请求以下URL

http://example.com/helloworld/index/index
http://example.com/helloworld/index/goodbye
你将会得到两个完全相同的页面。

开始输出和getChildHtml方法
在Magento默认的配置下,HTML输出是从名为“root”的块开始(其实是因为这个块拥有output属性【注:任何一个拥有output属性的 块都是顶层块,在拥有多个顶层块的情况下Magento将按照块定义的先后顺序输出HTML】)。我们覆盖了“root”块的模板

template="helloworld/simple_page.phtml"
模板文件的查找路径是当前主题(theme)的根目录,Magento默认设置时这里

app/design/frontend/base/default

为页面加入内容
到目前为止,我们的页面都比较无聊,啥也没有。我们来为页面加点有意义的内容。修改local.xml如下

<helloworld_index_index>
<reference name="root">
<block type="page/html" name="root" template="helloworld/simple_page.phtml">
<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
</block>
</reference>
</helloworld_index_index>
我们在“root”块里面嵌套了一个块“customer_form_register”。这个块是Magento本来就有的,包含了一张用户注册表单。 我们把这个块嵌套进来,那么我们在模板文件里面就能用这个块的内容。使用方法如下,修改simple_page.phtml

<body>
<?php echo $this->getChildHtml('customer_form_register'); ?>
</body>
这里“getChildHtml”的参数就是要引入的块的名字,使用起来相当方便。清空Magento缓存,刷新hello world页面,你应该在红色背景上看到用户注册表单。Magento还有一个块,叫做“top.links”,让我们把它也加进来。修改 simple_page.html

<body>
<h1>Links</h1>
< ?php echo $this->getChildHtml('top.links'); ?>
< ?php echo $this->getChildHtml('customer_form_register'); ?>
</body>
刷新页面,你会发现Links显示出来了,但是“top.links”什么都没有显示。那是因为我们并没有把这个块引入到local.xml,所以 Magento找不到这个块。“getChildHtml”的参数一定要是当前页面的布局文件中声明过的块。这样的话Magento就可以只实例化需要用 到的块,节省了资源,我们也可以根据需要为块设置不同的模板文件。

我们修改helloworld.xml文件如下

<?xml version="1.0" encoding="UTF-8"?>
<layout version="0.1.0">
<helloworld_index_index>
<reference name="root">
<block type="page/html" name="root" output="toHtml" template="helloworld/simple_page.phtml">
<block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
<block type="page/template_links" name="top.links"/>
</block>
</reference>
</helloworld_index_index>
</layout>
清空Magento缓存,刷新页面,你会看到一排链接显示出来了。【注:如果你细心一点的话你会发现“top.links”块没有template属性,那是因为这个块的类中一定定义了默认的模板

protected function _construct()
{
$this->setTemplate('page/template/links.phtml');
}

总结
这一章我们讲解了布局的基础知识。你可能会觉得这个很复杂,但是你也不必过分担心,因为平常使用Magento是不会用到这些知识的,Magento提供 的默认布局应该可以满足大部分需求。对于想要深入研究Magento的开发者来说,理解Magento的布局是至关重要的。布局,块和模板构成了 Magento MVC架构中的View,这也是Magento的特色之一。

magento

提到模型-视图-控制器这种MVC架构,要追溯到Smalltalk编程语言和Xerox Parc。从那个时候开始,就有许多系统将自己描述为MVC架构。这些系统虽然在某些地方有细微差别,但都实现了数据层,逻辑层和前段表现代码的分离。

大部分的PHP MVC框架都拥有以下基本特征,

  • 所有的URL请求会被前端控制器解析
  • 前端控制器会检查请求的URL路径,并从中获取一个控制器和动作名(这个过程叫做路由)
  • 实例化从URL中获取的控制器
  • 该控制器中,与从URL里获取的动作名一致的方法会被调用
  • 根据请求的变量,该方法被实例化或调用方法获取模型
  • 该动作方法从模型中获取到数据,这些获取的信息会被传递到视图中
  • 使用从数据结构中获取到的信息,视图输出HTML代码

相对于“一个PHP文件对应一个页面”来说,这种MVC架构是个巨大的进步。然后对于一些软件工程师来说,它依然是个丑陋的架构。他们经常抱怨说,

  • 前段控制器文件会在全局命名空间中运行
  • 约定型的架构没有配置型架构更具扩展性

比如说,

  • URL路由规则通常是无法扩展的
  • 控制器被绑定在特定的视图
  • 即使该系统拥有重写核心代码的功能,程序员依然只能在无尽的重构中编写代码

Magento团队创建了一个更为抽象的MVC模式,大概的运行过程是,

  • URL地址首先被一个PHP文件解析
  • 该PHP文件会根据解析情况实例化一个Magento应用
  • 这个Magento应用会实例化一个前端控制器对象
  • 接着,前端控制器实例化路由对象
  • 路由对象检查请求的URL地址,并作出相应的匹配
  • 如果匹配成功,相应的控制器和动作会被分发
  • 该控制器会被实例化,并且与动作同名的方法会被调用
  • 被调用的方法根据请求的类型,对相应的模型调用相应的方法以获取数据
  • 结束方法调用之后,控制器会实例化布局对象
  • 根据请求中包含的变量及系统属性(通常叫做句柄),布局对象会为该请求创建一系列的Block对象
  • 布局还会在相应的Block对象中调用输出方法,开始套嵌输出(Blocks之间的套嵌)
  • 每个Block都有相关联的模板文件,Blocks包含PHP逻辑代码,模板文件则负责生成HTML文件
  • Block从模型中获取相关数据,换句话说,控制器并不用来传递数据到视图中

我们会在整个教程中介绍上面的所有部分,本章主要涉及到到前端控制器->路由->动作控制器环节。

Hello World

又到Hello World了,你懂的。这一节主要任务是

  • 创建Hello World模块
  • 配置模块的路由规则
  • 为路由创建动作控制器

创建Hello World模块

首先,我们先为该模块创建好目录结构:

app/code/local/Magentotutorial/Helloworld/Block

app/code/local/Magentotutorial/Helloworld/controllers

app/code/local/Magentotutorial/Helloworld/etc

app/code/local/Magentotutorial/Helloworld/Helper

app/code/local/Magentotutorial/Helloworld/Model

app/code/local/Magentotutorial/Helloworld/sql

然后为这个模块创建配置文件,配置该配置文件路径位于,

app/code/local/Magentotutorial/Helloworld/etc/config.xml

并放入下列代码,

01

02

03

04

05

06

07
<config>
    <modules>
        <Magentotutorial_Helloworld>
            <version>0.1.0</version>
        </Magentototurial_helloworld>
    </modules>
</config>

和之前一样,需要创建一个文件激活该模块,该文件路径位于,

app/etc/modules/Magentotutorial_Helloworld.xml

放入如下代码,

01

02

03

04

05

06

07

08
<config>
    <modules>
        <Magentotutorial_Helloworld>
            <active>true</active>
            <codePool>local</codePool>
        </Magentototurial_helloworld>
    </modules>
</config>

最后,检查下我们是否成功建立Hello Wolrd模块并激活。

  • 清空Magento缓存
  • 在管理员界面中,点击System->Configuration->Advanced.
  • 打开”Disable Modules Output”
  • 大概在底部,会显示Magentotutorial_Helloworld模块

配置模块的路由规则

接着,我们开始配置路由规则,路由会将请求的URL地址分发到一个控制器和它的方法上。不像其它约定型的PHP MVC框架,在Magento中,你需要明确的在全局配置文件中配置路由规则,来告诉URL地址如何匹配对应的控制器和方法。

在config.xml文件中,加入下列代码,

01

02

03

04

05

06

07

08

09

10

11
<frontend>
    <routers>
        <helloworld>
            <use>standard</use>
            <args>
                <module>Magentototurial_Helloworld</module>
                <frontName>helloworld</frontName>
            </args>
        </helloworld>
    </routers>
</frontend>

这块代码里边牵涉到很多术语,接下来一一解析。

<frontend>是什么?

该标签涉及到Magento的一个术语Area。可以将Areas视为一些独立的Magento应用。“frontend” Area是Magento购物车应用的前端表现。”admin” Area是后端管理员应用。”install” Area是用来安装Magento的应用。

为什么配置一个模块的路由要使用复数<routers>呢?

引用一句Phil Karlton关于计算机科学的著名论断:

There are only two hard things in Computer Science: cache invalidation and naming things

意思是说计算机科学中最棘手的两件事就是缓存验证和命名。和许多大型系统一样,Magento同样受到命名的困扰。在全局配置文件书中,可以看到许多这种甚至是丑陋的命名方式。<routers>便是其中之一。该标签通常会包括关于路由规则的配置信息,有时候又会包含实际的路由对象的配置信息。这种命名方式初看起来有些不爽,但是随着你对于Magento系统的深入学习,你会逐渐改变对它的看法。

<frontName>是什么?

当路由器解析URL时,会将URL分为以下几个部分,

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

通过在<frontName>标签中定义”helloworld”值,Magento就能够响应所有以下列URL访问的地址

http://example.com/helloworld/*

很多刚刚接触Magento的开发者都会将frontName与Magento的前端控制器对象搞混淆。实际上它们完全不是一回事。frontName只属于路由。

<helloworld>标签的作用?

该标签必须是当前模块名的小写形式。我们创建的模块是Helloworld,所以该标签应该是<helloworld>。可能你会注意到<frontName>标签中的值也于模块名一致。这个其实是一个不成为的规定,并非是必须一致的。在你的自定义模块中,最好使用模块名与命名空间的组合来命名,以避免命名冲突。

<module>Magentotutorial_Helloworld</module>

该标签的值必须是模块的全名,包含package/namespace名。该配置让系统能够正确定位到控制器文件。

创建动作控制器

完成配置文件之后,接下来需要做的就是创建控制器文件及类。在以下路径创建该类,

app/code/local/Magentotutorial/Helloworld/controllers/IndexController.php

包含以下内容,

01

02

03

04

05
class Magentotutorial_Helloworld_IndexController extends Mage_Core_Controller_Front_Action {
    public function indexAction() {
        echo 'Hello Index';
    }
}

清空缓存,访问下列地址,

http://exmaple.com/helloworld/index/index

也可以访问以下地址,

http://exmaple.com/helloworld/index/index

http://exmaple.com/helloworld/

如果没有出错的话,页面中应该能显示”Hello World”。恭喜你,你已经成功搞定了第一个Magento控制器。

动作控制器文件路径

控制器应该放在模块的controllers文件夹中,系统会自动在这个路径中寻找控制器。

动作控制器命名方式

还记得在config.xml配置文件中的<module>标签吗?

01
<module>Magentotutorial_Helloworld</module>

动作控制器的命名应该遵循以下规则,

  • 以配置文件<module>标签中的值起头(Magentotutorial_Helloworld)
  • 紧接着一个下划线(Magentotutorial_Helloworld_)
  • 再接着是该控制器的名字(Magentotutorial_Helloworld_Index)
  • 最后,加上Controller(Magentotutorial_Helloworld_IndexController)

所有的Magento控制器都是继承自Mage_Core_Controller_Front_Action类。

index/index路径

上文中提到过,Magento的URL地址按照以下规则进行路由解析,

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

那么在下面这个地址中,

http://example.com/helloworld/index/index

URI中的”helloworld”是frontName,后面两个index分别是调用的控制器及方法名,即调用helloworld模块中的IndexController控制器中的indexAction方法。

如果URL地址中缺少控制器及方法部分,Magento默认使用index,所以下面地址访问的页面是相同的。

http://example.com/helloworld/index

http://example.com/helloworld

如果访问的URL地址如下,

http://example.com/checkout/cart/add

Magento会做如下操作,

  • 在全局配置文件中找到使用frontName为checkout的模块(Mage_Checkout)
  • 继续查询cart控制器(Mage_Checkout_CartController)
  • 调用cart控制器下的addAction()方法

控制器路径

让我们试着添加一个非默认方法到控制器中,添加如下代码到IndexController.php

01

02

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

然后访问下面的URL地址:

http://example.com/helloworld/index/goodbye

IndexController继承自Mage_Core_Controller_Front_Action类,有很多方法可以直接使用。例如,除上述URI中提到的三部分之外,其它部分会自动传给一个键值对数组。添加如下代码到IndexController中。

01

02

03

04

05

06

07

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

完成之后访问下面的URL地址

http://example.com/helloworld/index/params?foo=bar&baz=eof

一切正常的话,页面中会显示该URL地址中的参数。最后,对于下面这个URL地址,系统应该如何响应呢?

http://example.com/helloworld/messages/goodbye

该URL中,控制器的名字是messages,所以我们需要创建一个MessagesController控制器,在以下路径创建该文件

app/code/local/Magentotutorial/Helloworld/controllers/MessagesController.php

接着在该控制器中添加goodbyeAction()方法

01

02

03
public function goodbyeAction() {
    echo 'Another Goodbye';
}

关于Magento控制器的介绍已经完成了。虽然看起来比其他的PHP MVC框架要复杂许多,它的高度可扩展性却允许你创建任何你想要的URL结构。

在了解过世界最大的PHP站点,Facebook的后台技术后,今天我们来了解一个百万级PHP站点的网站架构:Poppen.de。Poppen.de是德国的一个社交网站,相对Facebook、Flickr来说是一个很小的网站,但它有一个很好的架构,融合了很多技术,如 Nigix、MySql、CouchDB、Erlang、Memcached、RabbitMQ、PHP、Graphite、Red5以及Tsung。

Poppen.de目前有200万注册用户数、2万并发用户数、每天20万条私有消息、每天25万登录次数。而项目团队有11个开发人员,两个设计,两个系统管理员。该站点的商业模式采用免费增值模式,用户可以使用搜索用户、给好友发送消息、上载图片和视频等功能。

如果用户想享受不受限制发送消息和上载图片,那么就得根据需要支付不同类型的会员服务,视频聊天及网站其他服务也采用同样的策略。

Nginx

Poppen.de 所有的服务都是基于Nginx服务上的。前端有两台Nginx服务器在高峰期提供每分钟15万次请求的负载,每个机器已经有四年寿命,并且只有一个CPU 和3GB RAM。Poppen.de拥有三台独立的图像服务器,由三台Nginx服务器为*.bilder.poppen.de提供每分钟8万次请求服务。

Nginx 架构中一个很酷的设计就是有很多请求是由Memcached处理的,因此请求从缓存中获取内容而不需要直接访问PHP机器。比如,用户信息页(user profile)是网站需要密集处理的内容,如果把用户信息页全部缓存到Memcached上,那么请求直接从Memcached上获取内容。 Poppen.de的Memcached每分钟可以处理8000次请求。

架构中有三个Nginx图像服务器提供本地图像缓存,用户上载图 像到一个中央文件服务器。当向这三个Nginx之一中请求图像时,如果服务器本地中没有存在该图像,则从中央文件服务器下载到该服务器上作缓存并提供服 务。这种负载均衡的分布式图像服务器架构设计可以减轻主要存储设备的负载。

PHP-FPM

该网站运行在PHP- FPM上。共有28台双CPU、6GB内存的PHP机器,每个机器上运行100个PHP-FPM的工作线程。使用启用了APC的PHP5.3.x。 PHP5.3可以降低CPU和内存使用率的30%以上。

程序代码是基于Symfony1.2框架之上开发的。一是可以使用外部资源,二是 能够提高项目开发进度,同时在一个著名的框架上可以让新开发人员更容易加入到团队中来。虽然没有任何事情都是十全十美的,但可以从Symfony框架中得 到很多好处,让团队可以更多的精力放在Poppen.de的业务开发上去。

网站性能优化使用XHProf,这是Facebook开源出来的一个类库。这个框架非常容易个性化和配置,能够可以缓存大部分高代价的服务器计算。

MySQL

MySQL是网站主要的RDBMS。网站又几个MySql服务器:一台4CPU、32GB的服务器存储用户相关信息,如基本信息、照片描述信息等。这台机器已经使用了4 年,下一步计划会使用共享集群来替换它。目前仍基于这个系统上进行设计,以简化数据访问代码。根据用户ID进行数据分区,因为网站中大部分信息都是以用户 为中心的,如照片、视频、消息等。

有三台服务器按主-从-从配置架构提供用户论坛服务。一台从服务器负责网站自定义消息存储,到现在有 2.5亿条消息。另外四台机器为主-从配置关系。另外由4台机器配置成NDB族群专门服务于密集型写操作数据,如用户访问统计信息。

数据表设计尽量避免关联操作,尽可能缓存最多的数据。当然,数据库的结构化规范已经完全被破坏掉了。因此,为了更容易搜索,数据库设计创建了数据挖掘表。大部分表是MyISAM型表,可以提供快速查找。现在的问题是越来越多的表已经全表锁住了。Poppen.de正考虑往XtraDB存储引擎上迁移。

Memcached

网站架构中Memcached应用相当多,超过45GB的高速缓存和51个节点。缓存了Session会话、视图缓存以及函数执行缓存等。架构中有一个系统 当记录被修改时可以自动地把数据更新到缓存中去。未来改善缓存更新的可能方案是使用新的Redis Hash API或者MongoDB。

RabbitMQ

在 2009年中开始在架构中使用RabbitMQ。这是一个很好的消息解决方案,便于部署和集中到这个架构中去,在LVS后运行了两台RabbitMQ服务 器。在上个月,已经把更多的东西集成到该队列中,意味着同一时刻有28台PHP服务器每天要处理50万次请求。发送日志、邮件通知、系统消息、图像上载等 更多的东西到这个队列中。

应用PHP-FPM中的fastcgi_finish_request()函数集成队列消息,可以把消息异步发 送到队列中。当系统需要给用户发送HTML或JSON格式响应时,就调用这个函数,这样用户就没有必要等到PHP脚本清理。

这个系统可以改善架构资源管理。例如,在高峰期服务每分钟可以处理1000次登录请求。这表示有1000并发更新用户表保存用户的登录时间。由于使用了队列机制,可以 按相反的顺序来运行这些查询。如果需要提高处理速度,只需要增加更多的队列处理者即可,甚至可以增加更多的服务器到这集群中去,而不需要修改任何配置和部 署新节点。

CouchDB

日志存储CouchDB运行在一台机器上。在这台机器上可以根据模块/行为进行日志查询 /分组,或者根据错误类型等等。这对定位问题非常有用。在使用日志聚合服务CouchDB之前,不得不逐台登录到PHP服务器上设法日志分析定位问题,这 是非常麻烦的。而现在把所有的日志集中到队列中保存到CouchDB中,可以集中进行问题检查和分析。

Graphite

网站使用Graphite采集网站实时信息并统计。从请求每个模块/行为到Memcached的命中和未命中、RabbitMQ状态监控以及Unix负载等等。Graphite服务平均每分钟有4800次更新操作。实践已经证实要监测网站发发生什么是非常有用的,它的简单文本协议和绘图功能可以方便地即插即 用的方式用于任何需要监控的系统上。

一件很酷的事情是使用Graphite同时监控了网站的两个版本。一月份部署了Symfony框架新 版本,以前代码作为一个备份部署。这就意味着网站可能会面临性能问题。因此可以使用Graphite来对两个版本在线进行对比。

发现新版本上的Unix负载表较高,于是使用XHProf对两个版本进行性能分析,找出问题所在。

Red5

网站为用户也提供了两种类型的视频服务,一种是用户自己上载的视频,另外一种是视频聊天,用户视频互动和分享。到2009年年中,每月为用户提供17TB的流量服务。

Tsung

Tsung 是一个Erlang编写的分布式基准分析工具。在Poppen.de网站中主要用于HTTP基准分析、MySQL与其他存储系统(XtraDB)的对比分 析。用一个系统记录了主要的MySQL服务器的流量,再转换成Tsung的基准会话。然后对该流量进行回放,由Tsung产生数以千计的并发用户访问实验 室的服务器。这样就可以在实验环境中与真实场景非常接近。

magento

Magento 的配置文件如同该系统的心脏一般。它负责从总体上配置所有被请求的模块,模型,类,模板文件等。它是绝大部分PHP程序员不会接触到的一块抽象层,同时也 会增加一系列的开发成本,但是,所有这些都是值得的,因为Magento的配置文件允许你对系统的默认功能进行你无法想象的高度扩展。

为了学习Magento配置文件,我们将会在本章创建一个Magento模块,使其能够在浏览器中访问并显示Magento系统的配置文件。

本篇文章中包括的内容有,

  • 设置一个Magento模块的目录结构
  • 创建模块的配置文件
  • 我能在配置文件中找到什么有用信息?
  • 为什么配置文件对我如此重要?

设置Magento模块的目录结构

这一节我们将创建一个Magento模块。Magento模块是由PHP及XML文件组成,用来给系统扩展新的功能,或重写、扩展核心系统的行为。这可能意味着添加新的数据模型来跟踪销售信息,改变系统中类的行为,或添加完全新的功能。

Magento系统中绝大多数你将使用到的模块都具有相同的结构。如果你查看,

app/code/core/Mage

该目录下的每个文件夹都是Magento团队创建的一个单独的模块。同时,你创建的模块将会放在以下文件夹中,

app/code/local/Packagename

“Packagename” 应该是能够区别你代码的唯一命名。一般情况下是可以是你的公司名称,但你也可以随意命名。例如,微软公司开发的Magento模块,可能会使用以下命名,

app/code/local/Microsoft

在这里,我们使用”Magentotutorial”。那么首先,要创建模块,我们需要创建如下所示的目录结构,

app/code/local/Magentotutorial/Configviewer/Block

app/code/local/Magentotutorial/Configviewer/controllers

app/code/local/Magentotutorial/Configviewer/etc

app/code/local/Magentotutorial/Configviewer/Helper

app/code/local/Magentotutorial/Configviewer/Model

app/code/local/Magentotutorial/Configviewer/sql

一个模块并不一定需要上述所有的目录,但是先把他们给全部创建好是个不错的注意。接着,需要创建两个配置文件,一个是位于上述路径etc目录中的config.xml ,

app/code/local/Magentotutorial/Configviewer/etc/config.xml

第二个位于下面的路径,

app/etc/modules/Magentotutorial_configviewer.xml

config.xml文件中会包含下面的代码,先不用理解这些代表什么,继续看,下面会细细讲解。

01
02
03
04
05
06
07
<config>
    <modules>
        <Magentotutorial_Configviewer>
            <version>0.1.0</version>
        </Magentotutorail_Configviewer>
    </modules>
</config>

最后, Magentotutorial_configviewer.xml需要包含以下配置文件代码,

01
02
03
04
05
06
07
08
<config>
    <modules>
        <Magentotutorial_Configviewer>
            <active>true</active>
            <codePool>local</codePool>
        </Magentotutorial_Configviewr>
    </modules>
</config>

再然后?不需要再然后了,你现在已经创建了一个基本的模块,当然它还什么都不能干,但是Magento已经能够识别到此模块的存在。登录Magento后台,进行如下操作,

  • 清空Magento缓存
  • 访问System->Configuration->Advanced
  • 点击”Disable modules output”,大概在最下面的位置,能够找到你刚创建的模块

找到了?恭喜你,你的第一个Magento模块创建完毕。

创建、完善模块的配置文件

向上面说的一样,这个模块目前还什么都不能做,完成本章之后,这个模块会有如下功能,

  • 检查请求路径中是否包含”showConfig”字符串
  • 如果存在该字符串,将会显示Magento的配置文件,并停止请求的默认执行
  • 检查请求路径中是否包含”showConfigFormat”字符串,它能够定制配置文件输出格式

首先,我们将下列代码中的<global>部分添加到config.xml文件中,

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<modules>
    ...
</modules>
<global>
    <events>
        <controller_front_init_routers>
            <observers>
                <Magentototurial_configviewer_model_observer>
                    <type>singleton</type>
                    <class>Magentototurial_Configviewer_Model_Observer</class>
                    <method>checkForConfigRequest</method>
                </Magentototurial_configviewer_model_observer>
            </observers>
        </controller_front_init_routers>
    </events>
</global>

然后在下面的路径中创建Ovserver.php文件,

Magentotutorial/Configviewer/Model/Observer.php

添加以下代码到该文件中,

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Magentotutorial_Configviewer_Model_Observer {
    const FLAG_SHOW_CONFIG = 'showConfig';
    const FLAG_SHOW_CONFIG_FORMAT = 'showConfigFormat';
 
    private $request;
 
    public function checkForConfigRequest($observer) {
        $this->request = $observer->getEvent()->getData('front')->getRequest();
        if($this->request->{self::FLAG_SHOW_CONFIG} === 'true'){
            $this->setHeader();
            $this->outputConfig();
        }
    }
 
    private function setHeader() {
        $format = isset($this->request->{self::FLAG_SHOW_CONFIG_FORMAT}) ?
        $this->request->{self::FLAG_SHOW_CONFIG_FORMAT} : 'xml';
        switch($format){
            case 'text':
                header("Content-Type: text/plain");
                break;
            default:
                header("Content-Type: text/xml");
        }
    }
 
    private function outputConfig() {
        die(Mage::app()->getConfig()->getNode()->asXML());
    }
}

到此,我们的配置文件查看模块完成了,清理下Magento缓存,然后在浏览器打开存放此模块的Magento的任意地址,并加上“showConfig=true”字符串。

http://magento.example.com/?showConfig=true

我能在配置文件中找到什么有用信息?

打开上述页面之后,你会看到一个巨大的XML文件。它描述了当前运行的Magento系统的整个状态。你能够找到所有的模块,模型,类,时间监听者以及所有存在于Magento系统中的配置。

还记得本章创建模块时建立的config.xml配置文件吗?试着在浏览器中的xml文档中搜索该配置文件中包含的Configviewer_Model_Observer,你会发现刚刚创建的这个配置文件实际上也包含在这个xml文档中。总的来说Magento中所有模块的配置文件都会被解析并包含在这个全局配置文件当中。

为什么配置文件对我如此重要?

目前为止可能有些难以理解,但是这个配置文件是你学习Magento系统的关键。你添加的任何所有模块都能够在这个配置文件中找到,任何时候当你需要请求核心系统的功能时,Magento都会使用到配置文件。

举个例子,作为一个MVC程序员,你经常会使用到一些助手类,使用如下方式实例化,

01
$helper_sales = new HelperSales();

而Magento是抽象化了这个过程,并非使用PHP的类声明方式。在Magento中,上面的助手类对象会使用如下方式实例化,

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

通俗的说,这个静态方法将会,

  • 在配置文件找查找<helpers />节点
  • 在<helpers />节点内,查找<sales />节点
  • 在<sales />节点内,再查找<class />节点
  • 实例化在<class />节点中找到的类

这看起来貌似费了很大的事,关键的优点是,通过在配置文件中查询类名,我们可以重写Magento核心功能,而不需要改变或添加任何代码到核心库中。这种“元编程“的方式在PHP中使用的并不多,不过它可以让你明确的扩展你想扩展的任意一个地方。