12.3 表现层
当一个请求到达系统时,系统必须能够理解请求中的需求是什么,然后调用适当的业务逻辑进行处理,最后返回响应结果。这就是表现层所需要实现的功能,简单的说就是需要满足处理请求,调用业务逻辑和派发视图的要求。
在实际系统中,视图层与命令和控制层的边界通常很模糊,因此这两个层统称为「表现层」。
12.3.1 前端控制器
前端控制器定义了一个中心入口,每个请求都要从这个入口进入系统。前端控制器处理请求并选择要执行的操作。操作通常定义在特定的 Command 类中。
首先我们定义一个 Controller 类:
namespace woo\controller
class Controller {
private $applicationHelper;
private function __construct(){}
static function run() {
$instance = new Controller();
$instance -> init();
$instance -> handleRequest();
}
function init() {
$applicationHelper = ApplicationHelper::instance();
$applicationHelper -> init();
}
function handleRequest() {
$request = new Requset();
$cmd_r = new CommandResolver();
$cmd = $cmd_r->getCommand();
$cmd -> execute($request);
}
}
这个控制器负责分配任务给其他类,具体的任务由其他类完成。run() 方法用于调用 init() 和 handleRequest() ,是一个静态方法,而且本类的构造器被设置为私有的,因此客户端只能通过调用 run() 方法来实例化控制器类,并执行相关操作。
因此,在客户端只需要两行来完成调用:
require 'woo/controller/Controller.php';
Controller::run();
init() 方法获得 ApplicationHelper (应用程序助理类)类的一个对象实例,这个助手类用于管理应用程序的配置信息。
handleRequest() 方法通过 CommandResolver 来获取一个 Command 对象,然后调用 Command 对象的 execute() 方法来执行实际操作。
然后,我们再来看应用程序助理类:
namespace woo\controller
class ApplicationHelper {
private static $instance;
private $config = "tmp/data/woo_options.xml";
private __construct(){}
static function instance() {
if (!self::$instance) {
self::$instance = new ApplicationHelper();
}
return self::$instance;
}
function init() {
$dsn = ApplicationRegistry::getDSN();
if (!is_null($dsn)) {
return;
}
$this->getOption();
}
private function getOption() {
$this->ensure(
file_exists($this->config),
"Could not find options file!"
);
$options = simplexml_load_file($this->config);
print get_class($options);
$dsn = $options->dsn;
$this->ensure(
$dsn,
"No DSN found"
);
ApplicationRegistry::setDSN($dsn);
}
private function ensure($expr, $message) {
if (!$expr) {
throw new AppException($message);
}
}
}
这个类的作用是读取配置文件中的数据并使客户端代码可以访问这些数据,通过实现一个单例模式使它能够为系统中所有的类服务。
init() 方法负责加载配置数据,同时,它检查 ApplicationRegistry,看数据是否被缓存,如果有缓存,init() 方法什么都不做。这种处理方式在系统需要大量初始化工作的情况下十分有用。缓存可以有效地保证复杂而且耗费时间的初始化过程只在第一次请求时发生。
这里抛出异常做了封装,封装到了单独一个 ensure() 方法中,能有效地避免在代码中导出使用条件检测和 throw 语句。
接下来我们看命令解析器。
控制器需要通过某种策略来决定如何解释一个 HTTP 请求,然后调用正确的代码来满足这个请求。我们可通过使用一个特定的类来完成这个任务。
前端控制器通常通过运行一个 Command 对象来调用应用程序逻辑,所以,下面我们构建一个 Command 对象,根据请求的参数和命令来决定选择哪个命令。
namespace woo\command;
class CommandResolver {
private static $base_cmd;
private static $default_cmd;
function __construct(){
if (!self::$base_cmd) {
self::$base_cmd = new ReflectionClass("Command");
self::$default_cmd = new DefaultCommand();
}
}
function getCommand(Requset $request) {
$cmd = $request->getProperty("cmd");
$sep = DIRECTORY_SEPARATOR;
if (!$cmd) {
return $default_cmd;
}
$cmd = str_replace(array('.',$sep), ' ', $cmd);
$filepath = "woo{$sep}command{$sep}{$cmd}.php";
$classname = "woo\\command\\{$cmd}";
if (file_exists($filepath)) {
$cmd_class = new ReflectionClass($classname);
if ($cmd_class->isSubClassOf(self::$base_cmd)) {
return $cmd_class->newInstance();
} else {
$request->addFeedback("command '$cmd' is not a Command");
}
}
$request->addFeedback("command '$cmd' not found");
return clone self::$default_cmd;
}
}
这个类通过 getCommand() 方法查找请求中的 cmd 参数。如果参数被找到,并与命令目录中的类文件相匹配,而且该文件也正好包含 cmd 类,则该方法创建并返回相应类的实例。如果不满足以上条件,则返回默认的 Command 对象。
在下一个是 Request 类,用于存储需要和视图层交换的数据。
namespace woo\controller;
class Request {
private $properties;
private $feedback = array();
function __construct() {
$this->init();
RequestRegistry::setRequest($this);
}
function init() {
if (isset($_SERVER['REQUEST_METHOD'])) {
$this->properties = $_REQUEST;
return;
}
foreach ($_SERVER['argv'] as $arg) {
if (strpos($arg, '=')) {
list($key, $val) = explode('=', $arg);
$this->setProperty($key, $val);
}
}
}
function getProperty($key) {
if (isset($this->properties[$key])) {
return $this->properties[$key];
}
}
function setProperty($key,$val) {
$this->properties[$key] = $val;
}
function addFeedback($msg) {
array_push($this->feedback, $msg);
}
function getFeedback() {
return $this->feedback;
}
function getFeedbackString($separator="\n"){
return implode($separator, $this->feedback);
}
}
init() 方法将 HTTP 请求和命令行参数设置到私有的 $properties 数组。并可以通过 getProperty() 方法得到 HTTP 请求中的参数。
在下一个是命令。
namespace woo\command;
abstract class Command {
final function __construct(){}
function execute(Request $request) {
$this->doExecute($request);
}
abstract function doExecute(Request $request);
}
class DefaultCommand extends Command {
function doExecute(Request $request) {
$request ->addFeedback('Welcome to WOO');
include 'woo/view/main.php';
}
}
抽象基类实现 execute() 方法,该方法向下调用由子类实现的 doExecute() 方法。DefaultCommand 通过 include() 方法来分配视图。
main.php 文件中包含了一个 HTML 代码和对 Request 对象的调用代码。
<!DOCTYPE html>
<html>
<head>
<title>Woo! it's Woo!</title>
</head>
<body>
<table>
<tr>
<td>Welcome to WOO</td>
</tr>
</table>
</body>
</html>
12.3.2 总结
整个前端控制器的执行流程可以通过一个序列图展示:
前端控制器 Controller 将初始化工作委托给 ApplicationHelper 对象,然后前端控制器 Controller 再从 CommandResolver 对象获取一个 Command 对象,最后调用 Command::execute() 进入业务逻辑。