一、什么是MVC
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。
MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部份分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:
(控制器Controller)- 负责转发请求,对请求进行处理。
(视图View) – 界面设计人员进行图形界面设计。
(模型Model) – 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
模型(Model) “数据模型”(Model)用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。“模型”有对数据直接访问的权力,例如对数据库的访问。“模型”不依赖“视图”和“控制器”,也就是说,模型不关心它会被如何显示或是如何被操作。但是模型中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此模型的视图必须事先在此模型上注册,从而,视图可以了解在数据模型上发生的改变。
视图(View) 视图层能够实现数据有目的的显示(理论上,这不是必需的)。在视图中一般没有程序上的逻辑。为了实现视图上的刷新功能,视图需要访问它监视的数据模型(Model),因此应该事先在被它监视的数据那里注册。
控制器(Controller) 控制器起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据模型上的改变。
二、为什么要自己开发MVC框架
网络上有大量优秀的MVC框架可供使用,本教程并不是为了开发一个全面的、终极的MVC框架解决方案,而是将它看作是一个很好的从内部学习PHP的机会,在此过程中,你将学习面向对象编程和设计模式,并学习到开放中的一些注意事项。
更重要的是,你可以完全控制你的框架,并将你的想法融入到你开发的框架中。虽然不一定是做好的,但是你可以按照你的方式去开发功能和模块。
三、开始开发自己的MVC框架
在开始开发前,让我们先来把项目建立好,假设我们建立的项目为todo,那么接下来的第一步就是把目录结构先设置好。
虽然在这个教程中不会使用到上面的所有的目录,但是为了以后程序的可拓展性,在一开始就把程序目录设置好使非常必要的。下面就具体说说每个目录的作用:
application – 存放程序代码
config – 存放程序配置或数据库配置
db – 用来存放数据库备份内容
library – 存放框架代码
public – 存放静态文件
scripts – 存放命令行工具
tmp – 存放临时数据
在目录设置好以后,我们接下来就要来顶一下一些代码的规范:
MySQL的表名需小写并采用复数形式,如items,cars
模块名(Models)需首字母大写,并采用单数模式,如Item,Car
控制器(Controllers)需首字母大写,采用复数形式并在名称中添加“Controller”,如ItemsController, CarsController
视图(Views)采用复数形式,并在后面添加行为作为文件,如:items/view.php, cars/buy.php
上述的一些规则是为了能在程序钟更好的进行互相的调用。接下来就开始真正的编码了。
第一步将所有的的请求都重定向到public目录下,解决方案是在todo文件下添加一个.htaccesss文件,文件内容为:
<IfModule mod_rewrite.c> RewriteEngine on RewriteRule ^$ public/ [L] RewriteRule (.*) public/$1 [L] </IfModule>
在我们把所有的请求都重定向到public目录下以后,我们就需要将所有的数据请求都再重定向到public下的index.php文件,于是就需要在public文件夹下也新建一个.htaccess文件,文件内容为:
<IfModule mod_rewrite.c> RewriteEngine On #如果文件存在就直接访问目录不进行RewriteRule RewriteCond %{REQUEST_FILENAME} !-f #如果目录存在就直接访问目录不进行RewriteRule RewriteCond %{REQUEST_FILENAME} !-d #将所有其他URL重写到 index.php/URL RewriteRule ^(.*)$ index.php?url=$1 [PT,L] </IfModule>
这么做的主要原因有:
可以使程序有一个单一的入口,将所有除静态程序以外的程序都重定向到index.php上;
可以用来生成利于SEO的URL,想要更好的配置URL,后期可能会需要URL路由,这里先不做介绍了。
做完上面的操作,就应该知道我们需要做什么了,没错!在public目录下添加index.php文件,文件内容为:
<?php define('DS',DIRECTORY_SEPARATOR); define('ROOT',dirname(dirname(__FILE__))); $url = $_GET['url']; require_once(ROOT.DS.'library'.DS.'bootstrap.php');
注意上面的PHP代码中,并没有添加PHP结束符号”?>”,这么做的主要原因是:对于只包含PHP代码的文件,结束标志(“?>”)最好不存在,PHP自身并不需要结束符号,不添加结束符号可以很大程度上防止末尾被添加额外的注入内容,让程序更加安全。
在index.php中,我们对library文件夹下的bootstrap.php发起了请求,那么bootstrap.php这个启动文件中到底会包含哪些内容呢?
<?php require_once(ROOT.DS.'config'.DS .'config.php'); require_once(ROOT.DS.'library'.DS .'shared.php');
以上文件都可以直接在index.php文件中引用,我们这么做的原因是为了在后期管理和拓展中更加的方便,所以把需要在一开始的时候就加载运行的程序统一放到一个单独的文件中引用。
先来看看config文件下的config .php文件,该文件的主要作用是设置一些程序的配置项及数据库连接等,主要内容为:
<?php # 设置是否为开发状态 define('DEVELOPMENT_ENVIRONMENT',true); # 设置数据库连接所需数据 define('DB_HOST','localhost'); define('DB_NAME','todo'); define('DB_USER','root'); define('DB_PASSWORD','root');
应该说config.php涉及到的内容并不多,不过是一些基础数据的一些设置,再来看看library下的共用文件shared.php应该怎么写。
<?php /* 检查是否为开发环境并设置是否记录错误日志 */ function setReporting(){ if (DEVELOPMENT_ENVIRONMENT == true) { error_reporting(E_ALL); ini_set('display_errors','On'); } else { error_reporting(E_ALL); ini_set('display_errors','Off'); ini_set('log_errors','On'); ini_set('error_log',ROOT.DS. 'tmp' .DS. 'logs' .DS. 'error.log'); } } /* 检测敏感字符转义(Magic Quotes)并移除他们 */ function stripSlashDeep($value){ $value = is_array($value) ? array_map('stripSlashDeep',$value) : stripslashes($value); return $value; } function removeMagicQuotes(){ if (get_magic_quotes_gpc()) { $_GET = stripSlashDeep($_GET); $_POST = stripSlashDeep($_POST); $_COOKIE = stripSlashDeep($_COOKIE); } } /* 检测全局变量设置(register globals)并移除他们 */ function unregisterGlobals(){ if (ini_get('register_globals')) { $array = array('_SESSION','_POST','_GET','_COOKIE','_REQUEST','_SERVER','_ENV','_FILES'); foreach ($array as $value) { foreach ($GLOBALS[$value] as $key => $var) { if ($var === $GLOBALS[$key]) { unset($GLOBALS[$key]); } } } } } /* 主请求方法,主要目的拆分URL请求 */ function callHook() { global $url; $urlArray = array(); $urlArray = explode("/",$url); $controller = $urlArray[0]; array_shift($urlArray); $action = $urlArray[0]; array_shift($urlArray); $queryString = $urlArray; $controllerName = $controller; $controller = ucwords($controller); $model = rtrim($controller, 's'); $controller .= 'Controller'; $dispatch = new $controller($model,$controllerName,$action); if ((int)method_exists($controller, $action)) { call_user_func_array(array($dispatch,$action),$queryString); } else { /* 生成错误代码 */ } } /* 自动加载控制器和模型 */ function __autoload($className) { if (file_exists(ROOT . DS . 'library' . DS . strtolower($className) . '.class.php')) { require_once(ROOT . DS . 'library' . DS . strtolower($className) . '.class.php'); } else if (file_exists(ROOT . DS . 'application' . DS . 'controllers' . DS . strtolower($className) . '.php')) { require_once(ROOT . DS . 'application' . DS . 'controllers' . DS . strtolower($className) . '.php'); } else if (file_exists(ROOT . DS . 'application' . DS . 'models' . DS . strtolower($className) . '.php')) { require_once(ROOT . DS . 'application' . DS . 'models' . DS . strtolower($className) . '.php'); } else { /* 生成错误代码 */ } } setReporting(); removeMagicQuotes(); unregisterGlobals(); callHook();
接下来的操作就是在library中建立程序所需要的基类,包括控制器、模型和视图的基类。
新建控制器基类为controller.class.php,控制器的主要功能就是总调度,具体具体内容如下:
<?php class Controller { protected $_model; protected $_controller; protected $_action; protected $_template; function __construct($model, $controller,$action) { $this->_controller = $controller; $this->_action = $action; $this->_model = $model; $this->$model =& new $model; $this->_template =& new Template($controller,$action); } function set($name,$value) { $this->_template->set($name,$value); } function __destruct() { $this->_template->render(); } }
新建控制器基类为model.class.php,考虑到模型需要对数据库进行处理,所以可以新建一个数据库基类sqlquery.class.php,模型去继承sqlquery.class.php。
新建sqlquery.class.php,代码如下:
<?php class SQLQuery { protected $_dbHandle; protected $_result; /** 连接数据库 **/ function connect($address, $account, $pwd, $name) { $this->_dbHandle = @mysql_connect($address, $account, $pwd); if ($this->_dbHandle != 0) { if (mysql_select_db($name, $this->_dbHandle)) { return 1; }else { return 0; } }else { return 0; } } /** 中断数据库连接 **/ function disconnect() { if (@mysql_close($this->_dbHandle) != 0) { return 1; } else { return 0; } } /** 查询所有数据表内容 **/ function selectAll() { $query = 'select * from `'.$this->_table.'`'; return $this->query($query); } /** 查询数据表指定列内容 **/ function select($id) { $query = 'select * from `'.$this->_table.'` where `id` = \''.mysql_real_escape_string($id).'\''; return $this->query($query, 1); } /** 自定义SQL查询语句 **/ function query($query, $singleResult = 0) { $this->_result = mysql_query($query, $this->_dbHandle); if (preg_match("/select/i",$query)) { $result = array(); $table = array(); $field = array(); $tempResults = array(); $numOfFields = mysql_num_fields($this->_result); for ($i = 0; $i < $numOfFields; ++$i) { array_push($table,mysql_field_table($this->_result, $i)); array_push($field,mysql_field_name($this->_result, $i)); } while ($row = mysql_fetch_row($this->_result)) { for ($i = 0;$i < $numOfFields; ++$i) { $table[$i] = trim(ucfirst($table[$i]),"s"); $tempResults[$table[$i]][$field[$i]] = $row[$i]; } if ($singleResult == 1) { mysql_free_result($this->_result); return $tempResults; } array_push($result,$tempResults); } mysql_free_result($this->_result); return($result); } } /** 返回结果集行数 **/ function getNumRows() { return mysql_num_rows($this->_result); } /** 释放结果集内存 **/ function freeResult() { mysql_free_result($this->_result); } /** 返回MySQL操作错误信息 **/ function getError() { return mysql_error($this->_dbHandle); } }
新建model.class.php,代码如下:
<?php class Model extends SQLQuery{ protected $_model; function __construct() { $this->connect(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME); $this->_model = get_class($this); $this->_table = strtolower($this->_model)."s"; } function __destruct() { } }
新建视图基类为template.class.php,具体代码如下:
<?php class Template { protected $variables = array(); protected $_controller; protected $_action; function __construct($controller,$action) { $this->_controller = $controller; $this->_action =$action; } /* 设置变量 */ function set($name,$value) { $this->variables[$name] = $value; } /* 显示模板 */ function render() { extract($this->variables); if (file_exists(ROOT.DS. 'application' .DS. 'views' .DS. $this->_controller .DS. 'header.php')) { include(ROOT.DS. 'application' .DS. 'views' .DS. $this->_controller .DS. 'header.php'); } else { include(ROOT.DS. 'application' .DS. 'views' .DS. 'header.php'); } include (ROOT.DS. 'application' .DS. 'views' .DS. $this->_controller .DS. $this->_action . '.php'); if (file_exists(ROOT.DS. 'application' .DS. 'views' .DS. $this->_controller .DS. 'footer.php')) { include (ROOT.DS. 'application' .DS. 'views' .DS. $this->_controller .DS. 'footer.php'); } else { include (ROOT.DS. 'application' .DS. 'views' .DS. 'footer.php'); } } }
做完了以上这么多操作,基本上整个MVC框架已经出来了,下面就该制作我们的站点了。我们要做的站点其实很简单,一个ToDo程序。
首先是在我们的/application/controller/ 目录下面新建一个站点控制器类为ItemsController,命名为itemscontroller.php,内容为:
<?php class ItemsController extends Controller { function view($id = null,$name = null) { $this->set('title',$name.' - My Todo List App'); $this->set('todo',$this->Item->select($id)); } function viewall() { $this->set('title','All Items - My Todo List App'); $this->set('todo',$this->Item->selectAll()); } function add() { $todo = $_POST['todo']; $this->set('title','Success - My Todo List App'); $this->set('todo',$this->Item->query('insert into items (item_name) values (\''.mysql_real_escape_string($todo).'\')')); } function delete($id) { $this->set('title','Success - My Todo List App'); $this->set('todo',$this->Item->query('delete from items where id = \''.mysql_real_escape_string($id).'\'')); } }
接下来就是先建站点的模型,在我们的/application/model/ 目录下面先建一个站点模型类为Item,内容直接继承Model,代码如下:
<?php class Item extends Model { }
最后一步是设置我们站点的视图部分,我们现在/application/views/目录下新建一个items的文件夹,再在items文件夹下建立与控制器重Action相同的文件,分别为view.php,viewall.php,add.php,delete.php,考虑到这么页面中可能需要共用页首和页尾,所以再新建两个文件,命名为header.php,footer.php,每个文件的代码如下:
view.php文件:查看单条待处理事务
<h2><?php echo $todo['Item']['item_name']?></h2> <a href="../../../items/delete/<?php echo $todo['Item']['id']?>"> <span>Delete this item</span> </a>
viewall.php文件:查看所有待处理事务
<form action="../items/add" method="post"> <input type="text" value="I have to..." onclick="this.value=''" name="todo"> <input type="submit" value="add"> </form> <br/><br/> <?php $number = 0?> <?php foreach ($todo as $todoitem):?> <a href="../items/view/<?php echo $todoitem['Item']['id']?>/<?php echo strtolower(str_replace(" ","-",$todoitem['Item']['item_name']))?>"> <span> <?php echo ++$number?> <?php echo $todoitem['Item']['item_name']?> </span> </a><br/> <?php endforeach?>
add.php文件:添加待处理事务
<a href="../items/viewall">Todo successfully added. Click here to go back.</a><br/>
delete.php文件:删除事务
<a href="../../items/viewall">Todo successfully deleted. Click here to go back.</a><br/>
header.php:页首文件
<html> <head> <title><?php echo $title?></title> <style> .item {width:400px;} input {color:#222222;font-family:georgia,times;font-size:24px;font-weight:normal;line-height:1.2em;color:black;} a {color:#222222;font-family:georgia,times;font-size:24px;font-weight:normal;line-height:1.2em;color:black;text-decoration:none;} a:hover {background-color:#BCFC3D;} h1 {color:#000000;font-size:41px;letter-spacing:-2px;line-height:1em;font-family:helvetica,arial,sans-serif;border-bottom:1px dotted #cccccc;} h2 {color:#000000;font-size:34px;letter-spacing:-2px;line-height:1em;font-family:helvetica,arial,sans-serif;} </style> </head> <body> <h1>My Todo-List App</h1>
footer.php:页尾文件
</body> </html>
当然还有一个必不可少的操作就是在数据中中建立一张表,具体代码如下:
CREATE TABLE IF NOT EXISTS `items` ( `id` int(11) NOT NULL AUTO_INCREMENT, `item_name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=17 ;
至此一个使用MVC开发的网站就开发完成了,你现在可以通过访问http://localhost/todo/items/viewall 查看新建的站点。