[php]前端控制器
当一个请求达到系统时,系统必须能够理解请求中的需求是什么,然后调用适当的业务逻辑进行处理,最后返回相应结果。对于简单的程序,整个过程可以放在视图中,但随着系统的增长,这种处理方式不能很好地满足请求、调用业务逻辑和显示适当视图。那么我们就需要在较大的系统中较好地管理这三者的关系,我们可以划分出视图层与命令和控制层。
视图层与命令和控制层之间的界线通常比较模糊,又是也把这两个层称为表现层。
前端控制器:单一入口(流行的PHP框架大多都是单一入口),定义了一个中心入口文件,每个请求都需要从这个入口进入系统,当然也可以在这里对用户的输入请求进行过滤。前端控制器处理请求后选择适当的命令(处理业务的)。在PHP中,这种模式每次都需要初始化,所以会导致性能的下降,但好处还是显而易见的。
前端控制器处理的是请求,那么我们就需要把用户的请求进行处理,这里用类赖封装用户的请求Request。
下面是Request的代码:
namespace demo\controller;
/**
* Request
* 封装用户请求
*/
class Request {
private $properties;
private $feedback = array();
public function __construct() {
$this->init();
$this->filterProperties();
// 保存入Registry
\demo\base\RequestRegistry::getInstance()->setRequest($this);
}
public function __clone() {
$this->properties = array();
}
public function init() {
if (isset($_SERVER['REQUEST_METHOD'])) {
if ($_SERVER['REQUEST_METHOD']) {
$this->properties = $_REQUEST;
return ;
}
}
// 命令行下的方式
foreach ($_SERVER['argv'] as $arg) {
if (strpos($arg, '=')) {
list($key, $val) = explode('=', $arg);
$this->setProperties($key, $val);
}
}
}
public function filterProperties() {
// 过滤用户请求...
}
public function getProperty($key) {
return $this->properties[$key];
}
public function setProperties($key, $val) {
$this->properties[$key] = $val;
}
public function getFeedback() {
return $feedback;
}
public function addFeedback($msg) {
array_push($this->feedback, $msg);
}
public function getFeedbackString($separator = '\n') {
return implode('\n', $this->feedback);
}
}
为什么Request不用使用内置的$_GET或者是$_POST等而是把封装在一起呢。这是为了能把用户的请求集中到一个地方进行统一处理,比如对请求过滤。这里还有一个好处就是可以从非HTTP请求中收集请求参数,允许应用程序在命令行或者在测试脚本中运行。
这里先来看看前端控制器的主体框架:
namespace demo\controller;
class Controller {
private $appHelper;
private function __construct() {
}
public static function run() {
$instance = new self();
// 加载配置
$instance->init();
// 处理请求
$instance->handleReuqest();
}
private function init() {
$appHelper = \demo\controller\ApplicationHelper::getInstance()->init();
}
private function handleReuqest() {
$request = new \demo\controller\Request();
$cmdReslover = new \demo\command\CommandReslover();
$cmd = $cmdReslover->getCommand($request);
$cmd->execute($request);
}
}
只要在一个php文件中包含这个类文件,并运行就可以了。use demo\controller\Controller;
require_once 'demo/controller/Controller.php';
Controller::run();
执行流程很简单:1)Controller::run()中调用init(),其中的ApplicationHelper是用于读取系统配置的类,它帮助加载系统配置; 2)解析Request由CommandReslover对象的getCommand()来处理,返回一个Command子类; 3)Command子类处理业务。
下面分别是Controller中出现的类。
ApplicationHelper的代码:
namespace demo\controller;
/**
* 助手类 获取系统配置
* 单例
*/
class ApplicationHelper {
private static $instance;
private $config = './data/config.xml';
private function __construct() {
}
public static function getInstance() {
if (isset(self::$instance) == false) {
self::$instance = new self();
}
return self::$instance;
}
public function init() {
// 用获取dsn例子,注意Regisrty缓存
$dsn = \demo\base\ApplicationRegistry::getInstance()->getDSN();
if (is_null($dsn) == false) {
return ;
}
$this->getOptions();
}
public function getOptions() {
$this->ensure(file_exists($this->config), "Could not find options file!");
$options = @simplexml_load_file($this->config);
$this->ensure($options instanceof \SimpleXMLElement, 'Could not resolve options file!');
$dsn = (string)$options->dsn;
$this->ensure($dsn, 'No dsn found!');
\demo\base\ApplicationRegistry::getInstance()->setDSN($dsn);
// 其它一些获取配置的代码...
}
private function ensure($expr, $msg) {
if (!$expr) {
throw new \demo\base\AppException($msg);
}
}
}
对于PHP来说每次都需要读文件是件很费时的事情,那ApplicationHelper就需要实现缓存机制。可以通过简单地判断变量是否已经设置来决定是否需要读取文件。其实最高效的方式是直接把配置内容写到PHP文件中。
CommandSlover通过Request来决定返回哪一个Command子类(业务封装在里面)。简单的办法获取url中cmd的值来决定的。比如runner.php?cmd=AddUser,那么CommandSlover就返回AddUser。
namespace demo\command;
class CommandReslover {
private static $baseCmd;
private static $defaultCmd;
public function __construct() {
self::$baseCmd = new \ReflectionClass('\demo\command\Command');
self::$defaultCmd = new DefaultCommand();
}
/**
* 解析请求返回命令
* @param \demo\controller\Request $request
*/
public function getCommand(\demo\controller\Request $request) {
// cmd为url参数名称,如 runner.php?cmd=addUser
$cmd = $request->getProperty('cmd');
$sep = DIRECTORY_SEPARATOR;
// 返回 默认的Command
if (empty($cmd) == true) {
return self::$defaultCmd;
}
//
$cmd = str_replace(array('.', $sep), '', $cmd);
$filePath = "demo{$sep}command{$sep}{$cmd}.php";
$className = '\demo\command\\' . $cmd;
if (file_exists($filePath)) {
@require_once $filePath;
if (class_exists($className)) {
// 判断cmd是否为Command的子类
$cmdClass = new \ReflectionClass($className);
if ($cmdClass->isSubclassOf(self::$baseCmd)) {
return $cmdClass->newInstance();
} else {
$request->addFeedback("Command '{$cmd}' is not a Command!");
}
}
}
$request->addFeedback("Command '{$cmd}' not found!");
return clone self::$defaultCmd;
}
}
Command子类主要封装业务。Command的类图:
Command抽象类代码:
namespace demo\command;
abstract class Command {
// 子类不能够重写
public final function __construct() {
}
public function execute(\demo\controller\Request $request) {
$this->doExecute($request);
}
// 子类实现对应的业务
protected abstract function doExecute(\demo\controller\Request $request);
}
DefaultCommand代码:namespace demo\command;
class DefaultCommand extends Command {
protected function doExecute(\demo\controller\Request $request) {
$request->addFeedback('Welcome~');
//
include('demo/view/default.php');
echo $request->getFeedbackString();
}
}
现在前端控制已经能够在一个地方统一处理请求了。搭建好核心部分后就可以用最简短的代码来封装它,以后就可以重复使用了。虽然平时能够用更少的代码很简单的方法实现同样的效果,但这是为了能够加深对前端控制器的理解。
Command子类中的处理试图当时还不是最好的,能够把试图和命令分离开来效果会好一些。
上一篇: 天气预报部分
下一篇: centOS命令行装androidSDK