溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何搭建自己的PHP MVC框架

發(fā)布時間:2021-02-18 10:05:07 來源:億速云 閱讀:127 作者:小新 欄目:開發(fā)技術(shù)

這篇文章給大家分享的是有關(guān)如何搭建自己的PHP MVC框架的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

前言

說到寫PHP的MVC框架,大家想到的第一個詞--“造輪子”,是的,一個還沒有深厚功力的程序員,寫出的PHP框架肯定不如那些出自大神們之手、經(jīng)過時間和各種項目考驗的框架。但我還是準(zhǔn)備并且這么做了,主要是因為:

認(rèn)為有關(guān)PHP的方方面面都了解了,但自己學(xué)習(xí)PHP的時間還短,基礎(chǔ)并不扎實,很多常用函數(shù)的參數(shù)還偶爾要查手冊,而且對于PHP的一些較新的特性如命名空間、反射等只是簡單的看過,并沒有能實際應(yīng)用過。

PHP的知識多且雜,一個普通的項目往住是業(yè)務(wù)邏輯代碼為主,而框架是一個能把這些知識點能融匯在一起的項目。

在自己寫一個框架的時候,也會參考一些我使用過的框架如TP/CI/YII等的源碼,在自己看源碼時也能幫助自己理解框架,更容易接受以后要使用的框架。

所以說,這次造輪子的目的不是為了造輪子而是為了在造輪子的過程中熟悉其工藝,總結(jié)輪子特點,更好的使用輪子。

如果說寫一個完整的PHP框架,那需要掌握的PHP知識點非常多,像設(shè)計模式、迭代器、事件與鉤子等等,還有許多基礎(chǔ)知識的靈活應(yīng)用。我自認(rèn)為這些還無法完全掌控,所以我的步驟是先自己搭建一個骨架,然后參考借鑒不同的PHP框架的特點,將其慢慢完善。因為工作原因,而且晚上還要補算法、網(wǎng)絡(luò)等編程基礎(chǔ),PHP框架部分可能只有周末有時間更新,我會在進行框架功能更新之后,總結(jié)使用的知識點,更新博文。

首先放上框架的目前源碼:GITHUB/zhenbianshu

或者點擊此處本站下載。

框架整體

首先自己總結(jié)一下PHP的MVC框架的工作流程:

簡單來說,它以一個入口文件來接受請求,選擇路由,處理請求,返回結(jié)果。

當(dāng)然,幾句話總結(jié)完的東西實際上要做的工作很多,PHP框架會在每次接受請求時,定義常量,加載配置文件、基礎(chǔ)類,根據(jù)訪問的URL進行邏輯判斷,選擇對應(yīng)的(模塊)控制器和方法,并且自動加載對應(yīng)類,處理完請求后,框架會選擇并渲染對應(yīng)的模板文件,以html頁面的形式返回響應(yīng)。在處理邏輯的時候,還要考慮到錯誤和異常的處理。

1、作為MVC框架,一定要有一個唯一的入口文件來統(tǒng)領(lǐng)全局,所有的訪問請求都會首先進入這個入口文件,如我框架根目錄的index.php,在里面,我定義了基本文件夾路徑,當(dāng)前環(huán)境,并根據(jù)當(dāng)前環(huán)境定義錯誤報告的級別。

2、PHP中加載另外的文件,使用require和include,它們都是將目標(biāo)文件內(nèi)容加載到當(dāng)前文件內(nèi),替換掉require或include語句,require是加載進來就執(zhí)行,而include是加載進來在需要的時候執(zhí)行,而它們的_once結(jié)構(gòu)都是表示在寫多次的時候只執(zhí)行一次。

3、框架內(nèi)的配置變量等使用專用的配置文件來保存,這里我仿照了TP里的數(shù)組返回法,用了一個compileConf()函數(shù)來解析數(shù)組,將數(shù)組的鍵定義為常量,值為數(shù)組的值。

if (!function_exists('compile_conf')) {
  function compileConf($conf) {
    foreach ($conf as $key => $val) {
    if(is_array($val)){
       compileConf($val);
      }else{
      define($key, $val);
      }
    }
   }
}
compileConf(require_once CONF_PATH.'config.php');

命名空間和自動加載

為什么把命名空間和自動加載放到一塊說呢?

在一個PHP項目中,類特別多的時候,如果類名重復(fù)的話就會造成混亂,而且相同文件夾內(nèi)也不能存在同名的文件,所以這時候命名空間和文件夾就搭檔出場了。文件夾就是一個一個的盒子,命名空間在我理解就像是一個標(biāo)簽,盒子對應(yīng)標(biāo)簽。我們定義類時,把各種類用不同的盒子分別裝好,并貼上對應(yīng)的標(biāo)簽。而在自動加載類時,我們根據(jù)標(biāo)簽(命名空間)可以很輕易找到對應(yīng)的盒子(文件夾)然后找到對應(yīng)的類文件。

而類的自動加載,我們知道的__autoload()魔術(shù)函數(shù),它會在你實例化一個當(dāng)前路徑找不到的對象時自動調(diào)用,根據(jù)傳入的類名,在函數(shù)體內(nèi)加載對應(yīng)的類文件。

現(xiàn)在我們多用spl_autoload_register()函數(shù),它可以注冊多個函數(shù)來代替__autoload函數(shù)的功能,我們傳入一個函數(shù)名為參數(shù),spl_autoload_register會將這個函數(shù)壓入棧中,在實例化一個當(dāng)前路徑內(nèi)找不到的類時,系統(tǒng)將會將函數(shù)出棧依次調(diào)用,直到實例化成功。

spl_autoload_register('Sqier\Loader::autoLoad');
class Loader {
public static function autoLoad($class) {
  //如果有的話,去除類最左側(cè)的\
  $class = ltrim($class, '\\');
  //獲取類的路徑全名
  $class_path = str_replace('\\', '/', $class) . EXT;
  if (file_exists(SYS_PATH . $class_path)) {
    include SYS_PATH . $class_path;
    return;
  }
  if (file_exists(APP_PATH . $class_path)) {
    include APP_PATH . $class_path;
    return;
  }
}

現(xiàn)在Loader類還是一個簡單的類,待以后慢慢完善。

路由選擇

接下來就是路由選擇了,其本質(zhì)是根據(jù)當(dāng)前定義的全局URL模式選擇合適的方法來分析傳入的URI,加載對應(yīng)的類,并實現(xiàn)對應(yīng)的方法。

class Router {
public static $uri;
public static function bootstrap() {
  self::$uri = $_SERVER['REQUEST_URI'];
  switch (URL_MODE) {
    case 1: {
      self::rBoot();
      break;
    }
    default: {
      self::rBoot();
    }
  }
}
public static function rBoot() {
  $router = isset($_GET['r']) ? explode('/', $_GET['r']) : [
    'index',
    'index'
  ];
  $cName = 'Controller\\' . ucfirst($router[0]);
  $aName = isset($router[1]) ? strtolower($router[1]) . 'Action' : 'indexAction';
  $controller = new $cName();
  $controller->$aName();
  }
}

這樣,我在地址欄輸入 zbs.com/index.php?r=index/login 后,系統(tǒng)會自動調(diào)用/app/Controller/Index.php下的login方法。完成了這么一個簡單的路由。

階段總結(jié):

接下來我會優(yōu)化現(xiàn)有的工具類,添加顯示層,添加數(shù)據(jù)庫類,還會將一些別的框架里非常cool的功能移植進來~

接上文(代碼有所更新),繼續(xù)完善框架(二):

對于本次更新,我想說:

① 本框架由本人挑時間完善,而我還不是PHP大神級的人物,所以框架漏洞難免,求大神們指出。
② 本框架的知識點應(yīng)用都會寫在博客里,大家有什么異議的可以一起討論,也希望看博客的也能學(xué)習(xí)到它們。
③ 本次更新,更新了函數(shù)規(guī)范上的一些問題,如將函數(shù)盡量的獨立化,每一個函數(shù)盡量只單獨做好一件事情,盡量減少函數(shù)依賴。還對框架的整體優(yōu)化了一下,添加了SQ全局類,用以處理全局函數(shù),變量。

回調(diào)函數(shù)

替換了很low的類名拼裝實例化,然后拼裝方法名的用法,使用PHP的回調(diào)函數(shù)方式:

原代碼:

$controller_name = 'Controller\\' . self::$c_name;
$action_name = self::$a_name . 'Action';
$controller = new $controller_name();
$controller->$action_name();

修改后代碼

$controller_name = 'Controller\\' . self::$c_name;
$controller = new $controller_name();
call_user_func([
  $controller,
  self::$a_name . 'Action'
]);

這里介紹一下PHP的函數(shù)回調(diào)應(yīng)用方式:call_user_func和call_user_func_array:

call_user_func ( callback $function [, mixed $parameter [, mixed $... ]] )

調(diào)用第一個參數(shù)所提供的用戶自定義的函數(shù)。

返回值:返回調(diào)用函數(shù)的結(jié)果,或FALSE。

call_user_func_array()的用法跟call_user_func類似,只不過傳入的參數(shù)params整體為一個數(shù)組。

另外,call_user_func系列函數(shù)還可以傳入在第一個參數(shù)里傳入匿名參數(shù),可以很方便的回調(diào)某些事件,這些特性在復(fù)雜的框架里應(yīng)用也十分廣泛,如yii2的事件機制里回調(diào)函數(shù)的使用就是基于此。

VIEW層和ob函數(shù)

框架在controller的基類中定義了render方法來渲染頁面,它會調(diào)用類VIEW的靜態(tài)函數(shù)來分析加載對應(yīng)頁面的模板。

public static function display($data, $view_file) {
  if(is_array($data)) {
    extract($data);//extract函數(shù)解析$data數(shù)組中的變量
  }else {
    //拋出變量類型異常
  }
  ob_start();
  ob_implicit_flush(0);
  include self::checkTemplate($view_file);//自定義checkTemplate函數(shù),分析檢查對應(yīng)的函數(shù)模板,正常返回路徑
  $content = ob_get_clean();
  echo $content;
}

這里重點說一下ob(output buffering)系列函數(shù),其作用引用簡明代魔法的ob作用介紹:

① 防止在瀏覽器有輸出之后再使用setcookie,或者header,session_start函數(shù)造成的錯誤。其實這樣的用法少用為好,養(yǎng)成良好的代碼習(xí)慣。
② 捕捉對一些不可獲取的函數(shù)的輸出,比如phpinfo會輸出一大堆的HTML,但是我們無法用一個變量例如$info=phpinfo();來捕捉,這時候ob就管用了。
③ 對輸出的內(nèi)容進行處理,例如進行g(shù)zip壓縮,例如進行簡繁轉(zhuǎn)換,例如進行一些字符串替換。
④ 生成靜態(tài)文件,其實就是捕捉整頁的輸出,然后存成文件,經(jīng)常在生成HTML,或者整頁緩存中使用。

它在ob_start()函數(shù)執(zhí)行后,打開緩沖區(qū),將后面的輸出內(nèi)容裝進系統(tǒng)的緩沖區(qū),ob_implicit_flush(0)函數(shù)來關(guān)閉絕對刷送(echo等),最后使用ob_get_clean()函數(shù)將緩沖區(qū)的內(nèi)容取出來。

類__URL__常量和全局類

TP里的__URL__等全局常量用著很方便,可以很簡單的實現(xiàn)跳轉(zhuǎn)等操作,而定義它的函數(shù)createUrl函數(shù)我又想重用,于是借鑒YII的全局類定義方法:

定義基類及詳細方法(以后的全局方法會寫在這里)

class BaseSqier{
  //方法根據(jù)傳入的$info信息,和當(dāng)前URL_MODE解析返回URL字符串
  public static function createUrl($info = '') {
    $url_info = explode('/', strtolower($info));
    $controller = isset($url_info[1]) ? $url_info[0] : strtolower(CONTROLLER);
    $action = isset($url_info[1]) ? $url_info[1] : $url_info[0];
    switch(URL_MODE){
      case URL_COMMON:
        return "/index.php?r=" . $controller . '/' . $action;
      case URL_REWRITE:
        return '/' .$controller . '/' . $action;
    }
  }
}

在啟動文件中定義類并繼承基類;

require_once SQ_PATH.'BaseSqier.php';
class SQ extends BaseSqier{
}

在全局內(nèi)都可以直接使用SQ::createUrl()方法來創(chuàng)建URL了。這樣,定義__URL__常量就很輕松了。

用單例模式定義數(shù)據(jù)庫連接基類

class Db {
  protected static $_instance;
  public static function getInstance() {
    if(!(self::$_instance instanceof self)) {
      self::$_instance = new self();
    }
    return self::$_instance;
  }
  private function __construct() {
    $link = new \mysqli(DB_HOST, DB_USER, DB_PWD, DB_NAME) or die("連接數(shù)據(jù)庫失敗,請檢查數(shù)據(jù)庫配置信息!");
    $link->query('set names utf8');
  }
  public function __clone() {
    return self::getInstance();
  }
}

使用單例模式的核心是:

① 私有化構(gòu)造函數(shù),使無法用new來創(chuàng)建對象,也防止子類繼承它并改寫其構(gòu)造函數(shù);
② 用靜態(tài)變量存放當(dāng)前對象,定義靜態(tài)方法來返回對象,如對象還未實例化,實例化一個,存入靜態(tài)變量并返回。
③ 構(gòu)造其__clone魔術(shù)方法,防止clone出一個新的對象;

DB類的sql查詢函數(shù)

DB查詢函數(shù)是一個很復(fù)雜的部分,它是一個自成體系的東西,像TP和YII的查詢方法都有其獨特的地方。我這里暫時先借用TP的MODEL基類,有時間再慢慢補這個。

嗯,介紹一下像TP的查詢里的方法聯(lián)查的實現(xiàn),其訣竅在于,在每個聯(lián)查方法的最后都用 return this 來返回已處理過的查詢對象。

階段總結(jié):

yii2里的數(shù)據(jù)表和model類屬性之間的映射很酷(雖然被深坑過), 前面一直避開的模塊(module,我可以想像得到把它也添加到URI時解析的麻煩)有時間考慮一下。

接上文,繼續(xù)完善框架(三)

本次更新的主要內(nèi)容有:

① 介紹了異常處理機制
② 完善了異常和錯誤處理
③ 數(shù)據(jù)表跟Model類的映射

異常處理

異常處理:異常處理是編程語言或計算機硬件里的一種機制,用于處理軟件或信息系統(tǒng)中出現(xiàn)的異常狀況(即超出程序正常執(zhí)行流程的某些特殊條件)

異常處理用于處理程序中的異常狀況,雖說是“異常狀態(tài)”,但仍然還是在程序編寫人員的預(yù)料之中,其實程序的異常處理完全可以用‘if else'語句來代替,但異常處理自然有其優(yōu)勢之處。

個人總結(jié)其優(yōu)點如下:

① 可以快速終止流程,重置系統(tǒng)狀態(tài),清理變量和內(nèi)存占用,在普通WEB應(yīng)用中,一次請求結(jié)束后,F(xiàn)AST CGI會自動清理變量和上下文,但如果在PHP的命令行模式執(zhí)行守護腳本時,它的效果就會很方便了。

② 大量的if else語句會使代碼變得繁雜難懂,使用異常處理可以使程序邏輯更清晰易懂,畢竟處理異常的入口只有catch語句一處。

③ 一量程序中的函數(shù)出現(xiàn)異常結(jié)果或狀況,如果使用函數(shù)的return方式返回異常信息,層層向上,每一次都要進行return判斷。使用異常處理我們可以假設(shè)所有的返回信息都是正常的,避免了大量的代碼重復(fù)。

雖然將代碼放在try catch塊中會有微微的效率差,但是跟這些優(yōu)點一比,這點消耗就不算什么了。那么PHP的異常處理怎么使用呢?

PHP內(nèi)置有Exception類,使得我們可以通過實例化異常類來拋出異常。我們將代碼放在try語句中執(zhí)行,并在其后用catch試圖捕捉到在try代碼塊中拋出的異常,并對異常進行處理。我們還可以在catch代碼段后使用finally語句塊,無論是否有異常都會執(zhí)行finally代碼塊的代碼,try catch語句形如下面代碼:

try{
  throw new Exeption('msg'[,'code',$previous_exeception]);
}catch(Exeption $var) {
  process($var);
}catch(MyException $e){
  process($e)
}finally{
  dosomething();
}

使用try catch語句,需要注意:

① 當(dāng)我們拋出異常時,會實例化一個異常類,此異常類可以自己定義,但在catch語句中,我們需要規(guī)定要捕獲的異常對象的類名,并且只能捕獲到特定類的異常對象,當(dāng)然我們可以在最后捕獲一個異?;悾≒HP內(nèi)置異常類)來確保異常一定能被捕獲。

② 在拋出異常時,程序會被終止,并回溯代碼找到第一個能捕獲到它的catch語句,try catch語句是可以嵌套的,并且如上面代碼所示 cacth語句是可以多次定義的。

③ finally塊會在try catch塊結(jié)束后執(zhí)行,即使在try catch塊中使用return返回,程序沒有執(zhí)行到最后。

框架里的異常處理

說了那么多異常相關(guān)(當(dāng)然解釋這些也是為了能理解和使用框架),那么框架里要怎么實現(xiàn)呢?

重寫異常類

我們可以重寫異常類,完善其內(nèi)部方法:

<?php
class Exception
{
  protected $message = 'Unknown exception';  // 異常信息
  protected $code = 0;            // 異常代碼
  protected $file;              // 發(fā)生異常的文件名
  protected $line;              // 發(fā)生異常的代碼行號
  function __construct($message = null, $code = null,$previous_exeception = null);
  final function getMessage();        // 返回異常信息
  final function getCode();          // 返回異常代碼
  final function getFile();          // 返回發(fā)生異常的文件名
  final function getLine();          // 返回發(fā)生異常的代碼行號
  final function getTrace();         // 返回異常trace數(shù)組
  final function getTraceAsString();     // 返回異常trace信息
  /**
   * 記錄錯誤日志
   */
  protected function log(){
    Logger::debug();
  }
}

如上,final方法是不可以重寫的,除此之外,我們可以定義自己的方法,如記錄異常日志,像我自定義的log方法,在catch代碼塊中,就可以直接使用$e->log來記錄一個異常日志了。

注冊全局異常方法

我們可以使用set_exception_handler('exceptionHandler')來全局捕獲沒有被catch塊捕獲到的異常,此異常處理函數(shù)需要傳入一個異常處理對象,這樣可以分析此異常處理信息,避免系統(tǒng)出現(xiàn)不人性化的提示,增強框架的健壯性。

function exceptionHandler($e) {
  echo '有未被捕獲的異常,在' . $e->getFile() . "的" . $e->getLine() . "行!";
}

其他全局函數(shù)

順便再說一下其他的全局處理函數(shù):

set_shutdown_function('shutDownHandler')來執(zhí)行腳本結(jié)束時的函數(shù),此函數(shù)即使是在ERROR結(jié)束后,也會自動調(diào)用。

set_error_handler('errorHandler')在PHP發(fā)生錯誤時自動調(diào)用,注意,必須在已注冊錯誤函數(shù)后才發(fā)出的錯誤才會調(diào)用。函數(shù)參數(shù)形式應(yīng)為($errno, $errstr, $errfile, $errline);

但是要注意這些全局函數(shù)需要在代碼段的前面已經(jīng)定義過再注冊。

數(shù)據(jù)表和Model類的ActiveRecord映射

初次使用yii2的ActivceRecord類覺得好方便,只需要定義其字段同名屬性再調(diào)用save方法就OK了(好神奇?。窃趺磳崿F(xiàn)的呢,看了下源碼,明白了其大致實現(xiàn)過程(基類)。

1. 使用‘describe table_name' 查詢語句;
2. 分析查詢結(jié)果:對每一個字段,有Field(字段名)、Type(數(shù)據(jù)類型)、Null(是否為空)、Key(索引信息,‘PRI'表示為主鍵)、Default(默認(rèn)值)、Extra(附加信息,如auto_increment)
3. 通過判斷其主鍵($row['KEY'] == 'PRI')信息,保存時看是否有主鍵信息,若存在,則為更新;不存在,則插入。

感謝各位的閱讀!關(guān)于“如何搭建自己的PHP MVC框架”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI