[php]应用控制器(一)
前端控制器已经能很好地在一个地方集中处理请求并选择适当的Command了,但是Command子类对象自己处理了视图的分配工作。要是能够使用一个类(根据Command处理后返回的状态值)来决定视图并返回到前端控制器,再由前端控制器来调用视图显示,这样的话前端控制器就处于视图层和业务层的中间了,而且也很好地把Command和视图分开了。应用控制器是个好的解决方案。
应用控制器负责映射请求到命令,并映射命令到视图。它允许改变应用程序的流程而不需要修改核心代码。它能把Command类解放出来,让Command类集中精力完成自己的工作,包括处理输入、调用应用程序逻辑和处理结果等。
先来看看顺序图吧。
应用控制器是一个类(或者一组类),它帮助前端控制接管处理请求的任务而且又把适当的视图返回给前端控制器调用。那么应用控制是什么方式运行的呢?它是通过一个xml配置文件来决定Command和视图工作的方式。比如下面这个xml文件(有点像struts的方式):
<?xml version="1.0" encoding="UTF-8"?> <options> <dsn>sqlite://data/demo.db</dsn> <username>root</username> <password>root</password> <controller> <view>default</view> <view status="CMD_OK">success</view> <view status="CMD_ERROR">error</view> <command name="ListStudents"> <view>list_students</view> </command> <command name="AddStudent"> <view>add_student</view> <status value="CMD_OK"> <forward>ListStudents</forward> </status> </command> <command name="SimpleAddStudent"> <classalias name="AddStudent"/> <view>simple_add_student</view> </command> </controller> </options>
可以看到xml中<command>中可以包含<view>、<status>、<forward>三类子元素,分别表示的是Command对应的视图、Command处理业务后的状态值、Command处理后的跳转(这里跳转到另一个Command)。
从xml的结构就能了解到Command类需要一些新的属性status了。
namespace demo\command; /** * 抽象父类 */ abstract class Command { // 状态值映射 private static $STATUS_STRINGS = array( 'CMD_DEFAULT' => 0, 'CMD_OK' => 1, 'CMD_ERROR' => 2, 'CMD_INSUFFICIENT_DATA' => 3 ); // 当前状态值 private $status = 0; public final function __construct() { // 子类不能重写构造函数 } /** * 按状态字符串返回状态值 * @param unknown_type $staStr */ public static function status($stauStr = 'CMD_DEFAULT') { if (empty($stauStr)) { $stauStr = 'CMD_DEFAULT'; } return self::$STATUS_STRINGS[$stauStr]; } /** * 调用子类实现的doExecute * @param \demo\controller\Request $request */ public function execute(\demo\controller\Request $request) { $this->doExecute($request); } protected abstract function doExecute(\demo\controller\Request $request); }
系统中有个专门获取配置的助手类ApplicationHelper可以实现对xml配置的读取。由于xml中的元素结构相对灵活一些,那么就需要一个ControllerMap来管理各元素中的值和Command、视图的一一映射关系。
namespace demo\controller; class ControllerMap { private $classrootMap = array(); private $forwardMap = array(); private $viewMap = array(); public function addClassroot($cmd, $classroot) { $this->classrootMap[$cmd] = $classroot; } public function getClassroot($cmd) { if (isset($this->classrootMap[$cmd])) { return $this->classrootMap[$cmd]; } return $cmd; } public function addForward($cmd = 'default', $status = 0, $newCmd) { $this->forwardMap[$cmd][$status] = $newCmd; } public function getForward($cmd, $status) { if (isset($this->forwardMap[$cmd][$status])) { return $this->forwardMap[$cmd][$status]; } return null; } public function addView($cmd = 'default', $status = 0, $view) { $this->viewMap[$cmd][$status] = $view; } public function getView($cmd, $status) { if (isset($this->viewMap[$cmd][$status])) { return $this->viewMap[$cmd][$status]; } return null; } }
首先需要获取xml中的配置ApplicationHelper类的getOptions。
namespace demo\controller; /** * 助手类:获取xml配置 * 单例模式 * */ 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 = \demo\base\ApplicationRegistry::getInstance()->getDSN(); $camp = \demo\base\ApplicationRegistry::getInstance()->getControllerMap(); if (is_null($dsn) || is_null($camp)) { $this->getOptions(); } } /** * 获取xml配置 */ public function getOptions() { // xml $this->ensure(file_exists($this->config), "Could not find options file!"); $options = @simplexml_load_file($this->config); var_dump($options); $this->ensure($options instanceof \SimpleXMLElement, 'Could not resolve options file!'); // <dsn> $dsn = (string)$options->dsn; $this->ensure($dsn, 'No dsn found!'); \demo\base\ApplicationRegistry::getInstance()->setDSN($dsn); // <controller> $map = new ControllerMap(); // <view> foreach ($options->controller->view as $default_view) { $stauStr = trim((string)$default_view['status']); $status = \demo\command\Command::status($stauStr); $map->addView('default', $status, (string)$default_view); } // <command> foreach ($options->controller->command as $cvf) { $cmd = trim((string)$cvf['name']); // <classalias> if($cvf->classalias) { $classroot = (string)$cvf->classalias['name']; $map->addClassroot($cmd, $classroot); } // <view>、<forward> if ($cvf->view) { $view = trim((string)$cvf->view); $forward = trim((string)$cvf->forward); $map->addView($cmd, 0, $view); if ($forward) { $map->addForward($cmd, 0, $forward); } } // <status> foreach ($cvf->status as $status) { $stauStr = trim((string)$status['value']); $view = trim((string)$status->view); $forward = trim((string)$status->forward); $stau = \demo\command\Command::status($stauStr); if ($view) { $map->addView($cmd, $stau, $view); } if ($forward) { $map->addForward($cmd, $stau, $forward); } } } // var_dump($map); \demo\base\ApplicationRegistry::getInstance()->setControllerMap($map); } private function ensure($expr, $msg) { if (!$expr) { throw new \demo\base\AppException($msg); } } }
获取xml配置的过程是一个比较费时的操作,可以先把ControllerMap对象序列化到文件中去,之后可以通过ApplicationRegistry获取并把它当做全局数据缓存起来。
/** * Application作用域 */ class ApplicationRegistry extends Registry { private static $instance; private $freezedir = "./data"; // 此处硬编码,具体根据实际情况配置 private $values = array(); private $mtimes = array(); private function __construct() { } public static function getInstance() { if (isset(self::$instance) == false) { self::$instance = new self(); } return self::$instance; } /** * 从序列化文件中获取$key数据 */ protected function get($key) { $path = $this->freezedir . DIRECTORY_SEPARATOR . $key; if (file_exists($path)) { // 清楚文件缓存 clearstatcache(); $mtime = filemtime($path); if (isset($this->mtimes[$key]) == false) { $this->mtimes[$key]=0; } // 文件最近被修改过,重新反序列化新的数据 if ($mtime > $this->mtimes[$key] ) { $data = file_get_contents($path); $this->mtimes[$key] = $mtime; return ($this->values[$key] = unserialize($data)); } } if (isset( $this->values[$key]) == true) { return $this->values[$key]; } return null; } protected function set($key, $val) { $this->values[$key] = $val; $path = $this->freezedir . DIRECTORY_SEPARATOR . $key; if (file_exists($path)) { touch($path); } file_put_contents($path, serialize($val)); $this->mtimes[$key]=time(); } public function getDSN() { if (isset($this->values['dsn'])) { return $this->values['dsn']; } return self::getInstance()->get('dsn'); } public function setDSN($dsn) { return self::getInstance()->set('dsn', $dsn); } /** * * @param \demo\controller\ControllerMap $map */ public function setControllerMap(\demo\controller\ControllerMap $map) { self::getInstance()->set('cmap', $map); } public function getControllerMap() { if (isset($this->values['cmap'])) { return $this->values['cmap']; } return self::getInstance()->get('cmap'); } /** * 获取AppController */ public function getAppController() { $obj = self::getInstance(); if (!isset($obj->appController)) { $cmap = $obj->getControllerMap(); $obj->appController = new \demo\controller\AppController($cmap); } return $obj->appController; } // 其它一些列getter和setter // ...... }
这次需要实现更加复杂的调用,比如forward,那么就需要简单地修改Request类的代码了,使它能够符合调用逻辑的需要。
namespace demo\controller; /** * 封装用户请求 * Request */ class Request { private $properties; private $feedback = array(); // 保存业务对象,可以供给视图使用 private $objects = array(); // 保存上一个已执行的Command对象 private $lastCommand; public function __construct() { $this->init(); $this->filterProperties(); \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); } /** * * @param \demo\command\Command $cmd */ public function setLastCommand(\demo\command\Command $cmd) { $this->lastCommand = $cmd; } public function getLastCommand() { return $this->lastCommand; } /** * * @param string $name * @param Domain $object */ public function setObject($name, $object) { $this->objects[$name] = $object; } public function getObject($name) { if (isset($this->objects[$name])) { return $this->objects[$name]; } return null; } }
现在已经添加了能够保存映射关系的类ControllerMap,修改了Request、Command、ApplicationHelper、ApplicationRegistry
,主要是添加,少量修改之前的代码。
上面这些类已经能够完成系统的初始化了,包括读取xml配置(ApplicationHelper)、全局变量访问(Registry);
之后就是核心部分了:Controller和AppController两个类了。