标签归档:企业应用架构

Yii 框架的视图层实现

如果你想看看 Yii 框架的视图实现过程,请继续向下;如果你想看看胖子的碎碎念,请直接拉到文章最后;如果你只是路过,那也路过留名吧^_^。

Martin Flower 在《企业应用架构模式》中提到 MVC 模式的关键点在于两个分离:从模型中分离视图和从视图中分离控制器。视图的表现在很大程度上决定了此模式的使用,以及框架对 MVC 模式的各个层级的分离水平。 Yii 框架是一个基于 MVC 模式的框架,它在视图这块做了很多的工作,很清晰的实现了视图的功能。

Yii 框架的视图是一个包含了主要的用户交互元素的 PHP 脚本。每个视图有一个名字,当渲染( render )时,名字会被用于识别视图脚本文件。视图的名称与其视图脚本名称是一样的。例如:视图 edit 的名称出自一个名为 edit.php 的脚本文件。要渲染时,需通过传递视图的名称调用 CController::render()。这个方法将在 “protected/views/控制器 ID” 目录下寻找对应的视图文件,其寻找方法为 getViewFile。这里的 protected/views 只是默认的存储位置,我们可以通过 Yii::app()->setViewPath 方法改变此路径。

在视图脚本内部,我们可以通过 $this 来访问控制器实例。同时,我们也可以在视图里以“$this->属性名”的方式获取控制器的任何属性,这种调用方式是通过实现__get魔法方法实现的。

一次较为完整的视图渲染过程在 CController 类的 render 函数中体现得淋漓尽致。当控制器中从模型(model)中拿到数据后,一般会执行 render() 方法创建视图,其大概过程如下:

  1. 执行预留的 beforeRender 钩子。
  2. 查找渲染局部内容 $output,实际上这里是一个部分视图渲染的过程,它包括获取视图文件路径,渲染视图文件,处理输出三个部分。在渲染的过程中,通过 PHP 中的 extract() 方法把数组中将变量导入到当前的符号表,直接 require 视图文件,从而合并数据和表现。
  3. 如果存在布局,则将得到的内容放入以 content 为下标的的数组传递给父类的 renderFile() 方法中,重复执行渲染视图的过程。在布局中执行 ,输出局部内容$output,实现了局部和布局视图的合并。为了实现多级布局,在布局中还可以通过控制器的视图装饰方法加载。
  4. 执行预留的 afterRender 钩子。

在渲染视图的时候,如果参数中有传递对应的值,会执行 processOutput() 方法,此方法一般在渲染视图结束时才会调用,它实现了三个过程:

  1. 注册客户端脚本,具体由 ClientScript 组件管理。
  2. 如果存在,则执行动态内容输出。
  3. 页面内容 base64_encode 加密,如果存在 zlib 扩展,则会先压缩。

在 CController 类中对视图的渲染除了上面的render方法外,还有其它多种方法:

  • render方法: 和布局一起渲染 render($view,$data=null,$return=false)
  • renderPartial方法: 仅渲染视图内容,或者是渲染部分页面内容。它与 render() 方法的不同是它不会渲染布局,并且在 render() 方法中也会调用此方法。 renderPartial($view,$data=null,$return=false,$processOutput=false)
  • renderText方法:渲染静态内容和布局。renderText($text,$return=false)
  • renderDynamic方法:通过回调函数渲染动态内容,通常我们会在模板文件中中调用此方法。renderDynamic($callback)->renderDynamicInternal($callback,$params)
  • renderClip方法:渲染显示 CClipWidget 生成的内容,此处需要指定名字。renderClip($name,$params=array(),$return=false)

对于不同的页面中共用的内容,虽然可以通过 renderPartial 方法渲染部分页面视图,但是必然存在对于数据部分的重复,因为这些视图都需要调用控制提供的数据,从而产生耦合。因此 Yii 框架 提供了另一个独立的视图部件,官方称之为 Widget (小物件?小挂件?)。

小物件是 CWidget 或其子类的实例。它是一个主要用于表现数据的组件。小物件通常内嵌于一个视图来产生一些复杂而独立的用户界面。也算是一种界面的独立和松耦合的设计。如我们做WEB应用时常用的列表,翻页,日历等。这些 Widget 增加了界面的复用度,减少了代码量。

与前面视图部分不同的是,它没有布局文件支持,并且 Widget 视图中的 $this 指向 Widget 实例而不是控制器实例,这里实现了与控制器的分离。如果要实现一个自定义的 Widget ,我们仅需要继承 CWidget 并覆盖其 init() 和 run() 方法,可以定义一个新的 Widget 。

我们在视图中通过 $this->widget() 或 $this->beginWidget() 和 $this->endWidget() 调用 Widget,两者的区别在于第二个方法可以在显示的过程中添加 html 内容。添加内容的位置在 init() 方法和 run() 方法输出的内容之间。

除了布局、Widget 外, Yii 框架实现系统级的视图,用来显示 Yii 的错误和日志信息。

系统视图的命名遵从了一些规则。比如像“errorXXX”这样的名称就是用于渲染展示错误号 XXX 的 CHttpException 的视图。在 framework/views 下, Yii 提供了一系列默认的系统视图. 我们可以通过在 protected/views/system 下创建同名视图文件进行自定义。系统默认的 exception 视图非常赞,结合 Yii 本身的 traces 机制,当抛出异常或出错时就会很详细的定位出问题的代码所在。

以上只是胖子阅读 Yii 框架源码的笔记。结合《企业应用架构模式》这本书的内容,如页面控制器、前端控制器、活动记录等,胖子发现对框架的实现有更深入的理解。一方面印证了书上的理论,一方面为实现过程的原理找到了出处。不晓得 Yii 框架的作者是否对此书也有精读,或者是经验的积累?

参考资料: http://www.yiiframework.com/doc/guide/1.1/en/basics.view

前端控制器

前端控制器

表现层的请求处理机制需要支持每个用户多个请求,我们可以以集中式或分散式的方式管理这些请求。

如果以分散的方式进行管理可能会导致如下的一些问题:

  • 每个请求都有一个共同的操作,分散处理可能会导致代码的重复。
  • 可能会导致视图导航和视图内容的耦合。
  • 分散处理可能会带来更高的维护成本。

如果我们采用集中的方式进行管理,则可以对安全认证、国际化等操作统一处理,同时也可以在一个集中点处理站点的某些操作(如日志记录,站点全局访问控制等),
并且可以在一个地方处理逻辑在多个视图中重复显示。如此我们有了选择前端控制器的理由。

前端控制器建议集中处理所有请求的处理,然而它并没有限制系统中请求处理器的个数,对于不同的服务,完全可以提供不同的处理器。
这与集中式的管理并不矛盾,其实集中只是一种相对的集中,从而达到解决分散式所产生的问题的目的,
任何一种模式只是为解决一些应用场景的特定问题。

运行机制
一个前端控制器其本体包括两部分:一个分发中心(或叫调度处理程序)和一个command(或动作)层次结构。
当一个请求到达服务器,前端控制器接收此请求,从其请求信息中获取足够的内容并决定下一步操作,然后委托给某个command,执行操作。

分发中心可以是一个类或几个类,它没有页面输出,它的作用就是决定最终运行哪一个command。
这里简单点,可以直接根据参数约定,动态识别并执行。
这种简单方法做到了开闭原则,可以在不修改分发中心的前提下添加新的command。

例子
前端控制器在PHP的框架中基本上都会出现,在实现方式上,前端控制器大多采用Apache的url_rewrite模块,
以在.htaccess中重写规则,将所有请求都转发到index.php文件处理。

如PHP的YII框架,Application即YII framework的前端处理器,它是整个请求过程的运行环境。
Application 接收用户的请求并把它分发到合适的控制器作进一步处理。
其一个访问到动作被执行,简单过程如下:

  • 用户访问 Web 服务器,假设其访问地址为index.php?r=post/show,则其入口脚本 index.php 会处理该请求。
  • index.php建立一个应用实例并运行(run方法,在运行前有若干组件加载,初始化操作)。
  • 在应用从一个叫 HTTPRequest 的应用组件获取此次请求的详情。
  • 在urlManager 的组件的帮助下,根据前面获取的请求详情确定用户要请求的控制器和动作,分别对应CController和CInlineAction。
  • 应用建立一个被请求的控制器实例来进一步处理用户请求,控制器确定由它的actionShow 方法来处理 show 动作。
  • 然后它创建并运行和该动作相关的过滤器(CController->runActionWithFilters( )),如果过滤器允许的话,动作被执行,即CController->runAction() ==> CInlineAction->run()。

对于前端控制器,Java体系中的Struts框架以XML配置方式体现,在strut.xml配置动作,在web.xml中配置过滤器。

  • 前端页面提交以“.do”结尾的请求。
  • FilterDispatcher接收请求并调用Action处理该请求。
  • Action处理完毕返回一个逻辑视图。
  • FilterDispatcher根据Action返回逻辑视图创建物理视图
  • 将物理视图返回给页面。

当然我们也可以在一个PHP文件中实现整个前端控制,直接约定命名规范,根据传递进来的参数动态加载处理器,处理方法,视图等。

数据源架构模式之活动记录

数据源架构模式之活动记录

【活动记录的意图】
一个对象,它包装数据表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。

【活动记录的适用场景】
适用于不太复杂的领域逻辑,如CRUD操作等。

【活动记录的运行机制】
对象既有数据又有行为。其使用最直接的方法,将数据访问逻辑置于领域对象中。
活动记录的本质是一个领域模型,这个领域模型中的类和基数据库中的记录结构应该完全匹配,类的每个域对应表的每一列。
一般来说,活动记录包括如下一些方法:
1、由数据行构造一个活动记录实例;
2、为将来对表的插入构造一个新的实例;
3、用静态查找方法来包装常用的SQL查询和返回活动记录;
4、更新数据库并将活动记录中的数据插入数据库;
5、获取或设置域;
6、实现部分业务逻辑。

【活动记录的优点和缺点】
优点:
1、简单,容易创建并且容易理解。
2、在使用事务脚本时,减少代码复制。
3、可以在改变数据库结构时不改变领域逻辑。
4、基于单个活动记录的派生和测试验证会很有效。

缺点:
1、没有隐藏关系数据库的存在。
2、仅当活动记录对象和数据库中表直接对应时,活动记录才会有效。
3、要求对象的设计和数据库的设计紧耦合,这使得项目中的进一步重构很困难

【活动记录与其它模式】
数据源架构模式之行数据入口:活动记录与行数据入口十分类似。二者的主要差别是行数据入口 仅有数据库访问而活动记录既有数据源逻辑又有领域逻辑。

【活动记录的PHP示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php
 
/**
 * 企业应用架构 数据源架构模式之活动记录 2010-10-17 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package architecture
 */
 
/**
 * 定单类
 */
class Order {
 
    /**
     *  定单ID
     * @var <type>
     */
    private $_order_id;
 
    /**
     * 客户ID
     * @var <type>
     */
    private $_customer_id;
 
    /**
     * 定单金额
     * @var <type>
     */
    private $_amount;
 
    public function __construct($order_id, $customer_id, $amount) {
        $this->_order_id = $order_id;
        $this->_customer_id = $customer_id;
        $this->_amount = $amount;
    }
 
    /**
     * 实例的删除操作
     */
    public function delete() {
        $sql = "DELETE FROM Order SET WHERE order_id = " . $this->_order_id . " AND customer_id = "  . $this->_customer_id;
        return DB::query($sql);
    }
 
    /**
     * 实例的更新操作
     */
    public function update() {
    }
 
    /**
     * 插入操作
     */
    public function insert() {
    }
 
    public static function load($rs) {
        return new Order($rs['order_id'] ? $rs['order_id'] : NULL, $rs['customer_id'], $rs['amount'] ? $rs['amount'] : 0);
    }
 
}
 
class Customer {
 
    private $_name;
    private $_customer_id;
 
    public function __construct($customer_id, $name) {
        $this->_customer_id = $customer_id;
        $this->_name = $name;
    }
 
    /**
     * 用户删除定单操作 此实例方法包含了业务逻辑
     * 通过调用定单实例实现
     * 假设此处是对应的删除操作(实际中可能是一种以某字段来标记的假删除操作)
     */
    public function deleteOrder($order_id) {
        $order = Order::load(array('order_id' => $order_id, 'customer_id' => $this->_customer_id));
        return $order->delete();
    }
 
    /**
     * 实例的更新操作
     */
    public function update() {
    }
 
    /**
     * 入口类自身拥有插入操作
     */
    public function insert() {
    }
 
    public static function load($rs) {
        /* 此处可加上缓存 */
        return new Customer($rs['customer_id'] ? $rs['customer_id'] : NULL, $rs['name']);
    }
 
    /**
     * 根据客户ID 查找
     * @param integer $id   客户ID
     * @return  Customer 客户对象
     */
    public static function find($id) {
        return CustomerFinder::find($id);
    }
 
}
 
/**
 * 人员查找类
 */
class CustomerFinder {
 
    public static function find($id) {
        $sql = "SELECT * FROM person WHERE customer_id = " . $id;
        $rs = DB::query($sql);
 
        return Customer::load($rs);
    }
}
 
class DB {
 
    /**
     * 这只是一个执行SQL的演示方法
     * @param string $sql   需要执行的SQL
     */
    public static function query($sql) {
        echo "执行SQL: ", $sql, " <br />";
 
         if (strpos($sql, 'SELECT') !== FALSE) { //  示例,对于select查询返回查询结果
            return array('customer_id' => 1, 'name' => 'Martin');
        }
    }
 
}
 
/**
 * 客户端调用
 */
class Client {
 
    /**
     * Main program.
     */
    public static function main() {
 
 
        header("Content-type:text/html; charset=utf-8");
 
        /* 加载客户ID为1的客户信息 */
        $customer = Customer::find(1);
 
        /* 假设用户拥有的定单id为 9527*/
        $customer->deleteOrder(9527);
    }
 
}
 
Client::main();
?>

同前面的文章一样,这仅仅是一个活动记录的示例,关于活动记录模式的应用,可以查看Yii框架中的DB类,在其源码中有一个CActiveRecord抽象类,从这里可以看到活动记录模式的应用

另外,如果从事务脚本中创建活动记录,一般是首先将表包装为入口,接着开始行为迁移,使表深化成为活动记录。
对于活动记录中的域的访问和设置可以如yii框架一样,使用魔术方法__set方法和__get方法。