php 基礎的MVC封裝


編寫自己的PHP MVC框架筆記

 

 

1、MVC

MVC模式(Model-View-Controller)是軟件工程中的一種軟件架構模式,把軟件系統分為三個基本部分:模型(Model)、視圖(View)和控制器(Controller)。

PHP中MVC模式也稱Web MVC,從上世紀70年代進化而來。MVC的目的是實現一種動態的程序設計,便於后續對程序的修改和擴展簡化,並且使程序某一部分的重復利用成為可能。除此之外,此模式通過對復雜度的簡化,使程序結構更加直觀。軟件系統通過對自身基本部份分離的同時,也賦予了各個基本部分應有的功能。

MVC各部分的職能:

  • 模型Model – 管理大部分的業務邏輯所有的數據庫邏輯。模型提供了連接和操作數據庫的抽象層。
  • 控制器Controller - 負責響應用戶請求、准備數據,以及決定如何展示數據。
  • 視圖View – 負責渲染數據,通過HTML方式呈現給用戶。

 

一個典型的Web MVC流程:

  1. Controller截獲用戶發出的請求;
  2. Controller調用Model完成狀態的讀寫操作;
  3. Controller把數據傳遞給View;
  4. View渲染最終結果並呈獻給用戶。

2、為什么自己開發MVC框架

網絡上有大量優秀的MVC框架可供使用,本教程並不是為了開發一個全面的、終極的MVC框架解決方案,而是將它看作是一個很好的從內部學習PHP的機會,在此過程中,你將學習面向對象編程MVC設計模式,並學習到開發中的一些注意事項。

更重要的是,你可以完全控制你的框架,並將你的想法融入到你開發的框架中。雖然不一定是做好的,但是你可以按照你的方式去開發功能和模塊。

3、准備開發自己的MVC框架

3.1目錄准備

在開始開發前,讓我們先來把項目建立好,假設我們建立的項目為 myphp-frame,MVC的框架可以命名為 MyPHP,那么接下來的第一步就是把目錄結構先設置好。

 

 

雖然在這個教程中不會使用到上面的所有的目錄,但是為了以后程序的可拓展性,在一開始就把程序目錄設置好使非常必要的。下面就具體說說每個目錄的作用:

  • application – 應用代碼
  • config – 程序配置或數據庫配置
  • myphp - 框架核心目錄
  • public – 靜態文件
  • runtime - 臨時數據目錄

3.2代碼規范

在目錄設置好以后,我們接下來就要來規定一下代碼的規范:

  1. MySQL的表名需小寫,如:item,car
  2. 模塊名(Models)需首字母大寫,,並在名稱后添加“Model”,如:ItemModel,CarModel
  3. 控制器(Controllers)需首字母大寫,,並在名稱中添加“Controller”,如:ItemController,CarController
  4. 視圖(Views)部署結構為“控制器名/行為名”,如:item/view.php,car/buy.php

上述的一些規則是為了能在程序中更好的進行互相的調用。接下來就開始真正的PHP MVC編程了

3.3重定向

將所有的數據請求都重定向 index.php 文件,在 myphp-frame 目錄下新建一個 .htaccess 文件,文件內容為:

復制代碼
<IfModule mod_rewrite.c>

    RewriteEngine On

    # 確保請求路徑不是一個文件名或目錄

    RewriteCond %{REQUEST_FILENAME} !-f

    RewriteCond %{REQUEST_FILENAME} !-d

    # 重定向所有請求到 index.php?url=PATHNAME

    RewriteRule ^(.*)$ index.php?url=$1 [PT,L]

</IfModule>
復制代碼

這樣做的主要原因有:

  1. 程序有一個單一的入口;
  2. 除靜態程序,其他所有程序都重定向到 index.php 上;
  3. 可以用來生成利於SEO的URL,想要更好的配置URL,后期可能會需要URL路由,這里先不做介紹了。

3.4入口文件

做完上面的操作,就應該知道我們需要做什么了,沒錯!在 myphp-frame目錄下添加 index.php 文件,文件內容為:

復制代碼
<?php 
// 應用目錄為當前目錄
define('APP_PATH', __DIR__.'/');
// 開啟調試模式
define('APP_DEBUG', true);
// 網站根URL
define('APP_URL', 'http://localhost/myphp');
// 加載框架
require './myphp/MyPHP.php';
復制代碼

注意,上面的PHP代碼中,並沒有添加PHP結束符號”?>”,這么做的主要原因是,對於只有 PHP 代碼的文件,結束標志(“?>”)最好不存在,PHP自身並不需要結束符號,不添加結束符號可以很大程度上防止末尾被添加額外的注入內容,讓程序更加安全。

3.5配置文件和主請求

在 index.php 中,我們對 myphp  文件夾下的 MyPHP.php 發起了請求,那么 MyPHP.php 這個啟動文件中到底會包含哪些內容呢?

復制代碼
<?php
// 初始化常量
defined('FRAME_PATH') or define('FRAME_PATH', __DIR__.'/');
defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/');
defined('APP_DEBUG') or define('APP_DEBUG', false);
defined('CONFIG_PATH') or define('CONFIG_PATH', APP_PATH.'config/');
defined('RUNTIME_PATH') or define('RUNTIME_PATH', APP_PATH.'runtime/');
// 包含配置文件
require APP_PATH . 'config/config.php';
//包含核心框架類
require FRAME_PATH . 'Core.php'; 
// 實例化核心類
$fast = new Core;
$fast->run();
復制代碼

以上文件都其實可以直接在 index.php 文件中包含,常量也可以直接在 index.php 中定義,我們這么做的原因是為了在后期管理和拓展中更加的方便,所以把需要在一開始的時候就加載運行的程序統一放到一個單獨的文件中引用。

先來看看config文件下的 config .php 文件,該文件的主要作用是設置一些程序的配置項及數據庫連接等,主要內容為:

<?php
/** 變量配置 **/
define('DB_NAME', mydb);
define('DB_USER', 'root');
define('DB_PASSWORD', 'root');
define('DB_HOST', 'localhost');

應該說 config.php 涉及到的內容並不多,不過是一些基礎數據庫的設置,再來看看 myphp下的共用框架入口文件 Core.php 應該怎么寫。

復制代碼
<?php
/**
 * MyPHP核心框架
 */
class Core
{
    // 運行程序
    function run()
    {
        spl_autoload_register(array($this, 'loadClass'));

        $this->setReporting();

        $this->removeMagicQuotes();

        $this->unregisterGlobals();

        $this->Route();
    }
    // 路由處理
    function Route()
    {
       $controllerName = 'Index';
        $action = 'index';
        if (!empty($_GET['url'])) {
            $url = $_GET['url'];
            $urlArray = explode('/', $url);
            // 獲取控制器名
            $controllerName = ucfirst($urlArray[0]);
            // 獲取動作名
            array_shift($urlArray);
            $action = empty($urlArray[0]) ? 'index' : $urlArray[0];
            //獲取URL參數
            array_shift($urlArray);
            $queryString = empty($urlArray) ? array() : $urlArray;
        }
        // 數據為空的處理
        $queryString  = empty($queryString) ? array() : $queryString;
        // 實例化控制器
        $controller = $controllerName . 'Controller';
        $dispatch = new $controller($controllerName, $action);
        // 如果控制器存和動作存在,這調用並傳入URL參數
        if ((int)method_exists($controller, $action)) {
            call_user_func_array(array($dispatch, $action), $queryString);
        } else {
            exit($controller . "控制器不存在");
        }
    }
    // 檢測開發環境
    function setReporting()
    {
        if (APP_DEBUG === true) {
            error_reporting(E_ALL);
            ini_set('display_errors','On');
        } else {
            error_reporting(E_ALL);
            ini_set('display_errors','Off');
            ini_set('log_errors', 'On');
            ini_set('error_log', RUNTIME_PATH. 'logs/error.log');
        }
    }
    // 刪除敏感字符
    function stripSlashesDeep($value)
    {
        $value = is_array($value) ? array_map('stripSlashesDeep', $value) : stripslashes($value);
        return $value;
    }
    // 檢測敏感字符並刪除
    function removeMagicQuotes()
    {
        if ( get_magic_quotes_gpc()) {
           $_GET = stripSlashesDeep($_GET );
            $_POST = stripSlashesDeep($_POST );
            $_COOKIE = stripSlashesDeep($_COOKIE);
            $_SESSION = stripSlashesDeep($_SESSION);
        }
    }
    // 檢測自定義全局變量(register globals)並移除
    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]);
                    }
                }
            }
        }
    }
    // 自動加載控制器和模型類
    static function loadClass($class)
    {
        $frameworks = FRAME_PATH . $class . '.class.php';
        $controllers = APP_PATH . 'application/controllers/' . $class . '.class.php';
        $models = APP_PATH . 'application/models/' . $class . '.class.php';
        if (file_exists($frameworks)) {
            // 加載框架核心類
            include $frameworks;
        } elseif (file_exists($controllers)) {
            // 加載應用控制器類
            include $controllers;
        } elseif (file_exists($models)) {
            //加載應用模型類
            include $models;
        } else {
            /* 錯誤代碼 */
        }
    }
}
復制代碼

下面重點講解主請求方法 route(),首先我們想看看我們的 URL 會這樣:

yoursite.com/controllerName/actionName/queryString

route()的作用就是,從全局變量 $_GET['url']變量中獲取 URL,並將其分割成三部分:$controller、$action 和 $queryString。

例如,URL鏈接為:myphp.com/item/view/1/first-item,那么

  • $controller 就是:item
  • $action 就是:view
  • 查詢字符串Query String就是:array(1, first-item)

分割完成后,會實例化一個新的控制器:$controller.'Controller'(其中“.”是連字符),並調用其方法 $action。

3.6控制器/Controller基類

接下來的操作就是在 myphp 中建立程序所需的基類,包括控制器模型視圖的基類。

新建控制器基類為 Controller.class.php,控制器的主要功能就是總調度,具體具體內容如下:

復制代碼
<?php 
/**
 * 控制器基類
 */
class Controller
{
    protected $_controller;
    protected $_action;
    protected $_view;
    // 構造函數,初始化屬性,並實例化對應模型
    function __construct($controller, $action)
    {
        $this->_controller = $controller;
        $this->_action = $action;
        $this->_view = new View($controller, $action);
    }
    // 分配變量
    function assign($name, $value)
    {
        $this->_view->assign($name, $value);
    }
    // 渲染視圖
    function __destruct()
    {
        $this->_view->render();
    }
}
復制代碼

Controller 類實現所有控制器、模型和視圖(View類)的通信。在執行析構函數時,我們可以調用 render() 來顯示視圖(view)文件。

3.7模型Model基類

新建模型基類為 Model.class.php,模型基類 Model.class.php 代碼如下:

復制代碼
<?php
class Model extends Sql
{
    protected $_model;
    protected $_table;
    function __construct()
    {
        // 連接數據庫
        $this->connect(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);  
        // 獲取模型名稱
        $this->_model = get_class($this);
        $this->_model = rtrim($this->_model, 'Model'); 
        // 數據庫表名與類名一致
        $this->_table = strtolower($this->_model);
    }
    function __destruct()
    {
    }
}
復制代碼

考慮到模型需要對數據庫進行處理,所以單獨建立一個數據庫基類 Sql.class.php,模型基類繼承 Sql.class.php,代碼如下:

復制代碼
<?php 
class Sql
{
    protected $_dbHandle;
    protected $_result;
    // 連接數據庫
    public function connect($host, $user, $pass, $dbname)
    {
        try {
            $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8", $host, $dbname);
            $this->_dbHandle = new PDO($dsn, $user, $pass, array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC));
        } catch (PDOException $e) {
            exit('錯誤: ' . $e->getMessage());
        }
    }
    // 查詢所有
    public function selectAll()
    {
        $sql = sprintf("select * from `%s`", $this->_table);
        $sth = $this->_dbHandle->prepare($sql);
        $sth->execute();
 
        return $sth->fetchAll();
    }
    // 根據條件 (id) 查詢
    public function select($id)
    {
        $sql = sprintf("select * from `%s` where `id` = '%s'", $this->_table, $id);
        $sth = $this->_dbHandle->prepare($sql);
        $sth->execute();
        
        return $sth->fetch();
    }
    // 根據條件 (id) 刪除
    public function delete($id)
    {
        $sql = sprintf("delete from `%s` where `id` = '%s'", $this->_table, $id);
        $sth = $this->_dbHandle->prepare($sql);
        $sth->execute();
 
        return $sth->rowCount();
    }
    // 自定義SQL查詢,返回影響的行數
    public function query($sql)
    {
        $sth = $this->_dbHandle->prepare($sql);
        $sth->execute();
 
        return $sth->rowCount();
    }
    // 新增數據
    public function add($data)
    {
        $sql = sprintf("insert into `%s` %s", $this->_table, $this->formatInsert($data));
 
        return $this->query($sql);
    }
    // 修改數據
    public function update($id, $data)
    {
        $sql = sprintf("update `%s` set %s where `id` = '%s'", $this->_table, $this->formatUpdate($data), $id);
 
        return $this->query($sql);
    }
    // 將數組轉換成插入格式的sql語句
    private function formatInsert($data)
    {
        $fields = array();
        $values = array();
        foreach ($data as $key => $value) {
            $fields[] = sprintf("`%s`", $key);
            $values[] = sprintf("'%s'", $value);
        }
 
        $field = implode(',', $fields);
        $value = implode(',', $values);
 
        return sprintf("(%s) values (%s)", $field, $value);
    }
    // 將數組轉換成更新格式的sql語句
    private function formatUpdate($data)
    {
        $fields = array();
        foreach ($data as $key => $value) {
            $fields[] = sprintf("`%s` = '%s'", $key, $value);
        }
        return implode(',', $fields);
    }
}
復制代碼

應該說,Sql.class.php 是框架的核心部分。為什么?因為通過它,我們創建了一個 SQL 抽象層,可以大大減少了數據庫的編程工作。雖然 PDO 接口本來已經很簡潔,但是抽象之后框架的可靈活性更高。

3.8視圖View類

視圖類 View.class.php 內容如下:

復制代碼
<?php
/**
 * 視圖基類
 */
class View
{
    protected $variables = array();
    protected $_controller;
    protected $_action;
 
    function __construct($controller, $action)
    {
        $this->_controller = $controller;
        $this->_action = $action;
    }
    /** 分配變量 **/
    function assign($name, $value)
    {
        $this->variables[$name] = $value;
    }
    /** 渲染顯示 **/
    function render()
    {
        extract($this->variables);
        $defaultHeader = APP_PATH . 'application/views/header.php';
        $defaultFooter = APP_PATH . 'application/views/footer.php';
        $controllerHeader = APP_PATH . 'application/views/' . $this->_controller . '/header.php';
        $controllerFooter = APP_PATH . 'application/views/' . $this->_controller . '/footer.php';
        // 頁頭文件
        if (file_exists($controllerHeader)) {
            include ($controllerHeader);
        } else {
            include ($defaultHeader);
        }
        // 頁內容文件
        include (APP_PATH . 'application/views/' . $this->_controller . '/' . $this->_action . '.php');
        // 頁腳文件
        if (file_exists($controllerFooter)) {
            include ($controllerFooter);
        } else {
            include ($defaultFooter);
        }
    }
}
復制代碼

這樣我們的核心的PHP MVC框架就編寫完成了,下面我們開始編寫應用來測試框架功能。

4、應用

4.1數據庫部署

在 SQL 中新建一個 mydb 數據庫,使用下面的語句增加 item 數據表並插入2條記錄:

CREATE DATABASE ` mydb ` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE ` mydb `;
CREATE TABLE `item` (
    `id` int(11) NOT NULL auto_increment,
    `item_name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO `item` VALUES(1, 'Hello World.');
INSERT INTO `item` VALUES(2, 'Lets go!');

4.2部署模型

然后,我們還需要在 models 目錄中創建一個 ItemModel.php 模型,內容如下:

<?php
 
class ItemModel extends Model
{
    /* 業務邏輯層實現 */
}

模型內容為空。因為 Item 模型繼承了 Model,所以它擁有 Model 的所有功能。

4.3部署控制器

在 controllers 目錄下創建一個 ItemController.php 控制器,內容如下:

復制代碼
<?php
class ItemController extends Controller
{
    // 首頁方法,測試框架自定義DB查詢
    public function index()
    {
        $items = (new ItemModel)->selectAll();
 
        $this->assign('title', '全部條目');
        $this->assign('items', $items);
    }
    // 添加記錄,測試框架DB記錄創建(Create)
    public function add()
    {
        $data['item_name'] = $_POST['value'];
        $count = (new ItemModel)->add($data);
 
        $this->assign('title', '添加成功');
        $this->assign('count', $count);
    }
    // 查看記錄,測試框架DB記錄讀取(Read)
    public function view($id = null)
    {
        $item = (new ItemModel)->select($id);
 
        $this->assign('title', '正在查看' . $item['item_name']);
        $this->assign('item', $item);
    }
    // 更新記錄,測試框架DB記錄更新(Update)
    public function update()
    {
        $data = array('id' => $_POST['id'], 'item_name' => $_POST['value']);
        $count = (new ItemModel)->update($data['id'], $data);
 
        $this->assign('title', '修改成功');
        $this->assign('count', $count);
    }
    
    // 刪除記錄,測試框架DB記錄刪除(Delete)
    public function delete($id = null)
    {
        $count = (new ItemModel)->delete($id);
 
        $this->assign('title', '刪除成功');
        $this->assign('count', $count);
    }
}
復制代碼

4.4部署視圖

在 views 目錄下新建 header.php 和 footer.php 兩個頁頭頁腳模板,內容如下。

header.php,內容:

復制代碼
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title><?php echo $title ?></title>
    <style>
        .item {
            width:400px;
        }
 
        input {
            color:#222222;
            font-family:georgia,times;
            font-size:24px;
            font-weight:normal;
            line-height:1.2em;
            color:black;
        }
 
        a {
            color:blue;
            font-family:georgia,times;
            font-size:20px;
            font-weight:normal;
            line-height:1.2em;
            text-decoration:none;
         }
 
        a:hover {
            text-decoration:underline;
        }
 
        h1 {
            color:#000000;
            font-size:41px;
            letter-spacing:-2px;
            line-height:1em;
            font-family:helvetica,arial,sans-serif;
            border-bottom:1px dotted #cccccc;
        }
 
        h2 {
            color:#000000;
            font-size:34px;
            letter-spacing:-2px;
            line-height:1em;
            font-family:helvetica,arial,sans-serif;
        }
    </style>
</head>
<body>
    <h1><?php echo $title ?></h1>
footer.php,內容:

</body>
</html>
復制代碼

然后,在 views/item 創建以下幾個視圖文件。

index.php,瀏覽數據庫內 item 表的所有記錄,內容:

復制代碼
<form action="<?php echo APP_URL ?>/item/add" method="post">
    <input type="text" value="點擊添加" onclick="this.value=''" name="value">
    <input type="submit" value="添加">
</form>
<br/><br/>
 
<?php $number = 0?>
 
<?php foreach ($items as $item): ?>
    <a class="big" href="<?php echo APP_URL ?>/item/view/<?php echo $item['id'] ?>" title="點擊修改">
        <span class="item">
            <?php echo ++$number ?>
            <?php echo $item['item_name'] ?>
        </span>
    </a>
    ----
    <a class="big" href="<?php echo APP_URL ?>/item/delete/<?php echo $item['id']?>">刪除</a>
<br/>
<?php endforeach ?>
復制代碼

add.php,添加記錄,內容:

<a class="big" href="<?php echo APP_URL ?>/item/index">成功添加<?php echo $count ?>條記錄,點擊返回</a>

view.php,查看單條記錄,內容:

復制代碼
<form action="<?php echo APP_URL ?>/item/update" method="post">
    <input type="text" name="value" value="<?php echo $item['item_name'] ?>">
    <input type="hidden" name="id" value="<?php echo $item['id'] ?>">
    <input type="submit" value="修改">
</form>
 
<a class="big" href="<?php echo APP_URL ?>/item/index">返回</a>
復制代碼

update.php,更改記錄,內容:

<a class="big" href="<?php echo APP_URL ?>/item/index">成功修改<?php echo $count ?>項,點擊返回</a>

delete.php,刪除記錄,內容:

<a href="<?php echo APP_URL ?>/item/index">成功刪除<?php echo $count ?>項,點擊返回</a>

4.5應用測試

這樣,在瀏覽器中訪問 myphp程序:http://localhost/myphp/item/index/,就可以看到效果了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM