hello,大家好。今天要給大家分享的是php的進階教程 ,透過本教程講同學們將學會如何一步步實現MVC的架構,打造自己的MVC框架。
還記得上次分享給同學們的教程 “php基礎-設計模式”的中用到的各種模式嗎?這些模式會在本章學習MVC架構時用到。
說了那麼多,那MVC究竟是個什麼鬼?我們首先看個圖,來直觀的感受下。
透過上圖,大家可以很清楚地看到 ,首先是瀏覽器傳送請求,透過路由找到controller,然後由controller 去排程view 和model。
Controller:排程 model和view層的排程器
Model: 處理業務程式碼,並與資料庫互動。
View:controller獲取model的資料後將資料渲染到View(返回html),最終呈現給瀏覽器端
瞭解了MVC的架構,也不難看出,這是個責任清晰的三層模型,每一層都有自己的責任。Controller負責排程model 和view ,model層處理業務。view層負責渲染資料。前端程式設計師只要管好view層的設計。後端程式設計師只需管好自己的那塊業務邏輯。各司其職。協作開發。大大提高了開發效率。
看到這裡,同學們對於為什麼要使用MVC應該有了的大致 的瞭解了吧。想到2007年以前,那個時代,MVC剛剛流行。有很多公司不屑去用它,結果工程專案大了,人多了,大家各自為政,寫了很多重複的東西。一個專案要組合在一起,也是要費很大的精力和時間的。
同學們在瞭解了什麼是MVC以及為什麼要使用MVC後。相信你們也是躍躍欲試,想要一探其奧秘。接下來馬上為大家揭開其神秘面紗。
我們先來回顧下上圖,任何請求都是由客戶端發起,經由路由到達controller。然後才由controller繼續下面的操作。因此讓我們先看下php是如何實現這部分功能的。
先來看下這麼個專案結構
等等,裡面好像少了model和view。到哪去了?呵呵,先別急,今天只講路由篇,所以只看這些就夠了,下一篇會有model,view,xss,csrf等等。慢慢來。心急吃不了那熱豆腐。
那我們就先從入口檔案開始講吧。先來條醒目地分割線
index.php
=============================================
require "Vendor/custome.php";require (ROOT.DIRECTORY_SEPARATOR."Vendor/lib/autoload.php");require "http/router.php";
=============================================
可以看見就三行程式碼。
Vendor/custome.php
=============================================
define("ROOT",dirname(dirname(__FILE__)));
=============================================
定義了專案根目錄,這個有什麼鳥用?其實在我們require 一個php檔案時,
都是根據這個根目錄來的。我這的專案的那個根就是 php-mvc-stepbystep。
注意:
__FILE__:當前目錄所在檔案,這裡指的就是
define("ROOT",dirname(dirname(__FILE__)));檔案所在父目錄,即 php-mvc-stepbystep 目錄
Vendor/autoload.php
=============================================
class MyAutoLoad { static function doPrint($classname) { $classPath = str_replace("\\","/",$classname); if(is_file(ROOT.DIRECTORY_SEPARATOR.$classPath.".php")) { require_once (ROOT.DIRECTORY_SEPARATOR.$classPath.".php"); } }}spl_autoload_register(array('MyAutoLoad','doPrint'));
=============================================
我們先來說個函式。function __autoload($classname)。這個函式的作用就是自定載入類。這個自動載入類又是個什麼鬼?
話說php5之前還沒有這個函式。它有什麼作用呢?回想當初,我們要引入一個php檔案,那麼就得require "xxx.php",那如果要引入很多檔案呢?哦。MyGod 。拿得多累啊!!!! 所有後來php的建立者就開發這個功能。你可以不用一個個引入php檔案。只需要如此便可。
舉個例子
Vendor/lib/autoload.php
=============================================
function __autoload($classname) { $classPath = str_replace("\\","/",$classname);//將名稱空間轉換成類檔案所在目錄 if(is_file(ROOT.DIRECTORY_SEPARATOR.$classPath.".php")) { require_once (ROOT.DIRECTORY_SEPARATOR.$classPath.".php"); } }
=============================================
php-mvc-stepbystep/index.php
=============================================
require (ROOT.DIRECTORY_SEPARATOR."Vendor/lib/autoload.php");
use Test\Test
Test::sayHello();
=============================================
php-mvc-stepbystep/Test/Test.php
=============================================
namespace Test;class Test{ static function sayHello() { echo "hello"; }}
=============================================
注意:
index.php在引入auload.php後 無需 require "Test/Test.php" 盡然也能執行。神奇吧,這就是autoload的好處。這裡的namespace 就是檔案所在位置。而 類名必須與檔名一致
在充分了解__autoload($classname)後,再來看下sp_autoload_register(array('自定義autoload類','方法'))
如果這個函式引數為空,則預設呼叫__autoload($classname),否則
就呼叫自定義類種的方法。
現在再回過頭來看下
Vendor/autoload.php
=============================================
class MyAutoLoad { static function doPrint($classname) { $classPath = str_replace("\\","/",$classname);//將namespace 轉換成類檔案目錄 if(is_file(ROOT.DIRECTORY_SEPARATOR.$classPath.".php"))//判斷檔案是否存在 { require_once (ROOT.DIRECTORY_SEPARATOR.$classPath.".php");//引入檔案 } }}spl_autoload_register(array('MyAutoLoad','doPrint'));
=============================================
http/router.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 18:25 */use Vendor\router;//route('執行請求的url{型別引數1,型別引數2}','控制器類@方法')router::route('/test1/myttt/{Ints,Bools,Bools}','http\controllers\Test@mytest');
=============================================
這就是本文的重點,路由,沒錯,就是它!!!一個神奇的路由。將客戶端請求路由到指定的類檔案下的某個方法。那它又是如何實現的呢?別急。請跟我來。
例子
根據上面的route設定,在瀏覽器中輸入
引數型別不匹配規則 ,比如最後一個引數改成sss
路徑不匹配規則 ,如
就是如此神奇。那這個是如何實現的呢?請看以下程式碼
Vendor/router.php
=============================================
static function route($url,$call){ $reqUrl = explode('/',$_SERVER['REQUEST_URI']);//客戶端請求的url if(count($reqUrl)>0) //是否帶引數 /test1/myttt/1/true/true { // 解析請求路徑 用來與$url作格式匹配 self::$classPath = $reqUrl[1]; self::$methodPath = $reqUrl[2]; } $arr = explode('@',$call);//要呼叫的控制器類和方法進行解析 self::$controller = $arr[0];//控制器類 self::$method = $arr[1];//方法 $params = self::getParams($reqUrl,3);//請求url 中獲得的型別引數 self::patternUrl($url,$reqUrl);//匹配請求路徑與$url是否一致 kernel::initObserver(self::$controller);//註冊控制器 call_user_func_array( array(registerObj::$objs[self::$controller],self::$method),arrayy($params) );//呼叫註冊好的控制器及對應的方法}//匹配路徑規則
static function patternUrl($patternUrl,$reqUrl){ $res = parse_url($patternUrl,PHP_URL_PATH);//解析路徑規則 $patternUrl = explode("/",$res);//將其分割成陣列 $patternUrlParams = explode( ',',trim(trim(self::getParams($patternUrl,3)[0],"{"),"}")//獲取型別引數 ); //url請求 是否匹配路徑規則 if($patternUrl[1] == self::$classPath ) { if($patternUrl[2] == self::$methodPath ) { } else { print_r("路徑與route中設定的格式不匹配"); exit; } } else { echo '路徑與route中設定的格式不匹配'; exit; } $params = self::getParams($reqUrl,3);//獲取請求url 的型別的引數 //型別引數是否與規則中設定的引數數量一致 if(count($patternUrlParams) != count($params)) { echo "引數個數不匹配"; exit; } //引數型別是否與設定的型別一致 Ints代表 int, Strings 代表string ,Bools 代表bool foreach ($patternUrlParams as $k=>$v) { //var_dump(Types::$v($params[$k])); if(!Types::$v($params[$k])) { echo '引數型別不匹配'; exit; } } unset($params); unset($res); unset($patternUrl); unset($patternUrlParams);}
=============================================
執行流程
首先 route($url,$call) 方法的作用是根據第一個引數設定路的徑規則,校驗請求的路徑和引數(具體如何校驗,請看patternUrl($patternUrl,$reqUrl))。滿足路徑匹配規則後解析第二個引數裡的對應控制器和方法。接著註冊該控制器,最後呼叫控制器及方法。
下面貼出route.php的完整程式碼
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 17:19 */namespace Vendor;use Vendor\lib\registerObj;use kernel\kernel;use Vendor\lib\Types\Types;class router{ private static $ins=null; private static $objList = []; private static $controller; private static $method; private static $classPath; private static $methodPath; private function __construct(){} private function __clone(){} public static function getParams($reqUrl,$step) { for($i=0;$i<$step;$i++) { array_shift($reqUrl); } return $reqUrl; } static function patternUrl($patternUrl,$reqUrl) { $res = parse_url($patternUrl,PHP_URL_PATH); $patternUrl = explode("/",$res); $patternUrlParams = explode(',',trim(trim(self::getParams($patternUrl,3)[0],"{"),"}")); if($patternUrl[1] == self::$classPath ) { if($patternUrl[2] == self::$methodPath ) { } else { print_r("路徑與route中設定的格式不匹配"); exit; } } else { echo '路徑與route中設定的格式不匹配'; exit; } $params = self::getParams($reqUrl,3); if(count($patternUrlParams) != count($params)) { echo "引數個數不匹配"; exit; } foreach ($patternUrlParams as $k=>$v) { //var_dump(Types::$v($params[$k])); if(!Types::$v($params[$k])) { echo '引數型別不匹配'; exit; } } unset($params); unset($res); unset($patternUrl); unset($patternUrlParams); } static function route($url,$call) { $reqUrl = explode('/',$_SERVER['REQUEST_URI']); if(count($reqUrl)>0) { self::$classPath = $reqUrl[1]; self::$methodPath = $reqUrl[2]??"index"; } $arr = explode('@',$call); self::$controller = $arr[0]; self::$method = $arr[1]; $params = self::getParams($reqUrl,3); self::patternUrl($url,$reqUrl); //var_dump($params);die; kernel::initObserver(self::$controller); call_user_func_array( array(registerObj::$objs[self::$controller],self::$method),array($params) ); }}
=============================================
在route.php,細心的同學可能對kernel::initObserver(self::$controller)表示很疑惑。它是如何註冊控制器的呢?不註冊不行嗎?是的。任何使用者自定義的控制器類必須註冊。否則不能使用。下面就讓我們來一探它的真實面貌。
kernel/kernel.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/25 * Time: 0:32 */namespace kernel;use Vendor\lib\RegObserver\RegObserver;use Vendor\lib\RegObs\RegObs;use Vendor\lib\ManagerObserver\ManagerObserver;class kernel{ private function __construct(){} private static $instance = null; private function __clone(){} static function initObserver($key) { $observer = new RegObserver(); $obs = new RegObs(); $mg = new ManagerObserver(); if (is_array($key)) { foreach ($key as $k=>$v) { $mg->add($v,$obs); $observer->notify($mg); } } else { $mg->add($key,$obs); $observer->notify($mg); } }}
=============================================
看到如此簡潔的程式碼,開不開心,意不意外?其思想是採用觀察者模式,透過代理 呼叫Vendor/lib/registerObj.php 將空控制器類註冊到 該物件的靜態變數registerObj::$objs,就是上面route.php 中的
call_user_func_array( array(registerObj::$objs[self::$controller],self::$method),array($params) );
是不是有點暈,沒關係。我們來分步驟講解
先看這段程式碼
static function initObserver($key) { $observer = new RegObserver();//觀察者 $obs = new RegObs();//被觀察者 $mg = new ManagerObserver();//新增,刪除被觀察者 if (is_array($key))//判斷控制器類是否是以陣列形式傳遞的 { foreach ($key as $k=>$v) { $mg->add($v,$obs);//新增控制器類到被觀察者 $observer->notify($mg);//通知被觀察者註冊控制器類 } } else { $mg->add($key,$obs); $observer->notify($mg); } }
這段程式碼主要就是註冊控制器類。再來看看註冊具體實現方式
$observer = new RegObserver();
Vendor/lib/RegObserver/RegObserver.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 1:12 */namespace Vendor\lib\RegObserver;use IFS\Observer\Observer;class RegObserver implements Observer { public function notify( $obs) { // TODO: Implement notify() method. /* *$obs->observers這是個被觀察者列表, *由Vendor/lib/ManagerObserver/ManagerObserver/ManagerObserver.php新增 */ $t = $obs->observers; foreach ($t as $k=>$v) {
$v->handler($k);//註冊控制器類 } }}
=============================================
這段程式碼這要是通知被觀察者去註冊控制器類,然後再看下這個
$mg = new ManagerObserver();
Vendor/lib/ManagerObserver/ManagerObserver.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 2:38 */namespace Vendor\lib\ManagerObserver;use IFS\Observermanager\Observermanager;class ManagerObserver implements Observermanager { public $observers = [];//被觀察者列表,就是控制器類 function add($key, $obs) { // TODO: Implement add() method. $this->observers[$key] = $obs; } function delete($key) { // TODO: Implement delete() method. unset($this->observers[$key]); }}
=============================================
Vendor/lib/RegObs/RegObs.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 2:34 */namespace Vendor\lib\RegObs;use IFS\Obs\Obs;use Vendor\lib\regProxy\regProxy;class RegObs implements Obs {//這裡的$obj 就是控制器類 function handler($obj) { //var_dump((new $obj));die; // TODO: Implement handler() method. //(new $obj)->mytest();die; $proxy = new regProxy(); $proxy->register($obj,(new $obj)); }}
=============================================
以上程式碼就是被觀察者
透過代理類regProxy 去掉Vendor/registerObj/registerObj.php的register方法來註冊控制器類
注意 (new $obj) 這個就是以字串作為物件來建立
例如:
(new 'http\controllers\Test') 等同於 new http\controllers\Test()
看完被觀察者,再來看下代理類
Vendor/lib/regProxy/regProxy.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 2:45 */namespace Vendor\lib\regProxy;class regProxy { function __call($func, $args) { // TODO: Implement registerObj() method. //$c = new registerObj(); //var_dump($args);die; call_user_func_array(array('Vendor\lib\registerObj',$func),$args); }}
=============================================
以上這段程式碼是代理registerObj.php的類的register方法來註冊控制器類,那理解了這個代理方式,接下來就請下這個registerObj吧
Vendor/lib/registerObj/registerObj.php
=============================================
<?php/** * Created by PhpStorm. * User: mario * Date: 2021/1/26 * Time: 3:10 */namespace Vendor\lib;use IFS\Register\Register;class registerObj implements Register { public static $objs = []; public static function register($key, $val) { self::$objs[$key] = $val; //var_dump(get_class_methods(self::$objs[$key])); }}
=============================================
以上程式碼即是正確的註冊控制器類的方法。
總結執行流程:
1 router 校驗url規則
2 透過自動載入方法對映對應控制器類
3由kernel類透過觀察者模式將控制器類註冊到registerObj的$objs中
4呼叫控制器的方法。
對應程式碼如下
kernel::initObserver(self::$controller);//註冊控制器類call_user_func_array( array(registerObj::$objs[self::$controller],self::$method),array($params));//呼叫控制器方法
到這裡為止,“php-mvc-stepbystep-路由篇” 已經講完。下一篇要講得是
“php-mvc-stepbystep-model篇”。同學們 敬請期待。下期再見