目標

實現Controller和Filter,程序可以從HTTP請求中解析Controller和Action,並在這兩個切面級別實現Filter鏈。此外,在Controller中,可以使用Action的參數直接訪問HTTP請求中的同名參數。

獲取代碼

項目目錄結構做了調整,framework目錄存放Flamework框架源碼,demo目錄存放示例項目。

1
git checkout v0.3

設計與實現

Controller的實現

要求請求URL的格式如下:

http://www.mydomain.com/index.php?r=post/save

r表示Route,斜杠前面的post表示Controller的名稱,後面的save表示Action的名稱。對HTTP請求的各種處理邏輯封裝在新對象HttpRequest中:

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
<?php
namespace org\x3f\flamework\base;
use org\x3f\flamework\Flame as Flame;
/**
* HTTP request wrapper
*
* @author Donie Leigh <donie.leigh@gmail.com>
* @link http://0x3f.org
* @copyright Copyright &copy; 2013-2014 Donie Leigh
* @license BSD (3-terms)
* @since 1.0
*/
class HttpRequest
{
/**
* @var HttpRequest Singleton instance
* @since 1.0
*/
private static $_instance;
/**
* @var string Controller name, null if no one is given
* @since 1.0
*/
private $_controller;
/**
* @var string Action name, null if no one is given
* @since 1.0
*/
private $_action;
/**
* Singleton constructor
* @return void
* @since 1.0
*/
private function __construct()
{
$this->parseRoute();
}
/**
* Disable the cloning
* @return void
* @since 1.0
*/
public function __clone()
{
trigger_error('Clone is not allow!', E_USER_ERROR);
}
/**
* Get the singleton instance
* @return HttpRequest
* @since 1.0
*/
public static function getInstance()
{
if (!(self::$_instance instanceof self))
self::$_instance = new self;
return self::$_instance;
}
/**
* Parse request route, set controller and action names
*
* @return void
* @since 1.0
*/
public function parseRoute()
{
if (isset($_GET['r'])) {
$arr = explode('/', $_GET['r']);
$this->_controller = $arr[0];
if (count($arr)>1) $this->_action = $arr[1];
} else {
$this->_controller = Flame::app()->getDefaultController();
}
}
/**
* Get controller name
*
* @return string null if no controller is present
* @since 1.0
*/
public function getController()
{
return $this->_controller;
}
/**
* Get action name
*
* @return string null if no action is found
* @since 1.0
*/
public function getAction()
{
return $this->_action;
}
/**
* Get parameter value
*
* @param string $param Parameter name
* @return mixed Parameter value
* @since 1.0
*/
public function getParam($param)
{
if (isset($_REQUEST[$param]))
return $_REQUEST[$param];
return null;
}
}
?>

考慮到HttpRequest可能在多個地方被調用,所以用單例模式實現。

WebApplication中添加如下內容:

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
<?php
class WebApplication {
// ...
/**
* @var string The default controller name
* @since 1.0
*/
public $defaultController = 'default';
// ...
/**
* Get the default controller name
* @return string Controller name
* @since 1.0
*/
public function getDefaultController()
{
return $this->defaultController;
}
/**
* Get controller path
* @return string The controller path
* @since 1.0
*/
public function getControllerPath()
{
return $this->getProtectedPath().DIRECTORY_SEPARATOR.'controller';
}
/**
* Create an instance of the controller
* @param string $controllerName
* @return Controller Controller instance
* @since 1.0
*/
public function createController($controllerName)
{
$className = ucfirst($controllerName).'Controller';
$classFile = $this->getControllerPath().DIRECTORY_SEPARATOR."$className.php";
if (file_exists($classFile)) {
$ns = include_once($classFile);
$fullClassName = "$ns\\$className";
if (class_exists($fullClassName)) {
return new $fullClassName();
} else {
throw new HttpException(404, "Request to $controllerName is unresolvable.");
}
} else {
throw new HttpException(404, "Request to $controllerName is unresolvable.");
}
}
}
?>

程序應指定一個缺省的Controller,覆蓋$defaultController屬性即可,默認為“default”。Controller的類名應在名稱後面加“Controller”字樣的後綴。由於需要包含命名空間的完整類名來動態實例化Controller,故Controller的源碼中都應在最後返回其命名空間(return \_NAMESPACE__;_)。

增加Controller類,作為所有Controller的父類:

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
<?php
namespace org\x3f\flamework\base;
use org\x3f\flamework\exceptions\HttpException;
/**
* Ancestor class for all controllers
*
* @author Donie Leigh <donie.leigh@gmail.com>
* @link http://0x3f.org
* @copyright Copyright &copy; 2013-2014 Donie Leigh
* @license BSD (3-terms)
* @since 1.0
*/
class Controller
{
/**
* @var string The default action
* @since 1.0
*/
protected $defaultAction = 'index';
/**
* @var array Filter classnames and rules.
* This is an associative array, in which keys are classnames of filters,
* and values are regular expressions.
* For example:
* array(
* 'org\\x3f\\flamedemo\filter\\PerformanceFilter' => '/^(save|update|delete)$/',
* );
* @since 1.0
*/
protected $filters = array();
/**
* Process http request
* @param HttpRequest $request
* @return void
* @since 1.0
*/
public function process(HttpRequest $request)
{
$action = $request->getAction() === null ? $this->defaultAction : $request->getAction();
if (method_exists($this, $action)) {
// do parameter bindings
$method = new \ReflectionMethod(get_class($this), $action);
$params = array();
foreach ($method->getParameters() as $param){
if (isset($_REQUEST[$param->getName()])) {
$params[] = $_REQUEST[$param->getName()];
} else if ($param->isDefaultValueAvailable()) {
$params[] = $param->getDefaultValue();
} else {
throw new HttpException(400, "Parameter ".$param->getName()." is missing.");
}
}
// create filter chain and run it
$filters = $this->getActionFilters($action);
$chain = new FilterChain($this, $action, $params, $filters);
$chain->run();
} else {
$msg = 'Request to '.$request->getController().'/'.$action.' cannot be resolved, action does not exist.';
throw new HttpException(404, $msg);
}
}
/**
* Get filters for the given action
* @param string $action
* @return array Filter names
* @since 1.0
*/
public function getActionFilters($action)
{
$filters = array();
foreach ($this->filters as $filterClass=>$regex){
if (preg_match($regex, $action))
$filters[] = $filterClass;
}
return $filters;
}
}
?>

Controller::process()是入口方法,它會通過反射機制實現HTTP參數與Action參數的綁定,並指定Action。

Filter與Filter鏈的實現

Filter中實現before()和after()方法,Filter鏈通過對Filter按順序遞歸調用,實現所有Filter::before()方法在切面之前順序執行,並且所有Filter::after()方法在切面之後逆序執行。

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
<?php
namespace org\x3f\flamework\base;
/**
* Ancestor class for all filters
*
* @author Donie Leigh <donie.leigh@gmail.com>
* @link http://0x3f.org
* @copyright Copyright &copy; 2013-2014 Donie Leigh
* @license BSD (3-terms)
* @since 1.0
*/
class Filter
{
/**
* Run this filter and the filter chain
* @param FilterChain $chain
* @return void
* @since 1.0
*/
public function filter(FilterChain $chain)
{
if ($this->before($chain)) {
$chain->run();
$this->after($chain);
}
}
/**
* The logic to be executed before the aspect point
* @param FilterChain $chain
* @return boolean Return true to continue the filter chain, return false to break the chain
* @since 1.0
*/
protected function before(FilterChain $chain) {
return true;
}
/**
* The logic to be executed after the aspect point
* @param FilterChain $chain
* @return void
* @since 1.0
*/
protected function after(FilterChain $chain) {
}
}
?>
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
<?php
namespace org\x3f\flamework\base;
/**
* Filter chain
*
* @author Donie Leigh <donie.leigh@gmail.com>
* @link http://0x3f.org
* @copyright Copyright &copy; 2013-2014 Donie Leigh
* @license BSD (3-terms)
* @since 1.0
*/
class FilterChain
{
/**
* @var object Object to be filtered
* @since 1.0
*/
public $obj;
/**
* @var string Method of the object to be filtered
* @since 1.0
*/
public $method;
/**
* @var array Parameters to be passed to the method
* @since 1.0
*/
public $params;
/**
* @var array Filters
* @since 1.0
*/
public $filters;
/**
* @var int The offset of filters array
* @since 1.0
*/
private $_offset = 0;
public function __construct($obj, $method, $params, $filters)
{
$this->obj = $obj;
$this->method = $method;
$this->params = $params;
$this->filters = $filters;
}
/**
* Run this filter and the filter chain
* @return void
* @since 1.0
*/
public function run()
{
$filter = $this->nextFilter();
if ($filter instanceof Filter) {
$filter->filter($this);
} else {
call_user_func_array(array($this->obj, $this->method), $this->params);
}
}
/**
* Get next filter
* @return Filter Filter instance
* @since 1.0
*/
private function nextFilter()
{
if ($this->_offset < count($this->filters)) {
$filter = $this->filters[$this->_offset];
$this->_offset++;
return new $filter;
}
}
}
?>

對FilterChain和Filter的使用方法在前面的WebApplication::run()和Controller::process()中均有包含。Controller級的Filter在配置文件中設置,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
return array(
// ...
// app namespace and its path
'namespaces' => array('org\\x3f\\flamedemo' => '/srv/http/flamework/demo/protected'),
// filter classes
'filters' => array(
'org\\x3f\\flamedemo\\filter\\GlobalFilterA',
'org\\x3f\\flamedemo\\filter\\GlobalFilterB',
),
);
?>

Action級的Filter在Controller裡覆蓋$filters屬性:

1
2
3
4
5
6
7
8
9
<?php
// ...
protected $filters = array(
'org\\x3f\\flamedemo\\filter\\ActionFilterC' => '/^(index|noindex)$/',
);
// ...
?>

Action級別的Filter通過$filters數組中的正則表達式選擇適用的Action。

Demo驗證

Demo中實現了兩個Controller級別的Filter(GlobalFilterA和GlobalFilterB),一個Action級別的Filter(ActionFilterC),訪問demo項目,頁面打印如下結果:

org\x3f\flamedemo\filter\GlobalFilterA::before
org\x3f\flamedemo\filter\GlobalFilterB::before
org\x3f\flamedemo\filter\ActionFilterC::before
org\x3f\flamedemo\controller\Defaultcontroller::index
org\x3f\flamedemo\filter\ActionFilterC::after
org\x3f\flamedemo\filter\GlobalFilterB::after
org\x3f\flamedemo\filter\GlobalFilterA::after

總結

WebApplication作為程序的統一入口,通過對HTTP請求的解析動態創建Controller,並借此實現了Controller級別的Filter鏈。Controller通過反射機制實現了HTTP參數與Action參數的綁定,以及Action級別的Filter鏈。而通過對Filter的遞歸執行,Filter鏈實現了面向切面編程。