手写PHP(MVC)框架_ViunsPHP核心篇①

手写PHP(MVC)框架_ViunsPHP核心篇①

简介

这篇文章主要为大家详细介绍了PHP如何实现开发MVC框架,具体过程是什么的内容,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望对大家学习或工作能有帮助,接下来就跟随小编一起来学习吧。

1、什么是MVC

MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式。MVC把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。 PHP中MVC模式也称WEBMVC,从上世纪70年代进化而来。 MVC的目的是实现一种动态的程序设计,便于后续对于程序的修改和扩展,并且使程序某一部分的重复利用成为可能。 除此之外,此模式通过对复杂度的简化,使程序结构更加直观。 MVC各部分的职能:

  • 模型Model – 管理大部分的业务逻辑和所有的数据库逻辑。模型提供了连接和操作数据库的抽象层。
  • 控制器Controller – 负责响应用户请求、准备数据,以及决定如何展示数据。
  • 视图View – 负责渲染数据,通过HTML方式呈现给用户。
图片[1]-手写PHP(MVC)框架_ViunsPHP核心篇①-耀雪资源网

一个典型的Web MVC流程:

  1. Controller截获用户发出的请求;
  2. Controller调用Model完成状态的读写操作;
  3. Controller把数据传递给View;
  4. 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,在文件系统中并不存在这样的文件或目录。 那么,ApacheNginx服务器会把请求发给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()方法则调用用类自身方法,完成下面几个操作:

  1. 加载公共函数
  2. 加载全局常量
  3. 类自动加载
  4. 加载自定义异常接管
  5. 注册异常接管
  6. 注册异常处理
  7. 过滤敏感字符
  8. 移除全局变量
  9. 路由处理

在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',
           ];
    }
}
© 版权声明
THE END
喜欢就支持一下吧
点赞12赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容