[php]應用控制器(一)
前端控制器已經能很好地在一個地方集中處理請求並選擇適當的Command了,但是Command子類對象自己處理了視圖的分配工作。要是能夠使用一個類(根據Command處理後返回的狀態值)來決定視圖並返回到前端控制器,再由前端控制器來調用視圖顯示,這樣的話前端控制器就處於視圖層和業務層的中間了,而且也很好地把Command和視圖分開了。應用控制器是個好的解決方案。
應用控制器負責映射請求到命令,並映射命令到視圖。它允許改變應用程式的流程而不需要修改核心代碼。它能把Command類解放出來,讓Command類集中精力完成自己的工作,包括處理輸入、調用應用程式邏輯和處理結果等。
應用控制器是一個類(或者一組類),它協助前端控制接管處理請求的任務而且又把適當的視圖返回給前端控制器調用。那麼應用控制是什麼方式啟動並執行呢?它是通過一個xml設定檔來決定Command和視圖工作的方式。比如下面這個xml檔案(有點像struts的方式):
sqlite://data/demo.dbrootrootmainmainerrorlist_studentsadd_studentListStudentssimple_add_student
可以看到xml中中可以包含、、三類子項目,分別表示的是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 = (string)$options->dsn; $this->ensure($dsn, 'No dsn found!'); \demo\base\ApplicationRegistry::getInstance()->setDSN($dsn); // $map = new ControllerMap(); // 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); } // foreach ($options->controller->command as $cvf) { $cmd = trim((string)$cvf['name']); // if($cvf->classalias) { $classroot = (string)$cvf->classalias['name']; $map->addClassroot($cmd, $classroot); } // 、 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); } } // 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::instance(); 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 setCommand(\demo\command\Command $cmd) { $this->lastCommand = $cmd; } public function getLastCommand() { return $this->lastCommand; } /** * * @param unknown_type $name * @param unknown_type $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兩個類了。