MVC是一种软件设计模式。顾名思义,这些字母分别代表着模型(Model)、视图(View)和控制器(Controller)。PHP中的MVC旨在分离业务与逻辑层,使得开发过程中的结构清晰,大大提高了效率。当下有很多流行的PHP框架均使用了MVC模式。如果是新手入门,查看这些框架的源代码肯定会一头雾水,毫无思路。上手新框架等于重新开始学习,这必然会消耗不必要的精力和时间。而学习MVC框架最好的方式就是自己动手写一个框架,既学习了设计模式,又巩固了编程能力,何乐而不为。更重要的是,在开发过程中能够将自己的想法融入到框架中去,从而实现各种功能。
准备工作
这篇文章是我Google学习很多文章后的总结。将各路大神的代码汇总,然后择取出MVC必要的元素,根据个人需要编写出的小型框架,而非使用Composer组装出的框架。这个框架包含以下特点:
根据这个思路,开始coding.
MVC流程
日常上网过程中可以简化理解为人与服务器中数据库交互的过程。
每一次网页上的请求发送到服务器,服务端将请求通过控制器处理后,再传送到模型。
模型内包含业务逻辑,负责处理如何与数据库交换数据,数据处理完成后,发送到视图。
视图将传来的数据渲染后,最终返还用户。这样便完成了一次请求。
熟悉MVC流程后,就有了框架的宏观印象,方便理清结构。
编写框架
建立目录
首先,在项目目录下建立如下文件和文件夹:
1 2 3 4 5 6 7 8
| |--application | |--controllers | |--models | |--views |--config |--framework |--.htaccess |--index.php
|
application 内放置用户编写的MVC文件;
config 内放置数据库设置文件;
framework 内放置框架的核心文件;
.htaccess 为重定向文件;
index.php 为入口文件。
重定向
.htaccess是服务器的配置文件,它用来将所有的请求转到入口文件处理。这样一来,网页程序便有了单一入口,掌控用户请求,避免熊孩子的无理取闹(笑。还可以生成对搜索引擎友好的URL。配置内容如下:
Apache服务器:
1 2 3 4
| RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.+)$ index.php/$1 [L]
|
nginx服务器:
1 2 3 4 5
| location / { if(!-e $request_filename){ rewrite ^(.+)$ /index.php/$1 break; } }
|
入口文件
打开同目录下的index.php文件,输入以下代码:
1 2
| <?php require './framework/framework.php';
|
内容很简单,就是包含核心文件夹内的入口文件。其实在这里可以定义一些预定义常量方便调用,例如:
1 2 3
| define('APP_PATH', __DIR__.'/'); define('APP_DEBUG', true); define('APP_URL', 'http://localhost');
|
核心文件
外入口文件(index.php)将请求传递给核心文件的内入口文件(framework.php)后,接下来的工作由核心文件完成。framework.php的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php defined('FRAME_PATH') or define('FRAME_PATH', __DIR__.'/'); defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/'); defined('APP_DEBUG') or define('APP_DEBUG', false); defined('CONFIG_PATH') or define('CONFIG_PATH', APP_PATH.'config/'); defined('RUNTIME_PATH') or define('RUNTIME_PATH', APP_PATH.'runtime/');
require APP_PATH . 'config/config.php';
require FRAME_PATH . 'Core.php';
$framework = new Core; $framework->run();
|
同样,在这里进行预定义常量和包含文件的工作,并实例化核心文件,让网页程序得以运行。题外话:看了一些外国人写的教程,大部分都喜欢用$framework->bootstrap(), 而这个bootstrap()并非代表着前端框架Bootstrap(原谅我脑洞大,第一个想到的就是这个),而是一种命名习惯吧。
从上述代码可以看出,包含了两个文件,其中config.php是数据库配置文件,要把它存放在config文件夹内,其内容如下:
1 2 3 4 5
| <?php define('DB_NAME', 'test_db'); define('DB_USER', 'root'); define('DB_PASSWORD', '1234'); define('DB_HOST', 'localhost');
|
根据个人情况修改即可。
回到framework文件夹,建立Core.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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| <?php class Core { public function run() { spl_autoload_register(array($this, 'loadClass')); $this->setReporting(); $this->removeMagicQuotes(); $this->unregisterGlobals(); $this->route(); } public function route() { $controllerName = 'Index'; $action = 'index'; $param = array(); $url = isset($_GET['url']) ? $_GET['url'] : false; if ($url) { $urlArray = explode('/', $url); $urlArray = array_filter($urlArray); $controllerName = ucfirst($urlArray[0]); array_shift($urlArray); $action = $urlArray ? $urlArray[0] : 'index'; array_shift($urlArray); $param = $urlArray ? $urlArray : array(); } $controller = $controllerName . 'Controller'; $dispatch = new $controller($controllerName, $action); if ((int)method_exists($controller, $action)) { call_user_func_array(array($dispatch, $action), $param); } else { exit($controller . "控制器不存在"); } } public function setReporting() { if (APP_DEBUG === 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', RUNTIME_PATH. 'logs/error.log'); } } public function stripSlashesDeep($value) { $value = is_array($value) ? array_map(array($this, 'stripSlashesDeep'), $value) : stripslashes($value); return $value; } public function removeMagicQuotes() { if (get_magic_quotes_gpc()) { $_GET = isset($_GET) ? $this->stripSlashesDeep($_GET ) : ''; $_POST = isset($_POST) ? $this->stripSlashesDeep($_POST ) : ''; $_COOKIE = isset($_COOKIE) ? $this->stripSlashesDeep($_COOKIE) : ''; $_SESSION = isset($_SESSION) ? $this->stripSlashesDeep($_SESSION) : ''; } } public 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]); } } } } } public static function loadClass($class) { $frameworks = FRAME_PATH . $class . '.class.php'; $controllers = APP_PATH . 'application/controllers/' . $class . '.class.php'; $models = APP_PATH . 'application/models/' . $class . '.class.php'; if (file_exists($frameworks)) { include $frameworks; } elseif (file_exists($controllers)) { include $controllers; } elseif (file_exists($models)) { include $models; } else { echo "控制器类或模型类不存在!" } } }
|
从run()中可以理解这一核心文件的运行机制,首先spl_autoload_register()用于自动加载类文件,在以后的类实例化中,可以方便的直接调用,而不是一个一个的require;接下来setReporting()用来开启调试模式,并记录到日志中;removeMagicQuotes()用于移除敏感字符;unregisterGlobals()用于检测并移除系统全局变量,避免如_GET不到变量的问题。准备工作完成后,route()方法截取URL,将形如:
localhost/?url=controller/action/parameters
的URL分离为controller,action和parameters,其中controller即application文件夹下用户自定义的控制器类文件,action为相应类文件内的方法,parameters为传入方法内的变量。实例化控制器后,并调用view方法。
这里的URL有点古怪,因为使用是GET方法获取的。如果想去掉?url也未尝不可,使用$_SERVER['PATH_INFO']可获取index.php后的内容,以/分离可获取控制器和方法;而?后的内容可以使用$_SERVER['QUERY_STRING']获取。PATHINFO的URL模式对搜索引擎更加友好。
MVC基类
接下来,在核心文件夹内创建Controller.class.php,Model.class.php和View.class.php三个基类文件,用户创建的子类文件需要继承基类文件,所以可以把一些常用的方法写入到这个文件内,日后使用时直接调用即可。同时,因为这些基类文件负责着整体的MVC流程,所以其中要包含一些总体调度方法。
Controller.class.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
| <?php class Controller { protected $_controller; protected $_action; protected $_view; public function __construct($controller, $action) { $this->_controller = $controller; $this->_action = $action; $this->_view = new View($controller, $action); } public function assign($name, $value) { $this->_view->assign($name, $value); } public function render() { $this->_view->render(); } }
|
从代码内容可知,控制器基类用于分配变量和传递变量给视图,所以在控制器子类中,可以直接使用$this->render() 进行渲染。
Model.class.php内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class Model extends Sql { protected $_model; protected $_table; public function __construct() { $this->connect(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME); $this->_model = get_class($this); $this->_model = substr($this->_model, 0, -5); $this->_table = strtolower($this->_model); } }
|
此类继承自数据库基类Sql.class.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
| <?php class Sql { protected $_dbHandle; protected $_result; private $filter = ''; public function connect($host, $user, $pass, $dbname) { try { $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8", $host, $dbname); $option = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC); $this->_dbHandle = new PDO($dsn, $user, $pass, $option); } catch (PDOException $e) { exit('错误: ' . $e->getMessage()); } } public function select($id) { $sql = sprintf("select * from `%s` where `id` = '%s'", $this->_table, $id); $sth = $this->_dbHandle->prepare($sql); $sth->execute(); return $sth->fetch(); } }
|
这里只封装了普通查询的方法,可以以这个方法为模板编写其他的CURD方法。再以后的使用过程中,就可以直接使用封装后的方法操作数据库
View.class.php内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class View { protected $variables = array(); protected $_controller; protected $_action;
function __construct($controller, $action) { $this->_controller = $controller; $this->_action = $action; } public function assign($name, $value) { $this->variables[$name] = $value; } public function render() { extract($this->variables); } }
|
这样,框架的核心部分就完成了。测试框架时,像使用其他框架一样,在application文件夹内的子文件夹分别建立用户类文件,并继承基类文件,写入测试方法和语句即可。
总结
当我翻阅了很多编写入门级MVC框架教程之后,发现很多代码内容都是一样的。比如截取URL和自动加载类的方法。查看这些源码很容易理解其原理和过程。由于目前能力有限,我还不能倒背如流般的coding出这些代码,只得借鉴大神的代码,让自己学习和巩固。在开发过程中,也可以使用其他人造好的轮子,利用Composer下载到项目文件夹内,然后根据命名空间规则引用,就可以使用了。这样便极大的加快了项目的开发速度。但是我不喜欢黑箱操作,单纯的调用API反而更容易让人摸不着北。创造,才是一件令人开心的事。