一,介绍单入口模式
摘要:
在这篇文章里我们将使用PHP5的标准类库构建一个简单的MVC系统
介绍
在这简文章中,我将逐步带您完成一个简单的Model-View-Controller 系统,这是开发一个大型项目所必须要了解的东西.
单一入口
关于我们的MVC系统的重要的事情之一是它将采用单入口模式。而不是每次引入很多文件.
像如下的代码:
- <?php
- include ('global.php');
- // 在这里放置实际页面的代码
- ?>
我们将有处理所有要求的一单一的页。这意味着我们不再需要每次引入global.php,我们每次想要创造一新的页。这“单一的入口”页将被称为‘index.php’;同时,这时,我们要写的代码看起来象这样:
- <?php
- // Do something
- ?>
如你所见,现在index.php什么也没有做.
为了让所有的请求页面访问index页面,我们将要设置一个.htaccess 使用mod_rewrite引擎设置重写规则,将下面的代码放置到和index.php相同目录下的.htaccess文件中.
- RewriteEngine on
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME} !-d
- RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]
如果你不能使用.htaccess或者mod重写,你不得不通过index.php手动重定向,这意味着所有你的链接必须以index.php?route=[跳转地址]”,如.index. php?route=chat /index.
现在所有的请求将使用单入口模式,我们可以写index.php了.
首先我们将要完成一些初始设置任务,创建一个新的includes目录,之后在它下面创建一个被称为startup.php的新文件
在index.php文件中写入以下代码
- // Startup tasks (define constants, etc)
- require 'includes/startup.php';
二,初始设置和实体类
初始文件被用来完成一些通常的初始任务,像定义常量,设置错误报告级别,等等
首先,我们的初始文件像这样
- <?php
- error_reporting (E_ALL);
- if (version_compare(phpversion(), '5.1.0', '<') == true) { die ('PHP5.1 Only'); }
- // Constants:
- define ('DIRSEP', DIRECTORY_SEPARATOR);
- // Get site path
- $site_path = realpath(dirname(__FILE__) . DIRSEP . '..' . DIRSEP) . DIRSEP;
- define ('site_path', $site_path);
在上面的例子中,我们定义了一些常量,设置了站点路径,也检查了当前的PHP版本至少为5.1
我们将不得不做的下一件事情是举行的设置一个实体对象来保存我们所有的全局数据.一个实体对象在我们MVC系统的所有单独的对象中间被传递.用来取代使用$BLOBALS或者global关键字
添加如下代码到startup.php文件中.
- $registry = new Registry;
如果你现在运行将会看到如下的错误
Fatal error: Class 'Registry' not found in g:"Projects"PHPit"content"simple mvc php5"demo"includes"startup.php on line 12
因为我们还没有有include()方法引用类文件.
我们可以使用php5的新特性来创建一个自动引用类文件的方法,像这样
- // For loading classes
- function __autoload($class_name) {
- $filename = strtolower($class_name) . '.php';
- $file = site_path . 'classes' . DIRSEP . $filename;
- if (file_exists($file) == false) {
- return false;
- }
- include ($file);
- }
当然,我们还没有创建实体类,所以这个一直是错误的,让我们先创建这个类
- <?php
- Class Registry {
- private $vars = array();
- }
- ?
我们再为这个类添加三个常用的方法,用来设置和取得变量值.
- function set($key, $var) {
- if (isset($this->vars[$key]) == true) {
- throw new Exception('Unable to set var `' . $key . '`. Already set.');
- }
- $this->vars[$key] = $var;
- return true;
- }
- function get($key) {
- if (isset($this->vars[$key]) == false) {
- return null;
- }
- return $this->vars[$key];
- }
- function remove($var) {
- unset($this->vars[$key]);
- }
如你所见,三个基本的方法
实现了set,get,unset设置$vars属性,在set方法中我匀可也可检查数据是否已经被赋值,如果没有我们将会抛出异常防止重复赋值。
我们有一个完整的实体类,但是我们不能仅公这样做,我们将要使用一个SPL‘s特性:
ArrayAccess.SPL,它是一个用来解决类库和接口问题的短小的PHP标准类库.
一个SPL’s接口,数组访问,可以被用于提供数组访问一个对象,考虑下面的代码片断:
- <?php
- $registry = new Registry;
- // Set some data
- $registry->set ('name', 'Dennis Pallett');
- // Get data, using get()
- echo $registry->get ('name');
- // Get data, using array access
- echo $registry['name']
- ?>
数组访问使$registry看样子更像一个数组,虽然使用ArrayAccess接口并没有明显的优势,但它可以减少键入,你不必再使用->get().使用ArrayAccess接口你首先要改变类的定义,像下面这样:
- Class Registry Implements ArrayAccess {
‘Implements’关键词是实现一个接口的意思,
通过继承ArrayAccess接口,类也必须增加四种新方法。
- function offsetExists($offset) {
- return isset($this->vars[$offset]);
- }
- function offsetGet($offset) {
- return $this->get($offset);
- }
- function offsetSet($offset, $value) {
- $this->set($offset, $value);
- }
- function offsetUnset($offset) {
- unset($this->vars[$offset]);
- }
这些方法的更多详细内容,请查找SPL文档.
现在我们已经实现了ArrayAccess,我们可以像数组一样使用这个对象了,如你所见,先前的例子改成这样:
- <?php
- $registry = new Registry;
- // Set some data
- $registry->['name'] = 'Dennis Pallett';
- // Get data, using get()
- echo $registry->get ('name');
- // Get data, using array access
- echo $registry['name']
- ?>
我们的实体类已经完成了,如果你想测试一下,就可以运行这个系统了(虽然它目前什么都不能显示).
我们的初始化文件已经完成,我们可以进行MVC系统下一步的设计:建立数据库的功能,也被称为模型(Model)。
三,模型和路由类
模型
MVC系统的M(Model)被用来查询数据库或其它的数据源,为控制器提供数据,我们可以实现模型装载通过一个请求
放置如下的代码在index.php中(放置在引用的初始文件下方):
- # Connect to DB
- $db = new PDO('mysql:host=localhost;dbname=demo', '[user]', '[password]');
- $registry->set ('db', $db);
在上面的例子中,我们首先创建了一个PDO类库的一个新的实例,然后连接到我的自己的MySQL数据库,我们创建了一个公共的数据库连接类($db),通过对实体类的访问.
我们系统中的模型部分已经完成,下一步是写控制器.
写控制器意思着我们不得不写一个路由类.一个路由类负责加载正确的控制器,是基于页面访问请求的(路由变量通过URL)让我们首先写下路由类.
路由类
我们的路由类分析页面请求,之后加载正确的命令,首先创建路由类的基本框架.
- <?php
- Class Router{
- private $registry;
- private $path;
- private $args = array();
- function __construct($registry){
- $this->registry=$registry;
- }
- }
- ?>
之后添加如下代码到index.php文件中
- #Load router
- $router = new Router($registry);
- $registry->set(‘router’,$router);
我们现在已经将Router类加入我们的MVC系统中,但是它还不能做任何事情,那么这个接下来为这个类添加必要的方法.
首先要做的事情是我们将添加一个setPath()方法,它将在我们的控制器中设置路径,setPath()方法像这样定义,它将被加入到Router类中.
- Function setPath($path){
- $path = trim($path,’/""’);
- $path .= DIRSEP;
- If(is_dir($path) == false){
- throw new Exception(‘Invalid controller path:`’.$path.’`’);
- $this->path = $path;
- }
- }
然后添加如下的代码到index.php文件中:
- $router->setPath(site_path.’controllers’);
现在我们设置了控制器的路径,我们可以写相应的方法来装载正确的控制器,这种方法被称为delegate(代理),并且它将分分析请求,这个方法的第一点看起来像这样:
- function delegate(){
- //analyze route
- $this->getController($file,$controller,$action,$args);
- }
如你所见,它使用了另外一个方法getController()去得到一个控制器名,和一些其它的变量,这个方法是这样实现的:
- private function getController(&file, $controller, $action, $args)
- {
- $route = (emptyempty($_GET[‘route’]))?’’:$_GET[‘route’];
- If (emptyempty($route)){
- $route = ‘index’;
- }
- //get separate parts
- $route = trim($route,’/""’);
- $parts = explode(‘/’,$route);
- //Find right controller
- $cmd_path = $this->path;
- foreach($parts as $part){
- $fullpath = $cmd_path . $part;
- if (is_dir($fullpath)){
- $cmd_path .= $part . DIRSEP;
- array_shift($parts);
- continue;
- }
- //Find the file
- if (is_file($fullpath . ‘.php’)){
- $controller = $part;
- array_shift($parts);
- break;
- }
- }
- if (emptyempty($controller)){
- $controller = ‘index’;
- };
- //Get action
- $action = array_shirt($parts);
- If (emptyempty($action)){$action = ‘index’;}
- $file = $cmd_path .$controller.’.php’;
- $args = $parts;
- }
让我们使用这种方法.它首先会得到$route的querystring变量的值,,然后到单独的部分分解它,使用explode()函数,如果这个请求是’member/view’,它将被分解到array(‘member’,’view’).
然后我们使用一个foreach循环,首先检查目录部分,如果它是,我们添加它到filepath并且进行下一步,这将允许我们放置控制器到子目录中,控制器的使用层.如果这一部分不是目录,而是一个文件,我们保存它到一个$controller变量中, 并且自从我们已找到我们想要的控制器,退出循环。
循环之后我们首先确认控制器被发现,如果这不是一个控制器,我们使用一个默认的被称为’index’. 然后我们进行么到我们需要执行的action,控制器是包含几种不同方法的一个类,而action就是其中一个方法.如果没有具体指定action,我们使用默认的action,被称为’index’.
最后,我们通过连接路径控制器名字和扩展得到控制器的完整路径.现在的请求已经被分析到这,
delegate()方法装载控制器并执行action,完整的delegate()方法像这样:
- function delegate() {
- // Analyze route
- $this->getController($file, $controller, $action, $args);
- // File available?
- if (is_readable($file) == false) {
- die ('404 Not Found');
- }
- // Include the file
- include ($file);
- // Initiate the class
- $class = 'Controller_' . $controller;
- $controller = new $class($this->registry);
- // Action available?
- if (is_callable(array($controller, $action)) == false) {
- die ('404 Not Found');
- }
- // Run action
- $controller->$action();
- }
分析GetController方法的请求之后,我们首先确认文件实际存在,否则我们返回一个简单的错误消息
接下来要做的事情我们包含控制器文件,然后初始化这个类,它部是被Controller_[name]名字调用,我们稍后会学习更多的关于控制器的内容.
那么我们使用callable()函数,来检查action是否存在并且是可执行的.最后我们运行action,它将会完成路由的功能.
现在我们完成了delegate()方法,把下面的代码添加到index.php文件中.
- $router->delegate();
如只你现在尝试运行这个系统,假如你没有创建controllers目录,将会发现如下的错误提示.
Fatal error: Uncaught exception 'Exception' with message 'Invalid controller path: `g:"Projects"PHPit"content"simple mvc php5"demo"controllers"`' in g:"Projects"PHPit"content"simple mvc php5"demo"classes"router.php:18 Stack trace: #0 g:"Projects"PHPit"content"simple mvc php5"demo"index.php(13): Router->setPath('g:"Projects"PHP...') #1 {main} thrown in g:"Projects"PHPit"content"simple mvc php5"demo"classes"router.php on line 18
或者你将会看到’404文件未找到’的错误,因为目前还没有控制器,那是我们现在要创建的.
四.控制器
MVC系统的控制器部分其实感觉简单,需要做的事情很少.首先确认控制器目录的存在,之后在’classes目录’创建一个新文件称为’controller_base.php’.在文件中写如下的代码:
- <?php
- Abstract Class Controller_Base{
- protected $registery;
- function __construct($registry){
- $this->registry = $registry;
- }
- Abstract function index();
- }
- ?>
这个抽象类将要做为所有控制器的父类,它将会做两件事情,保存一份实体类并确保我们所有的控制器都有一个index()方法.
现在,让我们创建第一个控制器,创建一个新文件命名为’index.php’ ,保存在controllers目录中.添加如下的代码:
- <?php
- Class Controller_Index Extends Controller_Base {
- function index() {
- echo 'Hello from my MVC system';
- }
- }
- ?>
这意味着我们的路由类所做的工作,并且正确的执行控制器和动作.让我们创建另一个看起来像 ‘members/view’所请请求的控制器.创建一个新的文件,命名为’members.php’,在controllers目录,添加下面的代码:
- <?php
- Class Controller_Members Extends Controller_Base{
- function index(){
- Echo ‘Detault index of the `members` controllers’;
- }
- function view(){
- echo ‘You are viewing the members/view request’;
- }
- }
- ?>
现在进入我们的MVC系统,确认请求是’member/view’,在网址栏里输入像这样的路径:index.php?route=members/view.你将得到这样的结果:
就像通过创建一个新的控制器类和增加一种方法一样,我们已经能够在一个MVC系统中定义一个全新的页面,并且我们不必在我们的系统中改变其它的东西,我们的控制器不需要包含’global.php’文件或其它东西.
现在我们已经完成了我们的MVC系统中的控制器部分,还有一件事情没有做,那就是MVC系统中的V(视图部分).
五.视图&安全措施&结论
视图
像模型一样,有几种不同的方式实现MVC系统中的视图部分,我们可以使用路由(Router)自动加载另一个文件,写法如下:’view_{name}.php’,但是为了保持这个例子简单,我们创建了一个自定义的模板类,它可以用来显示模板.
首先,在classes目录中创建一个名为’template.php’的文件,添加如下代码:
- <?php
- Class Template{
- private $registry;
- private $vars = array();
- function __construct($registry){
- $this->registry = $registry;
- }
- }
- ?>
如你所见,我们现在实现了一个基本的模板类的结构,下一步添加一些代码到index.php文件中(添加在Router结构之前)
- #Load template object
- $template = new Template($registry);
- $registry->set(‘template’,$template);
因为我们要从Model和controller中取得数据到我们模板中,我们将会使用set()方法去为模板中的变量赋值,如下所示:
- function set($varname,$value,$overwrite=false){
- if (isset($this->var[$varname]) == true AND $overwrite == false){
- trigger_error(‘Unable to set var`’.$varname.’’`’. Already set, and overrite not allowed.’,E_USER_NOTICE);
- return false;
- }
- this->vars[$varname] = $value;
- return true;
- }
- function remove($varname){
- unset($this->var[$varname]);
- return true;
- }
如你所见,set()和remove()是相当简单的方法,用来设置或删除一个变量.
现在我们能设置变量,所以我们需要写一个show方法.
最简单的方式是创建一个名为template的目录,作用include调用,显示一个模板.当然,你的show方法也可以用完全不同的写法,并且可以从数据库中取得数据.我们使用的show方法如下所示:
- function show($name){
- $path = site_path . ‘templates’ . DIRSET .$name .’.php’;
- If (file_exists($path) == false){
- trigger_error(‘Template`’.$name.’`does not exist.’E_USER_NOTICE);;
- return false;
- }
- //Load variables
- foreach ($this->vars as $key=>values){
- $$key = $value;
- }
- include($path);
- }
我们的模板类现在已经完成,可以在控制器的作用下显示模板,例如,创建一个名为’index.php’的文件在’templates’目录下,写下如下的代码:
- Hello from the View,<?php echo $first_name;?>!
然首在首页的控制器中(位置是controllers目录下的index.php文件):
- function index(){
- $this->registry[‘template’]->set(‘first_name’,’Dennis’);
- $this->registry[‘template’]->show(‘index’);
- }
如果你现在访问我们的MVC系统,你将会看到如下的页面效果:
现在我们已经完成了一个活动的视图组件,我们的MVC系统已经完成,并且能被用来创建一个很强大的网站,但是,还有一些事情需要仔细考虑。
安全措施
此刻所有的子目录,比如‘controllers’和’templates’对于访问它的任何人都是公开的可以访问的,意思是用户的控制器或者模板应该仅仅由我们的系统管理来管理,因此,我们应该阻止用户对那些目录的访问.
使用.htaccess文件真的很简单,仅仅需要一行命令.
- Deny from All
将上面的命令添加到一个新文件中,保存为.htaccess,将这个文件复制到’controllers’和’templates’目录中(也可以放置到你需要保护的任何目录下)这将会阻止每个用户对此目录的直接访问.
结论
在这篇教程中,我告诉你如何使用PHP5.1来创建一个简单的MVC系统,显然,还有许多创建MVC系统的方式,这篇教程或许不是最好的实现方式,但它确实显示出MVC系统的强大之处。