简介
这篇文章主要为大家详细介绍了PHP如何实现开发MVC框架,具体过程是什么的内容,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望对大家学习或工作能有帮助,接下来就跟随小编一起来学习吧。
1、什么是MVC
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式。MVC把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。 PHP中MVC模式也称WEBMVC,从上世纪70年代进化而来。 MVC的目的是实现一种动态的程序设计,便于后续对于程序的修改和扩展,并且使程序某一部分的重复利用成为可能。 除此之外,此模式通过对复杂度的简化,使程序结构更加直观。 MVC各部分的职能:
- 模型Model – 管理大部分的业务逻辑和所有的数据库逻辑。模型提供了连接和操作数据库的抽象层。
- 控制器Controller – 负责响应用户请求、准备数据,以及决定如何展示数据。
- 视图View – 负责渲染数据,通过HTML方式呈现给用户。
一个典型的Web MVC流程:
- Controller截获用户发出的请求;
- Controller调用Model完成状态的读写操作;
- Controller把数据传递给View;
- View渲染最终结果并呈献给用户。
2、为什么要自己开发MVC框架
网络上有大量优秀的MVC框架可供使用,本教程并不是为了开发一个全面的、终极的MVC框架解决方案。 我们将它看作是一个很好的从内部学习PHP的机会。 在此过程中,你将学习面向对象编程和MVC设计模式,并学习到开发中的一些注意事项。 更重要的是,通过自制MVC框架,每个人都可以完全控制自己的框架,将你的想法融入到你的框架中。 这不是很美妙的事情吗~~~
3、准备工作
3.1、环境准备
使用最基本的php环境即可,本程序已适配PHP8.0+
Nginx或Apache
PHP7.4+
MySQL
推荐使用phpStudy或dockr一键安装这样的LNMP环境。
3.2、代码规范
- MySQL的表名需小写或小写加下划线,如:item,car_orders。
- 模块名(Models)需用大驼峰命名法,即首字母大写,如:ItemTxt,CarOrder。
- 控制器(Controllers)需用大驼峰命名法,即首字母大写,如:Item,Car。
- 方法名(Action)需用小驼峰命名法,即首字母小写,如:index,indexPost。
- 视图(Views)部署结构为控制器名/行为名,如:item/index.html,car/buy.html。
上述规则是为了程序能更好地相互调用。 接下来就开始真正的PHP MVC编程了。
3.3、目录准备
在开始开发前,我们给这个框架先起个名字吧,就叫:ViunsPHP框架。 然后根据需要来把项目的目录创建。 假设我们建立的项目为 Viuns,目录结构就这样:
Viuns WEB部署根目录
├─app 模块目录
│ ├─index Index模块
│ ├─controller 控制器目录
│ ├─model 模型目录
│ └─view 视图目录
├─config 配置文件目录
│ ├─app.php 应用配置
│ ├─cookie.php Cookie配置
│ ├─database.php 数据库配置
│ ├─session.php Session配置
│ └─view.php 视图配置
├─public WEB目录(对外访问目录)
│ ├─static 静态文件目录
│ ├─index.php 入口文件
│ └─.htaccess 用于apache的重写
├─vendor 框架核心目录
│ ├─base MVC基类目录
│ ├─common 公共函数目录
│ ├─db 数据库操作类目录
│ └─App.php 内核文件
3.4、重定向
重定向的目的就是有两个:设置根目录为Viuns所在位置,以及将所有请求都发送给 index.php 文件。 如果是Apache服务,在 Viuns目录下新建一个 .htaccess 文件,内容为:
<IfModule mod_rewrite.c>
# 打开Rerite功能
RewriteEngine On
# 如果请求的是真实存在的文件或目录,直接访问
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# 如果访问的文件或目录不是真事存在,分发请求至 index.php
RewriteRule . index.php
</IfModule>
如果是Nginx服务,则填写下面内容
location ~*(runtime|application)/{
return 403;
}
location /{
if(!-e $request_filename){
rewrite A(.*)$ /index.php?s=$1 last; break;
}
}
这样做的主要原因是:
(1)静态文件能直接访问。如果文件或者目录真实存在,则直接访问存在的文件/目录。 比如,静态文件static/css/main.css真实存在,就可以直接访问它。
(2)程序有单一的入口。 这种情况是请求地址不是真实存在的文件或目录,这样请求就会传到index.php 上。 例如,访问地址:localhost/item/detail/1,在文件系统中并不存在这样的文件或目录。 那么,Apache或Nginx服务器会把请求发给index.php,并且把域名之后的字符串赋值给REQUEST_URI变量。 这样在PHP中用$_SERVER[‘REQUEST_URI’]就能拿到/item/detail/1;
(3)可以用来生成美化的URL,利于SEO。
4、PHP MVC核心文件
4.1、入口文件
首先,在根目录/public/创建index.php入口文件:
<?php
// [ 应用入口文件 ]
namespace inc;
define("APP_PATH", __DIR__ . '/../');
require __DIR__ . '/../vendor/App.php';
// 执行APP应用并响应
$app = (new App());
$app->run();
注意,上面的PHP代码中,并没有添加PHP结束符号?>。
这么做的主要原因是:对于只有 PHP 代码的文件,最好没有结束标志?>,PHP自身并不需要结束符号,不加结束符让程序更加安全,很大程度防止了末尾被注入额外的内容
4.2、配置文件
在入口文件中,我们加载了config目录的配置文件,那它有何作用呢? 从名称不难看出,它的作用是保存一些常用配置。 config目录的内容如下:
[app.php] 应用全局配置
<?php
/**
* 应用程序配置信息
*
* 该配置文件包含了应用程序的相关配置,默认控制器/操作名等。
*
* 注意:为了安全起见,密码不应该明文存储在配置文件中。应该使用其他安全的方式(如加密或哈希)来存储密码。
*/
return [
// 开启多应用
'multi_app' => true,
// 应用目录名称
'appPath_name' => 'app',
// 设置默认应用名称,多应用为true才生效
'default_app' => 'index',
// 默认控制器
'default_controller' => 'index',
// 默认操作,这里是index操作
'default_action' => 'index',
// 开启调试
'app_debug' => true,
// 错误显示信息,非调试模式有效
'error_message' => '页面错误!请稍后再试~',
// 显示错误信息
'show_error_msg' => true,
];
[database.php] 数据库链接配置
<?php
/**
* 数据库配置信息
*
* 该配置文件包含数据库连接信息。
*
*/
return [
/**
* 数据库连接配置
*/
'db' => [
// 数据库主机名
'host' => '123.99.201.88',
// 数据库用户名
'username' => 'con_viuns_com',
// 数据库密码
'password' => 'txMjrdhDGEcZradL',
// 数据库名称
'dbname' => 'con_viuns_com',
// 数据库端口号,默认为3306
'port' => 3306,
'prefix' => 'viu_'
],
];
[view.php] 视图配置
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Viuns
'type' => 'Viuns',
// 应用名称
'app_name' => 'app',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 设置模板所在的文件夹
'template_dir' => '',
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => '.html',
// 是否需要编译成静态的HTML文件
'cache_html' => false,
// 设置编译文件的后缀
'suffix_cache' => '.php',
// 多长时间自动更新,单位秒
'cache_time' => 7200,
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
// 设置编译后存放的目录
'compile_dir' => 'cache/',
// 是否支持原生PHP代码
'php_turn' => true,
// 是否开启模板调试
'template_debug' => false,
'cache_control' => 'control.dat',
];
[session.php] Session配置
<?php
// +----------------------------------------------------------------------
// | 会话设置
// +----------------------------------------------------------------------
return [
// session name
'name' => 'PHPSESSID',
// SESSION_ID的提交变量,解决flash上传跨域
'var_session_id' => '',
// 驱动方式 支持file cache
'type' => 'file',
// 存储连接标识 当type使用cache的时候有效
'store' => null,
// 过期时间
'expire' => 0,
// 前缀
'prefix' => '',
];
[cookie.php] Cookie配置
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
// samesite 设置,支持 'strict' 'lax'
'samesite' => '',
];
4.3、框架核心类
入口文件对框架类做了两步操作:实例化,调用run方法。run()方法则调用用类自身方法,完成下面几个操作:
- 加载公共函数
- 加载全局常量
- 类自动加载
- 加载自定义异常接管
- 注册异常接管
- 注册异常处理
- 过滤敏感字符
- 移除全局变量
- 路由处理
在vendor目录下新建核心类文件,名称App.php,代码:
<?php
namespace inc;
// 框架根目录
//defined('CORE_PATH') or define('CORE_PATH', __DIR__);
/**
* App框架核心
*/
class App
{
// 配置内容
protected $config = [];
protected $view = [];
protected $data = [];
// 数据库配置信息
protected $databasic = [];
public function __set($name, $value)
{
$this->data[$name] = $value;
}
public function __construct()
{
}
// 运行程序
public function run()
{
// 1、加载公共函数库
require 'common.php';
// 2、加载常量
$this->setConst();
spl_autoload_register(array($this, 'loadClass'));
// 3、加载自定义异常接管
require 'base/Exception.php';
// 注册异常接管
set_error_handler('\inc\base\Exception::deal');
// 注册异常处理
set_exception_handler('\inc\base\Exception::exception');
//$this->setReporting();
$this->removeMagicQuotes();
$this->unregisterGlobals();
$this->setDbConfig();
$this->route();
}
// 定义全局
public function globalConfiguration()
{
}
public static function setConst()
{
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
$domain = $_SERVER['HTTP_HOST'];
define('_DOMAIN', $protocol . $domain . '/');//域名常量
// 系统分割符 \/
defined('SPLI') or define('SPLI', DIRECTORY_SEPARATOR);
// 框架核心代码库目录
defined('CORE_PATH') or define('CORE_PATH', __DIR__);
// 项目根目录
defined('ROOT_PATH') or define('ROOT_PATH', dirname(__DIR__));
// 配置文件目录
$default_config = ROOT_PATH . SPLI . 'config' . SPLI . 'app.php';
define('CONFIG', require $default_config);
// view配置文件
$view_config = ROOT_PATH . SPLI . 'config' . SPLI . 'view.php';
define('VIEW', require $view_config);
// 数据库配置文件
$database_config = ROOT_PATH . SPLI . 'config' . SPLI . 'database.php';
define('DATABASE', require $database_config);
// 配置Session文件
$default_session = ROOT_PATH . SPLI . 'config' . SPLI . 'session.php';
define('SESSION', require $default_session);
// 配置Cookie文件
$default_cookie = ROOT_PATH . SPLI . 'config' . SPLI . 'cookie.php';
define('COOKIE', require $default_cookie);
// 应用根目录
$app_name = empty(VIEW['view']['app_name']) ? 'app' : VIEW['view']['app_name'];
defined('APP_PATH') or define('APP_PATH', __DIR__ . SPLI . $app_name);
// 调试开关
defined('APP_DEBUG') or define('APP_DEBUG', CONFIG['app_debug']);
}
// 路由处理
public function route()
{
// 多应用开关
$isMultiApp = CONFIG['multi_app'];
// 默认应用目录名称
$defaultApp = empty(CONFIG['appPath_name']) ? 'app' : CONFIG['appPath_name'];
// 默认应用模块名称
$appName = empty(CONFIG['default_app']) ? 'index' : CONFIG['default_app'];
// 模式控制器名称
$controllerName = empty(CONFIG['default_controller']) ? ucfirst('index') : ucfirst(CONFIG['default_app']);
// 模式方法名称
$actionName = empty(CONFIG['default_action']) ? 'index' : CONFIG['default_action'];
$param = array();
$url = $_SERVER['REQUEST_URI'];
// 清除?之后的内容
$position = strpos($url, '?');
$url = $position === false ? $url : substr($url, 0, $position);
// 删除前后的“/”
$url = trim($url, '/');
if ($url) {
// 使用“/”分割字符串,并保存在数组中
$urlArray = explode('/', $url);
// 删除空的数组元素
$urlArray = array_filter($urlArray);
// 判断是否开启多应用
if ($isMultiApp) {
// 获取应用名
$appName = $urlArray[0];
// 获取控制器名
$controller = isset($urlArray[1]) ? $urlArray[1] : $controllerName;
// 获取方法名
$method = isset($urlArray[2]) ? $urlArray[2] : $actionName;
// 切换到对应应用的目录
$controllerPath = $defaultApp . '\\' . $appName . '\\' . 'controller' . '\\' . ucfirst($controller);
// 删除应用名
array_shift($urlArray);
// 检测应用是否存在
$appPath = ROOT_PATH . SPLI . $defaultApp . SPLI . $appName;
if (!is_dir($appPath)) {
throw new \Exception('应用不存在: ' . $appName);
}
// 获取动作名
array_shift($urlArray);
$actionName = isset($urlArray[0]) ? $urlArray[0] : $method;
} else {
// 单应用传递空应用名称
$appName = null;
// 获取控制器名
$controller = isset($urlArray[0]) ? $urlArray[0] : $controllerName;
// 获取方法名
$method = isset($urlArray[1]) ? $urlArray[1] : $actionName;
// 获取控制器url
$controllerPath = $defaultApp . '\\' . 'controller' . '\\' . ucfirst($controller);
// 获取动作名
array_shift($urlArray);
$actionName = isset($urlArray[0]) ? $urlArray[0] : $method;
}
// 获取URL参数
array_shift($urlArray);
$param = $urlArray ? $urlArray : array();
} else {
// 使用默认应用名和控制器
if ($isMultiApp) {
$controller = $controllerName;
$controllerPath = $defaultApp . '\\' . $appName . '\\' . 'controller' . '\\' . ucfirst($controllerName);
} else {
$controller = $controllerName;
// 单应用传递空应用名称
$appName = null;
$controllerPath = $defaultApp . '\\' . 'controller' . '\\' . ucfirst($controllerName);
}
}
// 判断控制器和操作是否存在
if (!class_exists($controllerPath)) {
throw new \Exception('控制器不存在: ' . $controllerPath);
}
if (!method_exists($controllerPath, $actionName)) {
throw new \Exception('方法不存在: ' . $actionName);
}
// 如果控制器和操作名存在,则实例化控制器,因为控制器对象里面
// 还会用到控制器名和操作名,所以实例化的时候把他们俩的名称也
// 传进去。结合Controller基类一起看
$dispatch = new $controllerPath($controller, $actionName, $appName);
// $dispatch保存控制器实例化后的对象,我们就可以调用它的方法,
// 也可以像方法中传入参数,以下等同于:$dispatch->$actionName($param)
call_user_func_array(array($dispatch, $actionName), $param);
}
// 检测开发环境
public function setReporting()
{
}
// 删除敏感字符
public function stripSlashesDeep($value)
{
$value = is_array($value) ? array_map([$this, 'stripSlashesDeep'], $value) : stripslashes($value);
return $value;
}
// 检测敏感字符并删除
public function removeMagicQuotes()
{
if (getenv('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') {
// 如果是 AJAX 请求,不处理敏感字符
return;
}
// 删除 GET 参数中的敏感字符
if (isset($_GET)) {
$_GET = $this->stripSlashesDeep($_GET);
}
// 删除 POST 参数中的敏感字符
if (isset($_POST)) {
$_POST = $this->stripSlashesDeep($_POST);
}
// 删除 COOKIE 参数中的敏感字符
if (isset($_COOKIE)) {
$_COOKIE = $this->stripSlashesDeep($_COOKIE);
}
// 删除 SESSION 参数中的敏感字符
if (isset($_SESSION)) {
$_SESSION = $this->stripSlashesDeep($_SESSION);
}
}
// 检测自定义全局变量并移除。因为 register_globals 已经弃用,如果
// 已经弃用的 register_globals 指令被设置为 on,那么局部变量也将
// 在脚本的全局作用域中可用。 例如, $_POST['foo'] 也将以 $foo 的
// 形式存在,这样写是不好的实现,会影响代码中的其他变量。 相关信息,
// 参考: http://php.net/manual/zh/faq.using.php#faq.register-globals
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 setDbConfig()
{
if (DATABASE['db']) {
define('DB_HOST', DATABASE['db']['host']);
define('DB_NAME', DATABASE['db']['dbname']);
define('DB_USER', DATABASE['db']['username']);
define('DB_PASS', DATABASE['db']['password']);
define('DB_PORT', DATABASE['db']['port']);
define('DB_PREFIX', DATABASE['db']['prefix']);
}
}
// 自动加载类
public function loadClass($className)
{
$classMap = $this->classMap();
if (isset($classMap[$className])) {
// 包含内核文件
$file = $classMap[$className];
} elseif (strpos($className, '\\') !== false) {
// 包含应用(application目录)文件
$file = APP_PATH . str_replace('\\', '/', $className) . '.php';
if (!is_file($file)) {
return;
}
} else {
return;
}
include $file;
// 这里可以加入判断,如果名为$className的类、接口或者性状不存在,则在调试模式下抛出错误
}
// 内核文件命名空间映射关系
protected function classMap()
{
$compile = empty(VIEW['type']) ? 'Viuns' : VIEW['type'];
return [
'inc\base\Exception' => CORE_PATH . '/base/Exception.php',
'inc\base\Controller' => CORE_PATH . '/base/Controller.php',
'inc\base\Model' => CORE_PATH . '/base/Model.php',
'inc\base\View' => CORE_PATH . '/base/View.php',
'inc\base\Session' => CORE_PATH . '/base/Session.php',
'inc\base\Cookie' => CORE_PATH . '/base/Cookie.php',
'inc\base\\' . $compile => CORE_PATH . '/base/templates/'. $compile .'.php',
'inc\db\Query' => CORE_PATH . '/db/Query.php',
'inc\db\Db' => CORE_PATH . '/db/Db.php',
];
}
}
暂无评论内容