Smile 笑容

小時內打造你自己的PHP MVC框架

作者:趙春陽

摘要:這是一篇非常精簡的文章去介紹如何去建立一個微型PHP MVC框架,希望可以闡釋清楚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的請求和相應是如何工作的。例如,我們有一個商城網站,然后我們想要添加一個商品,那么最簡單的一個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控制器里的add方法。

前臺控制器設計

在上面的例子中index.php中是什么?在PHP框架中它被稱為入口文件。這個文件通常都被命名為index.php,當然你也可以給它別的命名。這個index.php的最主要的作用就是作為HTTP請求的唯一入口文件,這樣無論你的URL請求什么資源,它都必須通過這個Index.php來請求。你可能要問為什么,它是如何做到的?PHP中的前端控制器用到了Apache服務器的分布式配置.htaccess實現的。在這個文件中,我們可以使用重寫模塊告訴Apache服務器重定向到我們的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只有在啟動的時候才會讀取這些配置文件。

同時,index.php還會進行框架的初始化并且分發路由請求給對應的控制器和方法。

我們的MVC目錄結構

現在讓我們開始創建我們的框架目錄結構。我們你可以隨便先建立一個文件夾,命名為你項目的名稱,比如:/bestshop。在這個文件夾下你需要建立下面的文件夾:

/application-存放web應用程序目錄

/framework-存放框架文件目錄

/public-存放所有的公共的靜態資源,比如HTML文件,CSS文件和jJS文件。

index.php-唯一入口文件


然后在application文件夾下再建立下一級的目錄


/config-存放應用的配置文件

/controllers-應用的控制器類

/model-應用的模型類

/view-應用的視圖文件

 

現在在application/controllers文件夾下,我們還需要創建兩個文件夾,一個frontend,一個backend:

同樣的,在view下也建立frontend和backend文件夾:


就像你看到的,在application的controllers和view下面建立了backen和frontend文件夾,就像我們的用用有前臺和后臺功能一樣。但是為什么不在model下也這樣做呢?

Well, the reason here is, normally for a web app:是因為一般在我們的應用中,前臺和后臺其實是可以看做是兩個“網站的”,但是CRUD操作的是同一個數據庫,這就是問什么管理員更新了貨物的價格的時候,前臺用戶可以馬上看到價格的變化,因為前臺和后臺是共享一個數據庫(表)的。所以在model中沒必要再去建立兩個文件夾。

  

:現在讓我們回到framework文件夾中,一些框架的文件夾命名會用框架的名字命名,比如"symfony"。在framework中讓我們快速建立下面的子目錄:

/core-框架核心文件目錄

/database-數據庫目錄(比如數據庫啟動類)

/helpers-輔助函數目錄

/libraries-類庫目錄


現在進入public文件夾,建立下面的目錄:

/css-存放css文件

/images-存放圖片文件

/js-存放js文件

/uploads-存放上傳的文件


OK。到目前為止這就是我們這個迷你的MVC框架的目錄結構了!

框架核心類

現在在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();

你可以在你的瀏覽器里訪問index.php看到結果。通常這個靜態方法被命名為run()或者bootstrap()。在這個方法中,我們要做3件最主要的事情:

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();

}

在注釋中你可以看到每一步的目的。

自動加載

在項目中,我們不想在腳本中想使用一個類的時候手動的去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&rsquo;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,對于一個模型類,需要被命名成xxModel.class.php。為什么在使用一個框架的時候你需要遵守它的命名規則呢?自動加載就是一條原因。

路由/分發

// 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::Aciton()方法中。

基礎Controller類

通常在框架的核心類中都有一個基礎的控制器。在symfony中,被稱為sfAction;在iOS中,被稱為UIViewController。在這里我們命名為Controller,在framework/core下建立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類的實例化(后面介紹)。準確的說,$this->loader是一個變量指向了被實例化的Load類。在這里我不過多的討論,但是這的確是一個非常關鍵的概念。我遇到過一些PHP開發者相信在這個語句之后:

$this->loader = new Loader();

$this->load是一個對象。不,它只是一個引用。這是從Java中開始使用的,在Java之前,在C++和Objective C中被稱為指針。引用是個封裝的指針類型。比如,在iOS(O-C)中,我們創建了一個對象:

UIButton *btn = [UIButton alloc] init];

加載類

在framework.class.php中,我們已經封裝好了應用的控制器和模型的自動加載。但是如何自動加載在framework目錄中的類呢?現在我們可以新建一個Loader類,它會加載framework目錄中的類和函數。當我們加載framework類時,只需要調用這個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";

    }

}

封裝模型

我們需要下面兩個類來封裝基礎Model類:

Mysql.class.php - 在framework/database下建立,它封裝了數據庫的鏈接和一些基本查詢方法。

 

Model.class.php - framework/core下建立,基礎模型類,封裝所有的CRUD方法。


Mysql.class.php :

<?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&rsquo;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&rsquo;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控制器就正常執行了,控制器中實例化了模型類,并且將得到的數據傳給了視圖中的模板,這樣在瀏覽器中就能看到數據了。

 

這是一篇非常精簡的文章去介紹如何去建立一個微型PHP MVC框架,希望可以闡釋清楚MVC框架中的一些關鍵概念。


PHPChina原創譯文,轉載請注明來源以及作者!

原文鏈接:http://www.codeproject.com/Articles/1080626/WebControls/

安徽时时是真的吗