欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[php]前端控制器

程序员文章站 2022-05-29 21:38:35
...

当一个请求达到系统时,系统必须能够理解请求中的需求是什么,然后调用适当的业务逻辑进行处理,最后返回相应结果。对于简单的程序,整个过程可以放在视图中,但随着系统的增长,这种处理方式不能很好地满足请求、调用业务逻辑和显示适当视图。那么我们就需要在较大的系统中较好地管理这三者的关系,我们可以划分出视图层与命令和控制层。

视图层与命令和控制层之间的界线通常比较模糊,又是也把这两个层称为表现层。

前端控制器单一入口(流行的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子类处理业务。

[php]前端控制器



下面分别是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的类图:

[php]前端控制器

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子类中的处理试图当时还不是最好的,能够把试图和命令分离开来效果会好一些。