目录

PHP框架实战(三):实现Controller和Filter

目标

实现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链实现了面向切面编程。