MVC 模式在 Java 中表现的尤为出众,不光 Swing 是按照 MVC 来设计的,而且 Java 的 Web 框架也是 MVC1、MVC2 的。MVC 模式对于开发维护确有许多好处,所以 PHP 的框架,如 Zend、Symfony,PHP 的产品 WordPress 和 Joomla 都应用了 MVC 模式。PHP 不像 Servlet 那样有成熟的规范,如 web.xml、servlet、filter 等,但变换着一些把式同样能实现出优雅的 MVC 模式。这里简单介绍一下 PHP 是如何实现 MVC 模式,参照了了 Zend 的实现,我觉得还有许多改进的地方。说明的时候会拿它的各部分与 Struts1 的 MVC 相比较。
在 HTTP 环境中的 MVC 模式一句话描述就是:控制器根据 URI,把请求转给相应的 Action,由 Action 调用模型方法处理或得到数据,再选择相应的视图呈现界面。用过 Struts1 的请保留一些 Struts1 的实现原理,现在来看 PHP 的实现方式。
本例参考了 《PHP 高级程序设计 模式、框架与测试》一书中关于 MVC 的介绍,因本人受 Struts 等 MVC 的影响,所以对原书中的示例进行了大刀阔斧、面目全非的改造。代码结构如下:
lib 目录中为本 MVC 的核心代码,application 目录中为应用代码,index.php 为入口文件兼具引导功能。
第一要素:统一口径(/index.php):
要让 HTTP 请求都能进入到我们的 MVC 框架来,需要流经一个统一的入口,这里就是 /index.php 文件,也就是必须全部用 http://localhost/MvcSample/index.php/controller=user&name=Unmi.. 这样的方式来访问,你可以用某种方式让其他的 php 文件被禁止直接访问。
在 Struts1 中,是在 web.xml 中配置由 ActionServlet 处理所有的 *.do 的请求,Struts2 也是在 web.xml 中配置由 FilterDispatcher 来拦截所有的请求。而 PHP 没有像 Java Web 那么多的规范,但可以借助于 mod_rewrite 模块,将某些请求转发给 /index.php 处理,具体做法是在应用的根目录下建立一个 .htaccess 文件,内容为:
RewriteEngine On
RewriteRule !\.(js|gif|png|css)$ index.php
意思为除图片、js、css 文件都把请求定向给 index.php,当然 .htaccess 还是应该进行更优化配置的,还有就是在 Apache 中要启用 mod_rewrite 模块。有了 .htaccess 后,你随便输入些像 http://localhost/MvcSample/sfsf/sdfsdf 的地址都不会是 404 错误,而是全部转向到了 /index.php。/index.php 便成了一个统一的入口,后面发生的事情可受控了。
来看看 index.php 文件的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php //导入组件 require_once('lib/front.php'); require_once('lib/controller.php'); require_once('lib/view.php'); //初始化前端控制器 $front = new FrontController(); //转发请求到相应的控制器 //route 就是路由的意思,就是分布请求,Struts 里是用 Dispatch 的概念 $front->route(); //显示页面数据 echo $front->getBody(); |
第二要素:前端控制器(lib/front.php)
在 index.php 中调用了前端控制器 FrontController 的 route() 路由方法,根据 URL 中的参数找到相应的控制器,调用相应的方法,获得数据并使用视图显示出来。例如有 URL http://localhost/MvcSample/index.php?controller=user&method=user_list&name=Unmi,在 route() 方法中就会调用 UserController 的 user_list 方法执行业务逻辑,获得相应的数据,最后使用视图 views/user/list.php 显示页面。
需要特别注意的是控制器名与脚本文件、视图文件的对应规则,可自行约定。脚本文件需要临时 include 进来,你也可以用 SPL 的自动加载机制来加载文件。看代码 front.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?php class FrontController{ protected $_controller, $_method, $_params, $_body; public function __construct(){ $this->_controller = $_REQUEST['controller']; $this->_method = $_REQUEST['method']; $this->_params = $_REQUEST; } public function route(){ //引入控制器文件 include dirname(__FILE__) .'/../application/controllers/'.$this->_controller.'_controller.php'; $controller_class_name = ucwords($this->_controller . 'Controller'); $rc = new ReflectionClass($controller_class_name); $controller = $rc->newInstance(); $view_file; if($rc->isSubclassOf('AbstractController')){ if(!empty($this->_method)){ if($rc->hasMethod($this->_method)){ $method = $rc->getMethod($this->_method); $view_file = $method->invoke($controller,$this->_params); }else{ throw new Exception("Method " . $this->_method . " not exists"); } }else{ $view_file = $controller->execute($this->_params); } $view = new View(); //返回数据是数组则分散以键值对存到 View 里 if(is_array($controller->get_data())){ foreach($controller->get_data() as $key=>$val){ $view[$key] = $val; } }else{ //非数组则以 data 键存到 View 里 $view->data = $controller->get_data(); } $result = $view->render($view_file); $this->setBody($result); }else{ throw new Exception("Not Extends AbstractController"); } } public function getController(){ return $this->_controller; } public function getParams(){ return $this->_params; } public function getBody(){ return $this->_body; } public function setBody($body){ $this->_body = $body; } } |
这个 FrontController 的功能和 Struts1 的 ActiveServlet、DispatchAction 十分相似。因为 PHP 没有应用范围内全局的东西,像 Servlet 的 JVM,也没有像 JSP/ASP 那种 application 的变量,所以很多 PHP 的 MVC 框架都基本不用配置文件来配置 URL--Controller(Action)--View 间的映射关系。其实如果应用上内存高速缓存,如 Memcached,或内存数据数据库,如 Sqlit,我想也可以做出像 Struts 那样的 MVC 框架的。
第三要素:视图呈现数据(lib/view)
在 FrontController 的 route() 方法最后要调用视图来渲染数据,显示出来。视图最终要关联到某个模板,可以是 php 文件,也可以用 php 专门的模板组件。看看 view.php 的代码,继承自 ArrayObject 来存放显示数据,
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php class View extends ArrayObject{ public function __construct(){ parent::__construct(array(), ArrayObject::ARRAY_AS_PROPS); } public function render($file){ ob_start(); include($file); return ob_get_clean(); } } |
第四要素:所有控制器的基类(controller.php)
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php //你的自定义控制器要继承这个抽象类 abstract class AbstractController{ protected $_data; //存储数据 //默认的执行方法 abstract function execute($params); function get_data(){ return $this->_data; } } |
你自己写的所有的控制器都必须继承自它,execute($params) 是 controller 的默认执行方法。这个控制器相当于 Struts1 中的 Action 类。
MVC 中的模型没什么好说的,它其实不影响到框架的实现,无论用什么框架,模型都是必不可少的。好啦,到现在,PHP MVC 的基础框架就完成了,开始来应用它了,应用代码在 application 目录中,还是分布来看具体的应用步骤:
第一步:实现自己的控制器(application/controllers/user_controller.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php //用户控制器 class UserController extends AbstractController { //默认执行方法,调用模型方法获取数据 //页面显示的数据设置给 $_data 进而传递给 View public function execute($params){ include(dirname(__FILE__) . '/../models/user_model.php'); $user_model = new UserModel(); $this->_data = array('name'=>$user_model->wo_am_i()); return dirname(__FILE__) . '/../views/user/index.php'; } //URL 中用 method=user_list 指定了参数则会执行这个方法 public function user_list($params){ include(dirname(__FILE__) . '/../models/user_model.php'); $user_model = new UserModel(); $this->_data = $user_model->get_user_list($params['id']); return dirname(__FILE__) . '/../views/user/list.php'; } } |
请求参数中没有 method 参数时执行 execute() 方法,当 method=user_list 时执行上面的 user_list 方法,它们分别转向到不同的模板文件,一个是 views/user/index.php,另一个是 views/user/list.php 文件。
第二:实现你的 Model(application/model/user_model.php)
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php //用户模型,就是些业务方法,严格来讲模型是些数据对象 class UserModel { public function wo_am_i(){ return "Unmi"; } public function get_user_list($id){ $array = array('Unmi','Fantasia','Kypfos'); return $array; } } |
这一步,前面也提过,没什么好说的,那是业务相关的东西,该怎么就怎么。
第三:显示界面的模板文件(application/views/user/index.php 和 application/views/user/list.php)
模板仍然是属于视图的范畴,是视图的某种表现方式之一,这里用到的是 php 文件作为模板,请看那两文件内容:
1 2 |
<!-- application/views/user/index.php --> Hello, I am <?php echo $this->name; ?>! |
1 2 3 4 5 6 7 8 9 |
<!-- application/views/user/list.php --> 用户列表: <table border="1" style="border-collapse: collapse"> <?php foreach($this as $key=>$value){ echo "<tr><td>" . ($key+1) . "</td><td>$value</td></tr>"; } ?> </table> |
注意,在这两个 PHP 文件中是如何取得后台数据的。在控制器或模型类中可以有多个实现方法,而一个网站的 PHP 显示模板文件应该相对较多,所以模板文件进一步按控制器名再分一级目录来存放。
最后:看看执行效果
假设你你的 PHP 站点部署后要通过 http://localhost/MvcSample/index.php 来访问,比如直接把 MvcSample 目录拷到了 Apache 的 htdocs 目录中。
URL: http://localhost/MvcSample/index.php?controller=user
页面显示:
Hello, I am Unmi!
说明:首先进到 index.php,调用 FrontController.route() 方法,根据 controller=user,且无 method 参数,定位到 UserController.execute() 方法,最后用 views/user/index.php 来显示数据。
URL: http://localhost/MvcSample/index.php?controller=user&method=user_list&name=Unmi
页面显示:
用户列表:
1 | Unmi |
2 | Fantasia |
3 | Kypfos |
说明:首先进到 index.php,调用 FrontController.route() 方法,根据 controller=user&method=user_list 参数,定位到 UserController.user_list() 方法,最后用 views/user/list.php 来显示数据。
如果是用 PHP 实现一个像 Struts 那样的 MVC 框架,我想应该比 Struts 来得简单。有闲时值得一试。
本文链接 https://yanbin.blog/php-mvc-reference-implementation/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。