现在的位置: 首页 > 综合 > 正文

[转]MVC with PHP(一)

2013年06月01日 ⁄ 综合 ⁄ 共 12526字 ⁄ 字号 评论关闭

MVC with PHP 实践(一)

作者:大师兄

MVC是什么?MVC是一种设计模式,将一个应用程序分为三个部分Model(模型),View(视图), Controller(控制器),在程序中各自负责处理自
己的任务.SUN公司在JSP中将它发挥的淋漓尽致,尤其在structk中,它采用JSP来做View,severlet来做Controller,Bean做Model,最大程度上提高
了程序的可维护性与协调开发性,在最近开发的一个项目中我就使用了这种思想,现在就拿出来给大家分享,让更多人明白它的实现机理.
首先来介绍一下MVC.所谓的M,英文为model,是模型的意思,这里可能译为中文就不好理解了,不过没关系,大家只要明白它是用来表示
业务数据及业务规则的,再说的通俗一点,就是由它来对业务数据库进行操作,并在操作之前对数据进行验证.实际上它就是以类的方式实现以往
的函数的形式,只是将它更有效的组织起来罢了,这就是逻辑业务层.

所谓的V,英文为view,视图的意思,简单讲就是一个WEB界面,用户通过提交请求来得到自己希望得到的页面,这个页面就是一个view,如果
使用过VB或是DELPHI之类的RAD开发工具,大家就是想到,实际上V就是它们之中的那个form,就是那个使用鼠标拖拉出来的界面.

所谓的C,英文为controller,英文意思为控制器,控制什么东西呢?控制的内容其实有两种:1.用户的请求2.view的显示.根据这两个功能可
以把controller分为两个部分,一 frontController:引用phpmvc中的名词来讲叫前端操作器,主要的任务就是处理用户请求,将用户以POST或是
GET甚至一个普通的链接进行分析处理,然后调用model来进行处理. 二. 对view的显示. 处理完的用户请求要以后适的方式返回给用户
,controller负责将model处理完的结果以view的形式返回.实际上这个过程很简单,用户提交请求===>controller分析请求,调用
model====>model处理数据,与数据库交互,返回处理结果===>controller得到结果,调用合适的view来返回界面给用户.

下面讲一个简单的例子来说明一下它的执行教程:
场景: 一个新闻显示的页面,现在用户将选择一条他感兴趣的新闻然后点击(所有新闻都在数据库里保存,而非静态生成)....
在这个过程中发生了什么事了呢?用户点击一条感兴趣的新闻后,就是向服务器提交一个请求,请求服务器返回这个新闻页的内容,服务器
接到它的请求后从数据库里调出ID为请求N的新闻,然后在一个新闻页里显示出来,就这么简单.来看看在MVC模式下它是怎么执行的:用户请求提
交给controller,controller通过分析用户请求的URL,得知的用户请求是显示ID为n的,由它调用新闻类model的getOne方法,将告诉它要提取ID为
n的新闻,新闻类model在接授到这个指令后调用getOne方法, getOne接授一个参数ID,使用一个select语句从数据库里提取出ID=n的这条新闻及
相关信息,然后将返回集交给controller, contrller得到返回的结果后采用模板填充的办法将相关信息填在新闻页模板上,然后将它返回给用户
,这个模板,就是所谓的view.

看上去MVC是把一件简单的事情复杂化了,当然,这里与我讲的这个例子有关,例子是简单一点,所以看不出MVC有什么优势,反而给人的感觉
是简单的事情复杂化了,其实不然. 为什么呢?如果整个程序由一人来开发,那么感觉确实有些复杂复杂,但有没有想过程序如果由多人来开发呢?
即使采用模板技术,也仅可以分开程序员与美工,而不能实现程序与业务规则(业务逻辑)的分离,如果把业务规则也分离出来,使得一部分程序员
集中处理业务规则,而另一部分人集中处理业务返回数据及页面的显示,美工只需要做模板,这样是不是可以提高工作效率呢?MVC就是这个思路,
现在行的MVC被称为model2,它是由这三层所形成的,MVC model1就是只实现view与程序的分离,就像写VB或是delphi一样(当然,这里我说的是那
种一方面设计界面,另一方面将代码也界面相连的那种技术...),实际上模板技术就是MVCmodel1.

好了好了,其它的东西就不多说了,来看看我的MVC with PHP实践.
首先,view采用smarty,实际上MVC中采用模板是必然,我选择smarty是因为它的功能强大,再者就是用的顺手,实际从效率角度上讲,它是有
点低,有很多其它的模板技术可以选择,甚至有的高手自己写模板解析程序.
model采用ADODB,ADODB配合smarty是一个绝妙的组合,使用它可以方便的提取smarty在循环块中使用的二维数组,所以在我model中就使用
ADODB进行操作数据库,将smarty重新封装,形成自己需要的model.当然,这里又是以效率换取便利了,而我的程序开发数据库环境为mysql,但将来
可能向oracle转,实在不愿意自己写一个数据库操作类了,再者如果使用标准的东西也利于团队新成员快速进行角色.

好了,来看看程序结构:

+/网站目录结构
|----+comm (程序公用目录,配置文件及FrontController放置在这里)
| |
| |----+adodb (ADODB数据库连接类)
| |----+smarty (smarty模板类)
| |
| |----config.inc.php (全局配置文件,包含数据配置及网站路径等的配置)
| |----application.class.php (应用程序主类,也就是frontController类,这里名为
| | application,主要负责解析路径)
| |----exception.class.php (程序中所有用到的例外类的定义)
| |----security.class.php (用户权限检测及记录模块动作的接口)
|
|----+log (程序日志文件)
| |
| |----actionLog.txt (用户执行的模块及动作记录日志)
| |----userLog.txt (用户登录记录)
|
|----+model (Model定义,所有的模块与数据库进行交换数据的类)
| |
| |----+comm //公用目录,只存储模型的基类
| | |
| | |-----model.class.php (所有model的父类,为一个抽象类,完成ADODB的初始化)
| |
| |----+login (用户登录管理模块中的model目录)
| | |
| | |-----loginModel.class.php (用户登录控制的商业模型)
| |
| |----+customer (客户管理模块中的model目录,结构与Login相同)
| |----+.... (其它模块的Model)
|
|----+controller (Controller定义,存放所有模块的View控制器)
| |
| |----+comm (公用目录,只存储控制器的基类)
| | |
| | |----controller.class.php (所有controller类的父类,为一个抽象类,完成Smarty的初始化)
| |
| |---+login (用户登录controller目录)
| | |
| | |-----loginController.class.php (用户登录controller类文件)
| |
| |---+customer (客户管理模块中的Controller类目录,结构与Login相同)
| |---+... (其它的模块的Controller)
|
|----+view
| |
| |---+templates (smarty用到的编译目录)
| |---+cache (smarty的缓存目录)
| |
| |---+login (所有用户登录所用到的模板)
| |---+customer (所有客户信息所用到的模板)
| |---... (其它用到的模板)

乍一看,还真有点复杂,不过不急,你听我慢慢给你讲它们的用途.
首先是comm目录,这个好理解,存放系统的配置文件,同时它还包含一个frontContorller,叫"前端控制器",就这么叫吧,文件是
application.class.php, 由它来负责将型如"work?module=xxx&action=yyy"的URL转化为PHP能解析的文件,简单的讲,就是由它来调用那些与实
现URL所指定的功能的文件,细讲来就是先从这个URL中取得controller,然后controller调用model来完成对数据库的交互,最后用将model中得来
的数据使用view来显示. 在这个类中,我增加了两个功能:
1. 权限检测. 服务器在得到"work?module=xxx&action=yyy"这样的请求后在执行调用controller之前首先要完成对用户权限的检测,如果当
前用户没有权限执行这个模块的这个动作,它将会抛出一个例外叫PermissionNotAllowException的例外,这个一个自定义的例外,在
comm/exception.class.php中有它的定义.在这里因为会检测每一个模块的每一个动作,所以就连登录检测也不例外,如果这里不对它进行处理,
那么用户就无法进行登录,所以在文件中我将module=login&action=show与module=login&action=check排除在权限检测范围外,也就说任何人都
可以进行用户登录框的显示与用户登录检测,而其它的模块的动作都要用户登录后获取权限后才可以执行.这里的权限在数据库里保存,当用户进
行登录验证时通过验证则从数据库中提取这些值,并将它形成一个二维数组型如:
$_SESSION["userPermission"] = array(array("module"=>login, "action"=>show),
array("module"=>login, "action"=>check),
array("module"=>customer, "action"=>show),
array("module"=>customer, "action"=>del),
array("module"=>customer, "action"=>modify),
...);
2.日志记录. 日志管理是做项目开发时必要的东西,通过它可以把用户所有对系统进行的操作记录下来,为了简单,我这里没有考虑到多用户
使用的情况,只适合单用户.(不说大家也知道,多用户我得控制对日志文件的读写操作....)
其它的exception.class.php是系统中要用到的一些自定义例外,security.class.php是进行用户安全检测与日志记录的两个接口的声明,为
什么没有考虑把它们做成类然后在application.class.php中进行继承?因为我觉的它们没有这种关系,而且PHP5也支持接口(interface),所以就
定义了interface了.config.inc.php是站点的配置文件.

其次.log目录,这里为日志文件存放目录.有两个日志文件,一个是系统动作日志,就是我上面提到的那个,在执行每一个动作时都要进行记录
的,第二个简单些,就是用户登录的记录.

3. model, controller, view目录. 这3个目录分别就是传说中的MVC结构了,在做系统规划时以功能进行划分模块,每一模块分别在这三个
目录中都有对应的目录,例如,上面划分了一个用户登录管理模块, 模块名为login,那么就要在这3个目录中分别建立login目录,目录里存放这个
模块的相关文件. 那么model中的login中存放用户登录的model,主要用它来进行用户登录的验证(与数据库交互),而controller目录中则存放有
执行用户登录特定模块的动作执行文件,它们都是以一个类的形式存在的. 最后view中的login目录下存放就是所有登录时要显示的模板文件,没
什么特别的地方.

而所谓的框架(framework),也就上边的这种结构了,在这个框架下,你定义自已需要的功能模块,然后使用framework指定的方式进行工作,好
了,让我们从分析源程序入手吧,先就从这个frontController入手,看看它是怎样工作的:
==================================
application.class.php
==================================
/**
* FileName: application.class.php
* Introduce: 应用程序框架
*
* @author: 大师兄
* @Email: teacherli@163.com
* @version $Id$
* @copyright 2004-10-26
**/
include_once ("config.inc.php"); //全局配置文件
include_once ("security.class.php"); //提供安全检测的类
include_once ("exception.class.php"); //例外类

class Application implements Security, Log
{
private $controllerFile; //控制器类文件名
private $contrllerClass; //控制器类名
private $action; //url中的动作名
private $module; //url中用到的模块名称

private $userName; //用户名
private $permission; //用户权限

private $isLog; //是否进行记录动作日志
private $logFile; //动作日志文件名

public function __construct()
{
$this->userName = $_SESSION["userName"];
$this->permission = &$_SESSION["userPermission"];

$this->isLog = IS_ACTIONLOG;
$this->logFile = ACTIONLOG_FILE_NAME;
}

/**
* 检测用户是否登录,实现Security接口
*
* @return bool
**/
private function checkUserLogin()
{
if("" != $this->userName)
return true;
else
return false;
}

/**
* 检测用户权限,如果权限允许返回TRUE,否则返回FALSE
* 实现Security接口
*
* @param string $module
* @param string $action
* @return bool
**/
private function checkPermission($module, $action)
{
$permissionArray = array("module"=>$module, "action"=>$action);

for($i = 0; $i < count($this->permission); $i++)
{
$row = array_diff($permissionArray, $this->permission[$i]);
if(empty($row))
{
return true;
}
unset($row);
}
return false;

}

/**
* 检测用户是否具有执行$module的$action的权限
* 实现Security接口
*
* 注意:
* 因为将用户验证也放入这部分,所有如果当
* $module != "Login"时首先要验证用户是否登录,
* 然后才进行验证用户的权限
**/
private function checkSecurity($module, $action)
{
if("Login" != $module)
{
if(!$this->checkUserLogin())
throw new UserNotLoginException("对不起,您还没有进行登录,不能进行任何操作!
");
if(!$this->checkPermission($module, $action))
throw new Exception("对不起,你无权执行 $module 模块的 $action 操作...
");
}
}

/**
* 将执行动作记录到actionLog中
* 实现Log接口
*
* @param string $module
* @param string $action
* @return bool
**/
private function writeLog($module, $action)
{
if($this->isLog)
{
if(file_exists($this->logFile))
{
if(!$fp = fopen($this->logFile, "a"))
throw FileNotOpenException("动作记录文件: " . $this->logFile . "不能打开!
/n");
}else
{
if(!$fp = fopen($this->logFile, "w"))
throw FileNotOpenException("动作记录文件: " . $this->logFile . "不能打开!
/n");
}

$Recond = "用户: '$this->userName' 于 '" . date("Y-m-d H:i:s") . "' 执行模块 '" . $module . "' 中的 '" .
$action . "' 动作, 参数表为: '" . $_SERVER["REQUEST_URI"] . "' /n";
if(!fwrite($fp, $Recond))
throw new FileNotWriteException("不能将用户" . $this->userName . "的记录写入记录文件" .
$this->logFile . "
中.
/n");

fclose($fp);
}
}

/**
* 从路径取得Module名称
*
* 设置$this->module
* @return String
**/
private function getModule()
{
return $this->module = ("" != $_REQUEST["module"]) ? $_REQUEST["module"] : "Login";
}

/**
* 从路径取得Action名称
*
* 设置$this->action
* @return String
**/
private function getAction()
{
return $this->action = ("" != $_REQUEST["action"]) ? $_REQUEST["action"] : "show";
}

/**
* 从路径取得ControllerFile
*
* 设置$this->controllerFile
* @return String
**/
private function getControllerFile()
{
//检测module=Xxxx.Yyyy形式
if("" != stristr($this->module, "."))
{
$_controller = stristr($this->module, ".");
$_controller = substr($_controller, 1);
$_controllerPath = strtok($this->module, ".");
$_controllerPath .= "/" . strtok(".");

$this->controllerFile = "./controller/" . $_controllerPath . "/" . $_controller . "Controller.php";

unset($_controller);
unset($_controllerPath);
}else
{
$this->controllerFile = "./controller/" . $this->module . "/" . $this->module . "Controller.php";
}

//判断是否存在这个文件名
if(!file_exists($this->controllerFile))
{
throw new FileNotFoundException ("Controller file: " . $this->controllerFile . " not found!
/n");
}
require_once ($this->controllerFile);

return $this->controllerFile;
}

/**
* 从路径取得controllerClassName
*
* 设置$this->controllerFile
**/
private function getcontrollerClass()
{
$this->controllerClassNameName = $this->getModule() . "Controller";

$this->getControllerFile();

if(!class_exists($this->controllerClassName))
{
throw new ClassNotFoundException ("In conctroller file: " . $this->controllerFile . " not found class
" . $this->controllerClassName . "");
}

}

/**
* 解析路径
*
* 初始化类成员变量
**/
private function parsePath()
{
$this->getAction();
$this->getModule();
$this->getControllerFile();
$this->getcontrollerClass();
}

/**
* 执行函数
*
* 此类唯一对外的一个接口
**/
public function run()
{
$this->parsePath();
$this->checkSecurity($this->module, $this->action);
$controller = new $this->controllerClassName();
$controller->{$this->action}();
$this->writeLog($this->module, $this->action);
}
}
?>

先从application类的构造函数讲起.
构造函数首先由session中进行初始用户名与用户权限,然后检测是否要进行日志记录及日志文件名,其中用到的这个变量值来自
config.inc.php,用户权限的形式上面我已经讲过,它由用户登录时得到,存放于session中.

然后再来看看run()函数,这是唯一对外的一个接口,我们来分析分析它:
1. $this->parsePath()
parsePath()函数用来分析用户提交的URL,通过parsePath我们得到模块名称, 动作名称,控制类名及控制类文件名.URL形式为
?module=xxxx&action=yyyy, 从这个URL中我们要得到:
模块名称(实现上这里也就是model,将程序分为很多模块,每一个模块完成一个功能组合),文件名采用xxxxModel.php,类名采用Xxxx,
在相应的方法中进行设置. action名称,action名称实现就是controller的一个方法名,而module名又同时是controller名,文件名采用
xxxxController.php,类名采用Xxxx,

2. $this->checkSecurity($this->module, $this->action):
检测当前用户是否具有权限执行某个动作的权限.当然,这个方法中我将login模块的check方法除外,原因是如果我连这个都禁止未登
录用户执行的话那么这个用户就不能进行用户账号检测了.

3. $controller = new $this->controllerClass();
实例化一个controller,注意,这里的controllerClassName指的是一个变量名,在parsePath时就已经得到了,PHP允许在new 一个类时
类不加(),但习惯上我会加上.

4. $controller->{$this->action}();
调用contrller的方法{$this->action},一个小技巧,如果$this->action= show, 那么这时实际内容为:
$controller->show();

5. $this->writeLog($this->module, $this->action);
调用日志写入方法,整个程序至此完成.

来看看相关的文件:
====================================
config.inc.php
====================================
* FileName: config.inc.php
* Introduce: 网站全局配置文件
*
* @author: 大师兄
* @Email:
teacherli@163.com
* @version $Id$
* @copyright 2004-10-26
**/
//Database's setting
define ("DB_TYPE", "mysql");
define ("DB_HOST", "localhost");
define ("DB_USER", "root");
define ("DB_PASSWD", "");
define ("DB_NAME", "Eps");

//log's setting
define ("IS_ACTIONLOG", "true");
define ("IS_LOGINLOG", "true");
define ("ACTIONLOG_FILE_NAME", "./log/actionLog.txt");
define ("LOGINLOG_FILE_NAME", "./log/loginLog.txt");
?>

===========================================
security.class.php
===========================================
/**
* FileName: security.class.php
* Introduce: 系统级安全接口
*
* @author: 大师兄
* @Email:
teacherli@163.com
* @version $Id$
* @copyright 2004-10-26
**/
interface Security
{
private function checkPermission($module, $action); //检测权限
private function checkUserLogin(); //检测是否登录
private function checkSecurity($module, $action); //调用checkPermission
}

interface Log
{
private function writeLog($module, $action);
}
?>
===========================================
exception.class.php
===========================================
/**
* FileName: exception.class.php
* Introduce: 程序用到的例外名称
*
* @author: 大师兄
* @email:
teacherli@163.com
* @version $Id$
* @copyright 2004-10-26
**/

class ActionNotAllowException extends Exception{ }//动作不匹配
class ModuleNotAllowException extends Exception{ }//模块不合法

class UserNotLoginException extends Exception{ }//用户没有登录
class PermissionNotAllowException extends Exception{ }//权限不匹配

//文件操作例外
class FileNotFoundException extends Exception{ }
class FileNotWriteException extends Exception{ }
class FileNotReadException extends Exception{ }

class ClassNotFoundException extends Exception{ }//类没有找到

class DBLinkErrorException extends Exception{ }//数据库连接例外
class DBExecuteErrorException extends Exception{ }//数据库查询与执行例外
?>
======================================================

再来看看网站的入口文件是怎么调用application.class.php的:

==================================================
work
==================================================
/**
* 文件名:任意
* 基本功能:实例化一个Application
*
* @Author: 李晓军
* Email:
teacherli@163.com
* @copyright 2004 10 26
**/
session_start();
include ("./comm/application.class.php");

$app = new Application();
try
{
$app->run();
}catch(UserNotLoginException $e)
{
echo $e->getMessage();
}
catch(Exception $e)
{
echo $e->getMessage();
}
?>

work取用了隐藏文件名的小把戏,来看看配置文件:
=================================================
.htaccess
=================================================

forcetype application/x-httpd-php

DirectoryIndex work

强制解析不带扩展名的work为PHP类型,并有把它设置为目录索引.

好了,今天这一讲就到这里,感谢大龄青年hahawen,这个frontController基本上就是他的那个MVC教程中的appliction的实现.

//from: http://alone0504.blogbus.com/logs/2005/03/1076810.html

抱歉!评论已关闭.