如何开发一个简易的MVC框架?

前言

MVC 模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 的目的是实现一种动态的程序设计,便于后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部份分离的同时,也赋予了各个基本部分应有的功能。MVC 架构对于 PHP 开发者来说应该都不陌生,我们在日常的项目开发中所使用到的框架,比如:ThinkPHP3.2 , Laravel , TP5 等,这些框架都是用的 MVC 三层模式。

MVC 各部分的职能:

1、模型Model – 管理大部分的业务逻辑和所有的数据库逻辑。模型提供了连接和操作数据库的抽象层。

2、控制器Controller - 负责响应用户请求、准备数据,以及决定如何展示数据。

3、视图View – 负责渲染数据,通过HTML方式呈现给用户。

解析:Web MVC流程:

1、Controller获取用户发出的请求;

2、Controller调用Model完成状态的读写操作;

3、Controller把数据传递给View;

4、View渲染最终结果并呈献给用户。

目录准备

开发一款自己的 MVC 框架对开发者的基础知识的掌握要求是非常高的,比如:PHP 内置的各种函数的使用,常量的定义,文件的处理,面向对象的基础等。在开始开发前,让我们先来把项目建立好,假设我们建立的项目为 whphpCMS,那么接下来的第一步就是把目录结构先设置好。

下面就具体说说每个目录的作用:

1、app – 应用目录,即我们主要写代码的位置

2、admin - 后台模块,里面对应的是其MVC架构

3、home - 前台模块,里面对应的是其MVC架构

4、common/kernel - 框架核心文件,比如:模块路径、控制器路径,自动加载机制等。

5、common/controller - 公共控制器(基类)

6、common/model - 公共模型(基类)

7、config - 公共配置文件、数据库配置、全局辅助函数等

8、public - 公共文件、静态文件等

9、vendor - 第三方类库文件

10、admin.php - 项目后台入口

11、index.php - 项目前台入口

正式开发(后台)

1、本地创建好项目后,先来一个后台入口,任何框架都有一个入口文件,我们定义为admin.php

define("APP", "/admin");          //设置当前应用的目录
require('./common/kernel.php');   //加载框架的入口文件

2、在 common 文件夹中,创建 kernel.php 文件,里面设置模块名、控制器名和模型常量。参考代码:

<?php
/*********************************************************************************
 * kernel.php 框架入口文件,所有脚本都是从这个文件开始执行,主要是一些全局设置。 *
 * *******************************************************************************
 * 许可声明:专为《长乐未央教育》学员提供的“学习型”超轻量级php框架。*
 * *******************************************************************************
 * 版权所有 (C) 2013-2019 武汉长乐未央网络科技有限公司,并保留所有权利。           *
 * 网站地址: https://itfun.tv (长乐教育)                             *
 * *******************************************************************************
 * $Author: 黄栋进 (244500972@qq.com) $                                    *
 * $Date: 2019-09-10 10:00:00 $                                                  *
 * ******************************************************************************/


header("Content-Type:text/html;charset=utf-8");  //设置系统的输出字符为utf-8
date_default_timezone_set("PRC");             //设置时区(中国)

defined("SITE_PATH") or define("SITE_PATH", getcwd());  //站点路径
defined("APP_PATH") or define("APP_PATH", getcwd() . '/app');  //应用路径

define('MODULES_PATH', APP_PATH . APP);  //模块路径
define('CONTROLLERS_PATH', APP_PATH . APP . '/controllers');  //控制器路径
define('MODELS_PATH', APP_PATH . APP . '/models');  //模型路径
define('VIEWS_PATH', APP_PATH . APP . '/views');  //视图路径

//当前访问的模块、控制器、方法
$m = isset($_GET['m']) ? $_GET['m'] : APP;
$c = isset($_GET['c']) ? $_GET['c'] : 'index';
$a = isset($_GET['a']) ? $_GET['a'] : 'index';

define('MODULE_NAME', $m);
define('CONTROLLER_NAME', $c);
define('ACTION_NAME', $a);

//自动加载机制
function autoload($class_name)
{
    //判断类名是否是外层的公共类
    switch ($class_name) {
        case 'Smarty':
            $file = SITE_PATH . '/vendor/smarty/libs/Smarty.class.php';
            break;
        case 'Controller':
            $file = SITE_PATH . '/common/controller.class.php';
            break;
        case 'Model':
            $file = SITE_PATH . '/common/model.class.php';
            break;
        default:
            //判断类名是否是模块里面对应的类
            $type = substr($class_name, -10) == 'Controller' ? 'controller' : 'model';
            if ($type == 'controller') {
                $controller_name = '/' . strtolower(substr($class_name, 0, -10)) . '.class.php';
                $file = CONTROLLERS_PATH . $controller_name;
            }

            if ($type == 'model') {
                $name = strtolower(substr($class_name, 0, -5));
                $file = MODELS_PATH . "/{$name}.class.php";
            }
    }

    // 如果类名存在,加载该类名
    if (file_exists($file)) {
        require $file;
    }
}


spl_autoload_register("autoload");

//autoload('UserModel');


//定义类名
$controller_name = ucfirst($c) . 'Controller';
$controller = new $controller_name;
$controller->$a();

/***
 * 打印数组
 * @param $array
 */
function dump($array)
{
    echo "<pre>";
    print_r($array);
    echo "</pre>";
}

/**
 * 读取配置文件
 * @param $key
 * @return mixed
 */
function C($key)
{
    $config = require SITE_PATH . '/config/database.php';
    return $config["$key"];
}

/*
 * 实例化公共模型
 */
function M($table)
{
    return new Model($table);
}

/*
 * 实例化自定义的模型
 */
function D($table)
{
    $model = ucfirst($table) . 'Model';
    return new $model($table);
}

3、定义公共模型,在 common 文件夹下,创建 model.class.php 文件,里面设置数据库的连接,并自定义查询函数。

<?php

class Model
{
    public $db;

    private $table;

    public function __construct($table)
    {
        $this->db_config();

        $this->table = $table;
    }

    /**
     * 数据库连接
     */
    private function db_config()
    {
        $this->db = new mysqli(C('db_host'), C('db_user'), C('db_pwd'), C('db_name'));
        if (mysqli_connect_errno()) {
            exit("连接失败: %s<br>" . mysqli_connect_error());
        }
        $this->db->query("set names utf8");
    }

    /**
     * 查询单条数据
     * @param $sql
     * @return array
     */
    function one($sql)
    {
        $result = $this->db->query($sql);
        return $result->fetch_assoc();
    }

    /**
     * 查询多条记录
     * @param $sql
     * @return array
     */
    function all($sql)
    {
        $array = [];
        $result = $this->db->query($sql);
        while ($row = $result->fetch_assoc()) {
            $array[] = $row;
        }
        return $array;
    }
}

解析:其中的 C 函数是读取数据库的各项配置。

4、接下来创建数据库配置文件,在 config 文件夹中,创建 database.php 文件,里面写上如下代码:

<?php
return [
    'db_host' => '127.0.0.1', // 服务器地址
    'db_name' => 'chat', // 数据库名
    'db_user' => 'root', // 用户名
    'db_pwd' => 'root', // 密码
    'DB_CHARSET' => 'utf8', // 字符集
];

接下来在 vendor 文件夹中引入 smarty 类。

5、在 common 文件夹里面创建公共控制器 controller.class.php ,对 smarty 做基础配置,然后定义跳转和重写视图加载方法,具体代码如下:

<?php

class Controller extends Smarty
{
    function __construct()
    {
        parent::__construct();
        $this->smarty_config();
    }

    /**
     * smarty 初始化
     */
    private function smarty_config()
    {
        $view_path = VIEWS_PATH . '/';
        $this->setTemplateDir($view_path);
        $this->setCompileDir(SITE_PATH . '/runtime/templates_c/');
        $this->left_delimiter = '{{';
        $this->right_delimiter = '}}';
    }

    /**
     * 跳转,并返回信息
     * @param string $url
     * @param string $info
     */
    public function redirect($info = '', $url = '')
    {
        if ($info) {
            echo "<script>alert('" . $info . "')</script>";
        }

        if ($url) {
            echo "<script>location.href='" . $url . "';</script>";
        } else {
            echo "<script>location.href=document.referrer;</script>";
        }
    }

    /**
     * 重写display方法,现在可以不传模板名称了
     * @param null $template
     * @param null $cache_id
     * @param null $compile_id
     * @param null $parent
     */
    public function display($template = null, $cache_id = null, $compile_id = null, $parent = null)
    {
        $template = $template ? $template . '.html' : APP_PATH . MODULE_NAME . '/views' . '/' . CONTROLLER_NAME . '/' . ACTION_NAME . '.html';
        parent::display($template, $cache_id = null, $compile_id = null, $parent = null);
    }
}

解析:此处我加了一个 runtime 文件夹,用于存储模板缓存文件,那么,项目的目录中需要新增一个 runtime 文件夹。

6、测试
app\admin\controllers 里面,新建控制器index.class.php, 里面输出:

<?php

class IndexController extends Controller
{
    public function index()
    {
        echo "this is index_controller";
    }
}

终端执行启动命令:php -S localhost:8000,浏览器访问 http://localhost:8000/admin.php

接下来,我们来测试一点查询,还是在当前控制器的 index 方法中写上如下代码:

public function index()
{
    $Article = M('article');
    $articles = $Article->all("select * from article");
    dump($articles);exit;
}

刷新浏览器,你会看到如下结果:

如果你想访问其他控制器对应的方法,比如想访问 user.class.php 控制器里面的 login 方法,可以这么访问:http://localhost:8000/admin.php?c=user&a=login

如果你想加载静态页面,直接使用 $this->display(); 即可。

至此,后台配置已完成!

正式开发(前台)

1、在项目的根目录下创建 index.php 文件,表示前台入口,里面添加代码:

define("APP", "/home");             //设置当前应用的目录
require('./common/kernel.php');     //加载框架的入口文件

2、在 home\controllers 里面创建控制器 index.class.php ,里面添加代码:

<?php

class IndexController extends Controller
{
    function index()
    {
        echo "这是前台首页";
    }
}

如图所示:

如果你想访问其他控制器对应的方法,比如想访问 user.class.php 控制器里面的 login 方法,可以这么访问:http://localhost:8000/index.php?c=user&a=login

工厂模式和单例模式的使用

1、使用工厂模式获取实例

kernel.php 文件中修改实例化类名的方法。在 common 文件夹中,新增一个类,取名 factory.class.php,里面写上如下代码:

<?php

class Factory
{
    /***
     * 使用工厂类获取实例
     * @param $c
     * @return mixed
     */
    public static function build($c)
    {
        $controller_name = ucfirst($c) . 'Controller';
        return new $controller_name;
    }
}

然后在 kernel.php 文件中的 switch 加入如下代码:

case 'Factory':
    $file = SITE_PATH . '/common/factory.class.php';
    break;
    
****************************************************

//实例化类名,并执行访问方法
$controller = Factory::build($c);
$controller->$a();

2、使用单例模式封装数据库连接

common 文件夹中,新增一个类,取名 conn.class.php,里面写上如下代码:

<?php

class Conn
{
    //静态变量要设置为私有,防止被修改
    private static $instance;
    private static $db;

    private function __construct()
    {
//        echo 21313;
        $this->db_config();
    }


    //克隆函数声明为私有,防止克隆对象
    private function __clone()
    {

    }

    //提供一个创建唯一实例的接口
    public static function getInstance()
    {
        // instanceof 用于确定一个 PHP 变量是否属于某一类 class 的实例:
        if (!(self::$instance instanceof self)) {
            self::$instance = new self();
        }
        return self::$db;
    }


    /**
     * 数据库连接
     */
    private function db_config()
    {
        self::$db = new mysqli(C('db_host'), C('db_user'), C('db_pwd'), C('db_name'));
        if (mysqli_connect_errno()) {
            exit("连接失败: %s<br>" . mysqli_connect_error());
        }
        self::$db->query("set names utf8");
    }
}

然后在 kernel.php 文件中的 switch 加入如下代码:

case 'Conn':
    $file = SITE_PATH . '/common/conn.class.php';
    break;

修改公共模型 model.class.php 代码如下:

<?php

class Model
{
    protected $db;
    private $table;

    function __construct($table)
    {
        $this->db = Conn::getInstance();
        $this->table = $table;
    }

    /**
     * 查询单条数据
     * @param $sql
     * @return array
     */
    function one($sql)
    {
        $result = $this->db->query($sql);
        return $result->fetch_assoc();
    }

    /**
     * 查询多条记录
     * @param $sql
     * @return array
     */
    function all($sql)
    {
        $array = [];
        $result = $this->db->query($sql);
        while ($row = $result->fetch_assoc()) {
            $array[] = $row;
        }
        return $array;
    }
}

3、实例化自定义模型

kernel.php 中添加代码:

/*
 * 实例化自定义的模型
 */
function D($table)
{
    $model = ucfirst($table) . 'Model';
//    echo $model;
    return new $model($table);
}

admin/models 文件夹中创建模型 article.class.php。里面添加如下代码:

class ArticleModel extends Model
{
    function run()
    {
        echo 777;
    }
}

admin/controllers/index.class.php 中写入测试代码,刷新浏览器看是否能得到 777

$Article = D('article');
$Article->run();

封装模型链式方法

修改 model.class.php 代码如下:

<?php

class Model
{
    protected $db;
    private $field = '*';  //查询所有字段
    private $where = '1=1';
    private $order = 'id asc';
    private $limit = '0, 1000';
    private $table;

    function __construct($table)
    {
        $this->db = Conn::getInstance();
        $this->table = $table;
    }

    /***
     * 要查询的字段,如果不传值就查所有字段
     * @param null $field
     * @return $this
     */
    public function field($field = null)
    {
        $this->field = $field;
        return $this;
    }

    /***
     * 定义where查询条件
     * @param null $where
     * @return $this
     */
    public function where($where = null)
    {
        $sql = [];
        // 如果控制器以数组形式传多条件,默认连接用 and
        if (is_array($where)) {

            $_logic = ' and ';

            // 如果控制器传了_logic,就拼接条件,但是最后一个条件是没有and的,所以要释放掉
            if (isset($where['_logic'])) {
                $_logic = ' ' . $where['_logic'] . ' ';
                unset($where['_logic']);
            }
            // 如果控制器传的是数组,对数组进行循环,拼接SQL返回
            foreach ($where as $key => $value) {
                $sql[] = "{$key} = {$value}";
            }
            $sql = implode($_logic, $sql);
//            echo $sql;
            $this->where = $sql;
            return $this;
        }

        $this->where = $where;
        return $this;
    }

    /***
     * 查询多条
     * @return array
     */
    public function select()
    {
        $sql = "select {$this->field} from {$this->table} where {$this->where} order by {$this->order} limit  {$this->limit}";
//        echo $sql;exit;
        $array = [];
        $result = $this->db->query($sql);
        while ($row = $result->fetch_assoc()) {
            $array[] = $row;
        }
        return $array;
    }

    /***
     * 查询单条
     */
    public function find()
    {
        $sql = "select {$this->field} from {$this->table} where {$this->where} order by {$this->order} limit  {$this->limit}";
        $result = $this->db->query($sql);
        $row = $result->fetch_assoc();
        return $row;
    }

    /**
     * 排序
     * @param $order
     * @return $this
     */
    public function order($order = null)
    {
        $this->order = $order;
        return $this;
    }

    /**
     * 列偏移
     * @param $limit
     * @return $this
     */
    public function limit($limit)
    {
        $this->limit = $limit;
        return $this;
    }
}

在后台首页控制器里面的 index 方法中写入如下代码:

$Article = M('article');
$data['id'] = 1;
$articles = $Article->where($data)->select();
dump($articles);

刷新浏览器,看到你想要的结果即可。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值