65.9K
CodeProject 正在变化。 阅读更多。
Home

1 小时内编写您自己的 PHP MVC 框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (15投票s)

2016年2月24日

CPOL

8分钟阅读

viewsIcon

241500

从头开始编写我们自己的 MVC 框架。

引言

MVC 架构模式如今几乎无处不在,无论您是在处理 Java、C#、PHP 还是 iOS 项目。这可能不是100%准确,但PHP社区拥有数量最多的MVC框架。今天您可能在使用 Zend,明天在另一个项目上您可能需要切换到 Yii、Laravel 或 CakePHP。如果您是 MVC 框架的新手,并且刚刚从官方网站下载了一个,那么当您查看框架的源代码时,可能会感到不知所措,是的,它很复杂,因为这些流行的框架不是在一个月内编写完成的——它们经过发布、完善、反复测试,并且功能不断增加。因此,根据我的经验,了解 MVC 框架的核心设计方法至关重要,否则您可能会觉得当您在新项目中使用新框架时,不得不一次又一次地学习另一个框架。


理解 MVC 的最佳方式是自己从头开始编写一个 MVC 框架!在本系列文章中,我将向您展示如何编写一个,以便您可能理解为什么框架中某些事情会以这种方式发生。

MVC 架构模式

M: 模型 (Model)

V: 视图 (View)

C: 控制器 (Controller)

 

MVC 的核心概念是将业务逻辑与显示(视图部分)分离。首先让我解释一下 HTTP 请求和 HTTP 响应的整个工作流程。例如,我们有一个电子商务网站,我们想添加某个产品。一个非常简单的 URL 看起来像这样:

http://bestshop.com/index.php?p=admin&c=goods&a=add

 

http://bestshop.com 是域名或基本 URL;

p=admin 表示平台是管理员面板,或者是系统的后端站点。我们还有一个前端站点,它是对公众开放的(在这种情况下,它将是 p=public)


c=goods&a=add 表示此 URL 请求“goods”控制器(Controller)的 add 动作方法(action method)。

 

前端控制器设计模式 (Front Controller Design Pattern)

在上面的例子中,index.php 是什么?在 PHP 的 MVC 框架中,这个文件被称为“前端控制器(Front Controller)”。名称通常是 index.php,但您也可以将其命名为其他名称(很少有人这样做……)。这个 index.php 文件的其中一个功能是,它作为任何 HTTP 请求的单一入口点,也就是说,无论您在 URL 中请求什么资源,它都会首先进入这个 index.php 文件。但为什么呢?魔术是如何发生的?前端控制器设计模式在 PHP 中通过 Apache HTTP 服务器的分布式配置文件 .htaccess 实现。在这个文件中,我们可以通过其重写模块告诉 Apache HTTP 服务器将所有请求重定向到 index.php。您可以编写类似这样的代码:

 

<IfModule mod_rewrite.c>

   

   Options +FollowSymLinks

   RewriteEngine on


   # Send request via index.php

   RewriteCond %{REQUEST_FILENAME} !-f

   RewriteCond %{REQUEST_FILENAME} !-d

   RewriteRule ^(.*)$ index.php/$1 [L]


</IfModule>

 

这个配置文件非常强大,当你更改它时,你不需要重启 Apache。如果你更改 Apache 的其他配置文件,你需要重启它,原因是对于其他 Apache 配置文件,Apache 只会在启动时读取它们,所以任何更改都需要重启(顺便说一下,Apache 主要用 C 语言编写)。但是对于 .htaccess 分布式配置文件,任何更改都不需要重启。


简单来说,index.php 还会为框架做适当的初始化,并将请求路由到适当的控制器(Controller)(在上面的例子中是 goodsController)和该控制器类中的一个动作方法(action method)(成员函数)。

 

我们的 MVC 框架结构

那么,让我们创建我们的框架结构吧。

application 目录是网络应用程序的目录;

framework 目录是框架本身专用的;

public 目录用于存储所有公共静态资源,如 html、css 和 js 文件。

index.php 是“入口文件”,即前端控制器。

 

现在在 application 文件夹内,我们创建这些子文件夹:

config - 存储应用的配置文件

controllers - 用于所有应用的控制器类

model - 用于所有应用的模型类

view - 用于所有应用的视图类

 

现在在 application/controllers 文件夹中,我们将为前端和后端平台创建两个文件夹:

 

在 view 文件夹中,前端和后端也一样。

 

如您所见,在 application 文件夹中,我们在 controllers 和 views 文件夹中创建了 frontend 和 backend 子文件夹,因为我们的应用程序有一个前端站点和一个后端站点。但为什么我们没有在 models 文件夹中这样做呢?

嗯,这里的原因是,通常对于一个网络应用来说:

前端和后端可以是两个不同的“网站”,但它们对同一数据库进行增删改查 (CRUD),这就是为什么内部用户更新产品价格后,价格会立即显示在前端公共页面上——后端和前端共享相同的数据库/表。

 

所以这就是为什么后端和前端可以共享一组模型类,这也是我们没有在 models 文件夹中创建独立文件夹的原因。

 

现在让我们转到 framework 目录,有些框架会用框架的名称来命名这个文件夹,比如“symfony”。在这个文件夹中,我们首先快速创建这些子文件夹:

core - 它将存储框架的核心类

database - 数据库相关类,例如数据库驱动类

helpers - 辅助/助手函数

libraries - 用于类库

 

现在转到 public 文件夹,我们创建这些子文件夹:

css - 用于 css 文件

images - 用于图片文件

js - 用于 javascript 文件

uploads - 用于上传的文件,例如上传的图片

 

好的,到目前为止,这就是我们这个迷你 MVC 框架的结构。


 

框架的核心类

现在在 framework/core 文件夹下,我们将创建框架的第一个类——framework/core 文件夹中的 Framework.class.php

// framework/core/Framework.class.php

class Framework {


   public static function run() {

       echo "run()";

   }

 

我们创建了一个名为 run() 的静态函数。现在在 index.php 中测试它:

 

<?php


require "framework/core/Framework.class.php";


Framework::run();

您可以在浏览器中看到结果(此处省略了如何配置虚拟主机)。通常这个静态函数被称为 run() 或 bootstrap()。在这个函数中,我们可以做三件主要的事情,如下面的代码所示:

 

class Framework {


   public static function run() {

//        echo "run()";

       self::init();

       self::autoload();

       self::dispatch();

   }


   private static function init() {

   }


   private static function autoload() {


   }


   private static function dispatch() {


   }

}

 

初始化

这是 init() 方法的代码:

 

   // Initialization

   private static function init() {

       // Define path constants

       define("DS", DIRECTORY_SEPARATOR);

       define("ROOT", getcwd() . DS);

       define("APP_PATH", ROOT . 'application' . DS);

       define("FRAMEWORK_PATH", ROOT . "framework" . DS);

       define("PUBLIC_PATH", ROOT . "public" . DS);


       define("CONFIG_PATH", APP_PATH . "config" . DS);

       define("CONTROLLER_PATH", APP_PATH . "controllers" . DS);

       define("MODEL_PATH", APP_PATH . "models" . DS);

       define("VIEW_PATH", APP_PATH . "views" . DS);


       define("CORE_PATH", FRAMEWORK_PATH . "core" . DS);

       define('DB_PATH', FRAMEWORK_PATH . "database" . DS);

       define("LIB_PATH", FRAMEWORK_PATH . "libraries" . DS);

       define("HELPER_PATH", FRAMEWORK_PATH . "helpers" . DS);

       define("UPLOAD_PATH", PUBLIC_PATH . "uploads" . DS);


       // Define platform, controller, action, for example:

       // index.php?p=admin&c=Goods&a=add

       define("PLATFORM", isset($_REQUEST['p']) ? $_REQUEST['p'] : 'home');

       define("CONTROLLER", isset($_REQUEST['c']) ? $_REQUEST['c'] : 'Index');

       define("ACTION", isset($_REQUEST['a']) ? $_REQUEST['a'] : 'index');


       define("CURR_CONTROLLER_PATH", CONTROLLER_PATH . PLATFORM . DS);

       define("CURR_VIEW_PATH", VIEW_PATH . PLATFORM . DS);


       // Load core classes

       require CORE_PATH . "Controller.class.php";

       require CORE_PATH . "Loader.class.php";

       require DB_PATH . "Mysql.class.php";

       require CORE_PATH . "Model.class.php";


       // Load configuration file

       $GLOBALS['config'] = include CONFIG_PATH . "config.php";


       // Start session

       session_start();

   }

 

从注释中您可以看到每个步骤的目的。

 

自动加载 (Autoloading)

我们不希望在项目中的每个脚本中,手动为我们需要的类文件编写 include 或 require,这就是 PHP MVC 框架具有自动加载功能的原因。例如,在 Symfony 中,如果将自己的类文件放在“lib”文件夹下,那么它将被自动加载。神奇吗?不,没有魔术。让我们在我们的迷你框架中实现自动加载功能。

 

这里我们需要使用一个 PHP 内置函数,名为 spl_autoload_register

    // Autoloading

    private static function autoload(){

        spl_autoload_register(array(__CLASS__,'load'));

    }


    // Define a custom load method

    private static function load($classname){


        // Here simply autoload app’s controller and model classes

        if (substr($classname, -10) == "Controller"){

            // Controller

            require_once CURR_CONTROLLER_PATH . "$classname.class.php";

        } elseif (substr($classname, -5) == "Model"){

            // Model

            require_once  MODEL_PATH . "$classname.class.php";

        }

    }

每个框架都有一个命名约定,我们的也不例外。对于控制器类,它应该是 xxxController.class.php;对于模型类,它应该是 xxxModel.class.php。为什么您遇到的新框架必须遵循其命名约定?自动加载是原因之一。

 

路由/分发 (Routing/Dispatching)

 

    // Routing and dispatching

    private static function dispatch(){

        // Instantiate the controller class and call its action method

        $controller_name = CONTROLLER . "Controller";

        $action_name = ACTION . "Action";

        $controller = new $controller_name;

        $controller->$action_name();

    }

 

在这一步中,index.php 会将请求分派给正确的 Controller::Action() 方法。这里为了示例,它非常简单。

 

基础控制器类 (Base Controller class)

 

框架的核心类中总是有一个(或几个)基础控制器类。例如,在 Symfony 中它被称为 sfActions;在 iOS 中它被称为 UIViewController。这里我们只将其命名为 Controller,文件名为 Controller.class.php


 

<?php

// Base Controller

class Controller{

    // Base Controller has a property called $loader, it is an instance of Loader class(introduced later)

    protected $loader;


    public function __construct(){

        $this->loader = new Loader();

    }


    public function redirect($url,$message,$wait = 0){

        if ($wait == 0){

            header("Location:$url");

        } else {

            include CURR_VIEW_PATH . "message.html";

        }


        exit;

    }

}

基础控制器有一个名为 $loader 的属性,它是 Loader 类的一个实例(稍后介绍)。请注意“它是 Loader 类的一个实例”这句话——精确地说,$this->loader 是一个引用变量,它引用/指向 Load 类的一个实例。我们在这里不详细讨论,但这实际上是一个非常重要的概念。我遇到过一些 PHP 开发者,他们相信在执行这条语句后:

$this->loader = new Loader();

$this->loader 是一个对象。不,它是一个引用。这个术语始于 Java,在 Java 之前,它在 C++ 或 Objective C 中被称为指针。引用是一种封装的指针类型。例如,在 iOS (Objective-C) 中,我们使用以下方式创建对象:

UIButton *btn = [UIButton alloc] init];


 

加载器类 (Loader class)

在 framework.class.php 中,我们已经实现了应用程序控制器和模型类的自动加载。但是如何加载 framework 目录中的类呢?这里我们可以创建一个名为 Loader 的新类,它将用于加载框架的类和函数。当我们需要加载框架的类时,只需调用这个 Loader 类的方法即可。

 

class Loader{

    // Load library classes

    public function library($lib){

        include LIB_PATH . "$lib.class.php";

    }


    // loader helper functions. Naming conversion is xxx_helper.php;

    public function helper($helper){

        include HELPER_PATH . "{$helper}_helper.php";

    }

}


 

实现模型 (Implementing Model)

 

我们将以最简单的方式实现 Model,通过创建两个类文件:

Mysql.class.php - 这个类位于 framework/database 目录下,它用于封装数据库连接和一些基本的 SQL 查询方法。

 

Model.class.php - 这是基础模型类,它包含各种 CRUD 方法。


 

<?php

/**

*================================================================

*framework/database/Mysql.class.php

*Database operation class

*================================================================

*/

class Mysql{

    protected $conn = false;  //DB connection resources

    protected $sql;           //sql statement

   

    /**

     * Constructor, to connect to database, select database and set charset

     * @param $config string configuration array

     */

    public function __construct($config = array()){

        $host = isset($config['host'])? $config['host'] : 'localhost';

        $user = isset($config['user'])? $config['user'] : 'root';

        $password = isset($config['password'])? $config['password'] : '';

        $dbname = isset($config['dbname'])? $config['dbname'] : '';

        $port = isset($config['port'])? $config['port'] : '3306';

        $charset = isset($config['charset'])? $config['charset'] : '3306';

       

        $this->conn = mysql_connect("$host:$port",$user,$password) or die('Database connection error');

        mysql_select_db($dbname) or die('Database selection error');

        $this->setChar($charset);

    }

    /**

     * Set charset

     * @access private

     * @param $charset string charset

     */

    private function setChar($charest){

        $sql = 'set names '.$charest;

        $this->query($sql);

    }

    /**

     * Execute SQL statement

     * @access public

     * @param $sql string SQL query statement

     * @return $result,if succeed, return resrouces; if fail return error message and exit

     */

    public function query($sql){        

        $this->sql = $sql;

        // Write SQL statement into log

        $str = $sql . "  [". date("Y-m-d H:i:s") ."]" . PHP_EOL;

        file_put_contents("log.txt", $str,FILE_APPEND);

        $result = mysql_query($this->sql,$this->conn);

       

        if (! $result) {

            die($this->errno().':'.$this->error().'<br />Error SQL statement is '.$this->sql.'<br />');

        }

        return $result;

    }

    /**

     * Get the first column of the first record

     * @access public

     * @param $sql string SQL query statement

     * @return return the value of this column

     */

    public function getOne($sql){

        $result = $this->query($sql);

        $row = mysql_fetch_row($result);

        if ($row) {

            return $row[0];

        } else {

            return false;

        }

    }

    /**

     * Get one record

     * @access public

     * @param $sql SQL query statement

     * @return array associative array

     */

    public function getRow($sql){

        if ($result = $this->query($sql)) {

            $row = mysql_fetch_assoc($result);

            return $row;

        } else {

            return false;

        }

    }

    /**

     * Get all records

     * @access public

     * @param $sql SQL query statement

     * @return $list an 2D array containing all result records

     */

    public function getAll($sql){

        $result = $this->query($sql);

        $list = array();

        while ($row = mysql_fetch_assoc($result)){

            $list[] = $row;

        }

        return $list;

    }

    /**

     * Get the value of a column

     * @access public

     * @param $sql string SQL query statement

     * @return $list array an array of the value of this column

     */

    public function getCol($sql){

        $result = $this->query($sql);

        $list = array();

        while ($row = mysql_fetch_row($result)) {

            $list[] = $row[0];

        }

        return $list;

    }


   

    /**

     * Get last insert id

     */

    public function getInsertId(){

        return mysql_insert_id($this->conn);

    }

    /**

     * Get error number

     * @access private

     * @return error number

     */

    public function errno(){

        return mysql_errno($this->conn);

    }

    /**

     * Get error message

     * @access private

     * @return error message

     */

    public function error(){

        return mysql_error($this->conn);

    }

}

这是 Model.class.php:

 

<?php

// framework/core/Model.class.php

// Base Model Class

class Model{

    protected $db; //database connection object

    protected $table; //table name

    protected $fields = array();  //fields list

    public function __construct($table){

        $dbconfig['host'] = $GLOBALS['config']['host'];

        $dbconfig['user'] = $GLOBALS['config']['user'];

        $dbconfig['password'] = $GLOBALS['config']['password'];

        $dbconfig['dbname'] = $GLOBALS['config']['dbname'];

        $dbconfig['port'] = $GLOBALS['config']['port'];

        $dbconfig['charset'] = $GLOBALS['config']['charset'];

       

        $this->db = new Mysql($dbconfig);

        $this->table = $GLOBALS['config']['prefix'] . $table;

        $this->getFields();

    }

    /**

     * Get the list of table fields

     *

     */

    private function getFields(){

        $sql = "DESC ". $this->table;

        $result = $this->db->getAll($sql);

        foreach ($result as $v) {

            $this->fields[] = $v['Field'];

            if ($v['Key'] == 'PRI') {

                // If there is PK, save it in $pk

                $pk = $v['Field'];

            }

        }

        // If there is PK, add it into fields list

        if (isset($pk)) {

            $this->fields['pk'] = $pk;

        }

    }

    /**

     * Insert records

     * @access public

     * @param $list array associative array

     * @return mixed If succeed return inserted record id, else return false

     */

    public function insert($list){

        $field_list = '';  //field list string

        $value_list = '';  //value list string

        foreach ($list as $k => $v) {

            if (in_array($k, $this->fields)) {

                $field_list .= "`".$k."`" . ',';

                $value_list .= "'".$v."'" . ',';

            }

        }

        // Trim the comma on the right

        $field_list = rtrim($field_list,',');

        $value_list = rtrim($value_list,',');

        // Construct sql statement

        $sql = "INSERT INTO `{$this->table}` ({$field_list}) VALUES ($value_list)";

        if ($this->db->query($sql)) {

            // Insert succeed, return the last record’s id

            return $this->db->getInsertId();

            //return true;

        } else {

            // Insert fail, return false

            return false;

        }

       

    }

    /**

     * Update records

     * @access public

     * @param $list array associative array needs to be updated

     * @return mixed If succeed return the count of affected rows, else return false

     */

    public function update($list){

        $uplist = ''; //update fields

        $where = 0;   //update condition, default is 0

        foreach ($list as $k => $v) {

            if (in_array($k, $this->fields)) {

                if ($k == $this->fields['pk']) {

                    // If it’s PK, construct where condition

                    $where = "`$k`=$v";

                } else {

                    // If not PK, construct update list

                    $uplist .= "`$k`='$v'".",";

                }

            }

        }

        // Trim comma on the right of update list

        $uplist = rtrim($uplist,',');

        // Construct SQL statement

        $sql = "UPDATE `{$this->table}` SET {$uplist} WHERE {$where}";

       

        if ($this->db->query($sql)) {

            // If succeed, return the count of affected rows

            if ($rows = mysql_affected_rows()) {

                // Has count of affected rows  

                return $rows;

            } else {

                // No count of affected rows, hence no update operation

                return false;

            }    

        } else {

            // If fail, return false

            return false;

        }

       

    }

    /**

     * Delete records

     * @access public

     * @param $pk mixed could be an int or an array

     * @return mixed If succeed, return the count of deleted records, if fail, return false

     */

    public function delete($pk){

        $where = 0; //condition string

        //Check if $pk is a single value or array, and construct where condition accordingly

        if (is_array($pk)) {

            // array

            $where = "`{$this->fields['pk']}` in (".implode(',', $pk).")";

        } else {

            // single value

            $where = "`{$this->fields['pk']}`=$pk";

        }

        // Construct SQL statement

        $sql = "DELETE FROM `{$this->table}` WHERE $where";

        if ($this->db->query($sql)) {

            // If succeed, return the count of affected rows

            if ($rows = mysql_affected_rows()) {

                // Has count of affected rows

                return $rows;

            } else {

                // No count of affected rows, hence no delete operation

                return false;

            }        

        } else {

            // If fail, return false

            return false;

        }

    }

    /**

     * Get info based on PK

     * @param $pk int Primary Key

     * @return array an array of single record

     */

    public function selectByPk($pk){

        $sql = "select * from `{$this->table}` where `{$this->fields['pk']}`=$pk";

        return $this->db->getRow($sql);

    }

    /**

     * Get the count of all records

     *

     */

    public function total(){

        $sql = "select count(*) from {$this->table}";

        return $this->db->getOne($sql);

    }

    /**

     * Get info of pagination

     * @param $offset int offset value

     * @param $limit int number of records of each fetch

     * @param $where string where condition,default is empty

     */

    public function pageRows($offset, $limit,$where = ''){

        if (empty($where)){

            $sql = "select * from {$this->table} limit $offset, $limit";

        } else {

            $sql = "select * from {$this->table}  where $where limit $offset, $limit";

        }

       

        return $this->db->getAll($sql);

    }

}

 

现在我们可以在 application 文件夹中创建一个 User 模型类,这是为了我们数据库中的 User 表。代码看起来会是这样:
 

<?php

// application/models/UserModel.class.php

class UserModel extends Model{


    public function getUsers(){

        $sql = "select * from $this->table";

        $users = $this->db->getAll($sql);

        return $users;

    }

}

我们的应用程序后端 indexController 可能看起来像这样:

 

<?php

// application/controllers/admin/IndexController.class.php


class IndexController extends BaseController{

    public function mainAction(){

        include CURR_VIEW_PATH . "main.html";

        // Load Captcha class

        $this->loader->library("Captcha");

        $captcha = new Captcha;

        $captcha->hello();

        $userModel = new UserModel("user");

        $users = $userModel->getUsers();

    }

    public function indexAction(){

                       $userModel = new UserModel("user");

        $users = $userModel->getUsers();

        // Load View template

        include  CURR_VIEW_PATH . "index.html";

    }

    public function menuAction(){

        include CURR_VIEW_PATH . "menu.html";

    }

    public function dragAction(){

        include CURR_VIEW_PATH . "drag.html";

    }

    public function topAction(){

        include CURR_VIEW_PATH . "top.html";

    }

}

到目前为止,我们应用程序后端的 Index 控制器正在工作,它与 Model 进行通信并将结果变量传递给 View 模板,这样就可以在浏览器中渲染了。

 

这是对迷你 MVC 框架的非常简短的介绍,希望它能阐明 MVC 框架中的一些基本概念。

 

 

历史

2016年2月25日 - 初始版本

© . All rights reserved.