本教程翻譯自John Squibb 的Build a PHP MVC Framework in an Hour,但有所改動,原文地址:http://johnsquibb.com/tutorials
-
這個教程可以使大家掌握用mvc模式開發php應用的基本概念。此教程分爲三個部分,現在這篇是第一部分。
現在市面上有很多流行的框架供大家使用,但是我們也可以自己動手開發一個mvc框架,採用mvc模式可以大大減少我們開發應用的時間,並且能夠更好的組織項目源代碼,而且其中的某些模塊還可在其它項目中使用。現在我要教大家寫一個簡單的mvc框架。由於這個項目很簡單,輕量,所以可能並不是最佳實踐,也不具備安全性,還需要大家在實際應用中完善。
所用技術:php,面向對象開發方法。
開始
首先在網站根目錄下建立三個文件夾
- models
- views
- controllers
然後在根目錄下新建一個文件:
- index.php
現在項目結構應該像這樣
§ 網站根目錄
§ index.php
§ models/
§ views/
§ controllers/
index.php是整個web應用的入口點,所有的用戶請求都會經過它。我們會寫一些代碼來把用戶請求分派到相應的控制器中,這些控制器存放在controllers文件夾裏。之後,我們就可以用下面的方式來實現頁面跳轉:
- http://你的域名.com/index.php?page1
- http://你的域名.com/index.php?page2
- http://你的域名.com/index.php?page3
設置前端控制器index.php
首先在index.php中定義網站根目錄和網站域名,以便在整個應用中訪問。
- <?php
- //應用的根目錄就是index.php的父目錄
- define("SERVER_ROOT", dirname(__FILE__));
- //你的域名.comm 是你的服務器域名
- define('SITE_ROOT' , 'http://你的域名.com');
<?php //應用的根目錄就是index.php的父目錄 define("SERVER_ROOT", dirname(__FILE__)); //你的域名.comm 是你的服務器域名 define('SITE_ROOT' , 'http://你的域名.com');
定義了網站根目錄後,在任何php文件中,都能很方便的引用其它目錄的php文件,因爲index.php是入口文件,這樣就能夠在整個應用中訪問在它之中定義的這些變量。
設置路由器router.php(轉發用戶請求到相應控制器)
在controllers目錄下新建一個文件,名字爲“router.php",這個文件用來處理所有頁面請求。想像一下你家裏的路由器,它負責把internet路由到家中的每個電腦。router.php文件將會獲取傳入到index.php的頁面請求,然後把請求分派給不同的控制器(controllers)。
route.php中的代碼:
<?php //獲取所有請求 $request = $_SERVER['QUERY_STRING'];
這句代碼會獲取傳入到應用中的請求參數。QUERY_STRING就是”?“後面的所有字符串。- http://你的域名.com/index.php?page1
上面的地址會在代碼中得到”page1&action=login“,爲了把page1和後面的參數分開,我們需要在route.php中繼續加入下列代碼:- //解析$request變量,得到用戶請求的頁面(page1)和其它GET變量(&分隔的變量)如一個請求http://你的域名.com/index.php?page1&article=buildawebsite,則被解析爲array("page1", "article=buildawebsite")
- $parsed = explode('&' , $request);
- //用戶請求的頁面,如上面的page1,爲$parsed第一個變量,shift之後,數組爲array("article=buildawebsite")
- $page = array_shift($parsed);
- //剩下的爲GET變量,把它們解析出來
- $getVars = array();
- foreach ($parsed as $argument)
- {
- //用"="分隔字符串,左邊爲變量,右邊爲值
- list($variable , $value) = split('=' , $argument);
- $getVars[$variable] = $value;
- }
- //這是測試語句,一會兒會刪除
- print "The page your requested is '$page'";
- print '<br/>';
- $vars = print_r($getVars, TRUE);
- print "The following GET vars were passed to the page:<pre>".$vars."</pre>";
//解析$request變量,得到用戶請求的頁面(page1)和其它GET變量(&分隔的變量)如一個請求http://你的域名.com/index.php?page1&article=buildawebsite,則被解析爲array("page1", "article=buildawebsite") $parsed = explode('&' , $request); //用戶請求的頁面,如上面的page1,爲$parsed第一個變量,shift之後,數組爲array("article=buildawebsite") $page = array_shift($parsed); //剩下的爲GET變量,把它們解析出來 $getVars = array(); foreach ($parsed as $argument) { //用"="分隔字符串,左邊爲變量,右邊爲值 list($variable , $value) = split('=' , $argument); $getVars[$variable] = $value; } //這是測試語句,一會兒會刪除 print "The page your requested is '$page'"; print '<br/>'; $vars = print_r($getVars, TRUE); print "The following GET vars were passed to the page:<pre>".$vars."</pre>";
- <?php
- /**
- * 定義文檔路徑
- */
- define("SERVER_ROOT", dirname(__FILE__));
- define('SITE_ROOT' , 'http://你的域名.com');
- /**
- * 引入router.php
- */
- require_once(SERVER_ROOT . '/controllers/' . 'router.php');
- ?>
如果順利的話,你可以打開瀏覽器輸入:<?php /** * 定義文檔路徑 */ define("SERVER_ROOT", dirname(__FILE__)); define('SITE_ROOT' , 'http://你的域名.com'); /** * 引入router.php */ require_once(SERVER_ROOT . '/controllers/' . 'router.php'); ?>
- http://你的域名.com/index.php?news&article=howtobuildaframework
我們會看到如下輸出- The page you requested is 'news'
- The following GET vars were passed to the page:
- Array
- (
- [article] => howtobuildaframework
- )
The page you requested is 'news' The following GET vars were passed to the page: Array ( [article] => howtobuildaframework )
如果沒有上述輸出,請檢查你的服務器配置是否正確,並檢查代碼是否有錯誤。現在來讓我們添加一個頁面到我們的網站裏,這樣就可以讓router.php來產生一個頁面,而不是直接輸出上面的信息。創建一個控制器(controller)在controllers文件夾裏新建一個文件名爲“news.php",定義如下的類:- <?php
- /**
- * 這個文件處理文章的查詢,並提供文章
- */
- class News_Controller
- {
- /**
- * $template變量會保存與此控制器相關的"view(視圖)"的文件名,不包括.php後綴
- */
- public $template = 'news';
- /**
- * 此方法爲route.php默認調用
- *
- * @param array $getVars 傳入到index.php的GET變量數組
- */
- public function main(array $getVars)
- {
- //測試代碼,以後會刪除
- print "We are in news!";
- print '<br/>';
- $vars = print_r($getVars, TRUE);
- (
- "The following GET vars were passed to this controller:" .
- "<pre>".$vars."</pre>"
- );
- }
- }
<?php /** * 這個文件處理文章的查詢,並提供文章 */ class News_Controller { /** * $template變量會保存與此控制器相關的"view(視圖)"的文件名,不包括.php後綴 */ public $template = 'news'; /** * 此方法爲route.php默認調用 * * @param array $getVars 傳入到index.php的GET變量數組 */ public function main(array $getVars) { //測試代碼,以後會刪除 print "We are in news!"; print '<br/>'; $vars = print_r($getVars, TRUE); print ( "The following GET vars were passed to this controller:" . "<pre>".$vars."</pre>" ); } }
注意我們把route.php中的測試代碼複製過來了,並做了一些修改,我們把它放置在main函數裏。現在讓我們來修改route.php中的代碼:- <?php
- /**
- * 此文件會把所有的傳入參數分派到相應的控制器中
- */
- //獲取請求參數
- $request = $_SERVER['QUERY_STRING'];
- //解析請求頁面和其它GET變量
- $parsed = explode('&' , $request);
- //頁面是第一個元素
- $page = array_shift($parsed);
- //剩餘的爲GET變量,也把它們解析出來
- $getVars = array();
- foreach ($parsed as $argument)
- {
- //split GET vars along '=' symbol to separate variable, values
- list($variable , $value) = split('=' , $argument);
- $getVars[$variable] = $value;
- }
- //構成控制器文件路徑
- $target = SERVER_ROOT . '/controllers/' . $page . '.php';
- //引入目標文件
- if (file_exists($target))
- {
- include_once($target);
- //修改page變量,以符合命名規範(如$page="news",我們的約定是首字母大寫,控制器的話就在後面加上“<strong>_Controller”</strong>,即News_Controller)
- $class = ucfirst($page) . '_Controller';
- //初始化對應的類
- if (class_exists($class))
- {
- $controller = new $class;
- }
- else
- {
- //類的命名正確嗎?
- die('class does not exist!');
- }
- }
- else
- {
- //不能在controllers找到此文件
- die('page does not exist!');
- }
- //一但初始化了控制器,就調用它的默認函數main();
- //把get變量傳給它
- $controller->main($getVars);?>
<?php /** * 此文件會把所有的傳入參數分派到相應的控制器中 */ //獲取請求參數 $request = $_SERVER['QUERY_STRING']; //解析請求頁面和其它GET變量 $parsed = explode('&' , $request); //頁面是第一個元素 $page = array_shift($parsed); //剩餘的爲GET變量,也把它們解析出來 $getVars = array(); foreach ($parsed as $argument) { //split GET vars along '=' symbol to separate variable, values list($variable , $value) = split('=' , $argument); $getVars[$variable] = $value; } //構成控制器文件路徑 $target = SERVER_ROOT . '/controllers/' . $page . '.php'; //引入目標文件 if (file_exists($target)) { include_once($target); //修改page變量,以符合命名規範(如$page="news",我們的約定是首字母大寫,控制器的話就在後面加上“_Controller”,即News_Controller) $class = ucfirst($page) . '_Controller'; //初始化對應的類 if (class_exists($class)) { $controller = new $class; } else { //類的命名正確嗎? die('class does not exist!'); } } else { //不能在controllers找到此文件 die('page does not exist!'); } //一但初始化了控制器,就調用它的默認函數main(); //把get變量傳給它 $controller->main($getVars);?>
再次訪問http://你的域名.com/index.php?news&article=howtobuildaframework,你將會看到從News_Controller打印出來的信息。注意,我們現在用die()來處理錯誤,我們可以用其它更好的錯誤處理來規制它,但現在使用die()足夠了,試試訪問其它頁面如http://你的域名.com/index.php?books,你會看到"page does not exist!"錯誤。創建一個Model(模型)完善News_Controller。假設我們有一些新聞片段來供讀者閱讀,那麼就需要News_Controller這個控制器去調用一個模型來抓取相關的新聞片段,無論它們是存儲在數據庫還是文件裏。在models文件夾裏新建一個文件,“news.php”,代碼如下:
- <?php
- /**
- * 新聞模型爲新聞控制器做複雜的後臺操作
- */
- class News_Model
- {
- public function __construct()
- {
- print 'I am the news model';
- }
- }
<?php /** * 新聞模型爲新聞控制器做複雜的後臺操作 */ class News_Model { public function __construct() { print 'I am the news model'; } }
現在,我們需要對新聞控制器稍做一些更改,打開controllers裏的news.php,把News_Controller類的main函數的代碼改爲如下,這樣,我們就會在“News_Model”初始化時,看到打印在屏幕上的信息:
現在刷新頁面,你會看到:public function main(array $getVars) { $newsModel = new News_Model; }
等一下,這不是我們想要的結果!我們正試圖去加載一個不存在的類。那麼原因就是我們並沒有引入/models/news.php文件。爲了解決這個問題,讓們重新來看一下router.php,然後在它的頂部添加一些代碼:Fatal error: Class 'News_Model' not found in /var/www/mvc/controllers/news.php on line xx
- //當類初始化時,自動引入相關文件
- function __autoload($className)
- {
- //解析文件名,得到文件的存放路徑,如News_Model表示存放在models文件夾裏的news.php(這裏是作者的命名約定)
- list($filename , $suffix) = split('_' , $className);
- //構成文件路徑
- $file = SERVER_ROOT . '/models/' . strtolower($filename) . '.php';
- //獲取文件
- if (file_exists($file))
- {
- //引入文件
- include_once($file);
- }
- else
- {
- //文件不存在
- die("File '$filename' containing class '$className' not found.");
- }
- }
這個函數重載了PHP內置的autoload函數。當我們試圖去初始化一個不存在的類時,這個‘魔術方法’允許我們攔截php所執行的動作。通過使用__autoload函數,我們能夠告訴php尋找包含此類的文件的位置。假設你遵循了這篇文章中類和文件名的命名約定,那麼每當你初始化一個類時,你就不必手動去引入包含此類的文件了!//當類初始化時,自動引入相關文件 function __autoload($className) { //解析文件名,得到文件的存放路徑,如News_Model表示存放在models文件夾裏的news.php(這裏是作者的命名約定) list($filename , $suffix) = split('_' , $className); //構成文件路徑 $file = SERVER_ROOT . '/models/' . strtolower($filename) . '.php'; //獲取文件 if (file_exists($file)) { //引入文件 include_once($file); } else { //文件不存在 die("File '$filename' containing class '$className' not found."); } }
保存route.php,再刷新一次瀏覽器,你會看到:I am the news model
讓我們在新聞模型類裏定義一些函數來提供文章。現在,我們只簡單的定義了一個數組,並保存一些文章,然後提供一個函數,讓控制器從中根據標題獲取一篇文章。修改models/news.php:- <?php
- /**
- * 新聞模型爲新聞控制器做複雜的後臺操作
- *
- */
- class News_Model
- {
- /**
- * 文章數組. key爲文章標題, 值爲相應的
- * 文章。
- */
- private $articles = array
- (
- //文章1
- 'new' => array
- (
- 'title' => 'New Website' ,
- 'content' => 'Welcome to the site! We are glad to have you here.'
- )
- ,
- //2
- 'mvc' => array
- (
- 'title' => 'PHP MVC Frameworks are Awesome!' ,
- 'content' => 'It really is very easy. Take it from us!'
- )
- ,
- //3
- 'test' => array
- (
- 'title' => 'Testing' ,
- 'content' => 'This is just a measly test article.'
- )
- );
- public function __construct()
- {
- }
- /**
- * 根據標題獲取文章
- *
- * @param string $articleName
- *
- * @return array $article
- */
- public function get_article($articleName)
- {
- //從數組中獲取文章
- $article = $this->articles[$articleName];
- return $article;
- }
- }?>
<?php /** * 新聞模型爲新聞控制器做複雜的後臺操作 * */ class News_Model { /** * 文章數組. key爲文章標題, 值爲相應的 * 文章。 */ private $articles = array ( //文章1 'new' => array ( 'title' => 'New Website' , 'content' => 'Welcome to the site! We are glad to have you here.' ) , //2 'mvc' => array ( 'title' => 'PHP MVC Frameworks are Awesome!' , 'content' => 'It really is very easy. Take it from us!' ) , //3 'test' => array ( 'title' => 'Testing' , 'content' => 'This is just a measly test article.' ) ); public function __construct() { } /** * 根據標題獲取文章 * * @param string $articleName * * @return array $article */ public function get_article($articleName) { //從數組中獲取文章 $article = $this->articles[$articleName]; return $article; } }?>
- public function main(array $getVars)
- {
- $newsModel = new News_Model;
- //獲取一篇文章
- $article = $newsModel->get_article('test');
- print_r($article);
- }
現在我們並沒有考慮過濾用戶輸入的問題,因爲我們現在只是爲了儘快讓大家掌握PHP MVC的基本內容,所以我們現在不必太關心這些。public function main(array $getVars) { $newsModel = new News_Model; //獲取一篇文章 $article = $newsModel->get_article('test'); print_r($article); }
如果訪問如下網址:§ http://yourdomain.com/mvc/index.php?news&article=test
你會看到如下輸出:
Array ( [title] => Testing [content] => This is just a measly test article. )
創建視圖(VIEW)現在我們已經有控制器和模型了,只差一個視圖。視圖是表現層,它是你的應用中,與用戶接觸最頻繁的部分。之前我提到過,視圖是提供與業務邏輯分離的用戶接口,有很多方法可以做到這個。你可以使用模板引擎Smarty或其它類似的。你也可以寫一個自己的模板引擎,但那肯定是相當艱鉅的任務。最後,你可以使用原生php視圖。
對於目前來說,php視圖足夠了。這個就像以前php與html代碼混合編程一樣,但是有一點不同是,我們的業務邏輯已經和視圖分離了。看一下如下代碼:
- <html>
- <head></head>
- <body>
- <h1>Welcome to Our Website!</h1>
- <hr/>
- <h2>News</h2>
- <h4><?=$data['title'];?></h4>
- <p><?=$data['content'];?></p>
- </body>
- </html>
<html> <head></head> <body> <h1>Welcome to Our Website!</h1> <hr/> <h2>News</h2> <h4><?=$data['title'];?></h4> <p><?=$data['content'];?></p> </body> </html>
注意,嵌入的php標籤利用了PHP 快捷操作符。這樣就能夠把我們的內容直接輸出到HTML裏面了。在views文件夾裏新建一個文件“news.php”,把上述代碼拷貝進來。現在我們有了視圖文件,但是我們需要一個與視圖交互的方法。在models文件夾裏新建一個文件“view.php”,添加如下代碼:
- <?php
- /**
- * 在我們的MVC框架中,處理視圖的功能
- */
- class View_Model
- {
- /**
- * 保存賦給視圖模板的變量
- */
- private $data = array();
- /**
- * 保存視圖渲染狀態
- */
- private $render = FALSE;
- /**
- * 加載一個視圖模板
- */
- public function __construct($template)
- {
- //構成完整文件路徑
- $file = SERVER_ROOT . '/views/' . strtolower($template) . '.php';
- if (file_exists($file))
- {
- /**
- * 當模型對象銷燬時才能渲染視圖
- * 如果現在就渲染視圖,那麼我們就不能給視圖模板賦予變量
- * 所以此處先保存要渲染的視圖文件路徑
- */
- $this->render = $file;
- }
- }
- /**
- * 接受從控制器賦予的變量,並保存在data數組中
- *
- * @param $variable
- * @param $value
- */
- public function assign($variable , $value)
- {
- $this->data[$variable] = $value;
- }
- public function __destruct()
- {
- //把類中的data數組變爲該函數的局部變量,以方便在視圖模板中使用
- $data = $this->data;
- //渲染視圖
- include($this->render);
- }
- }
<?php /** * 在我們的MVC框架中,處理視圖的功能 */ class View_Model { /** * 保存賦給視圖模板的變量 */ private $data = array(); /** * 保存視圖渲染狀態 */ private $render = FALSE; /** * 加載一個視圖模板 */ public function __construct($template) { //構成完整文件路徑 $file = SERVER_ROOT . '/views/' . strtolower($template) . '.php'; if (file_exists($file)) { /** * 當模型對象銷燬時才能渲染視圖 * 如果現在就渲染視圖,那麼我們就不能給視圖模板賦予變量 * 所以此處先保存要渲染的視圖文件路徑 */ $this->render = $file; } } /** * 接受從控制器賦予的變量,並保存在data數組中 * * @param $variable * @param $value */ public function assign($variable , $value) { $this->data[$variable] = $value; } public function __destruct() { //把類中的data數組變爲該函數的局部變量,以方便在視圖模板中使用 $data = $this->data; //渲染視圖 include($this->render); } }
現在,最後一件要做的事就是從News_Controller里加載視圖。修改controllers/news.php:- <?php
- /*
- *這個文件處理文章的查詢,併產生新聞文章*
- */
- class News_Controller{
- /**
- * $template變量會保存與此控制器相關的"view(視圖)"的文件名,不包括.php後綴
- *
- */
- public $template = 'news';
- /**
- * 此方法爲route.php默認調用
- *
- * @param array $getVars 傳入到index.php的GET變量數組
- */
- public function main(array $getVars) {
- $newsModel = new News_Model;
- //獲取一片文章
- $article = $newsModel->get_article($getVars['article']);
- //創建一個視圖,並傳入該控制器的template變量
- $view = new View_Model($this->template);
- //把文章數據賦給視圖模板
- $view->assign('title' , $article['title']);
- $view->assign('content' , $article['content']);
- }
- }
- ?>
<?php /* *這個文件處理文章的查詢,併產生新聞文章* */ class News_Controller{ /** * $template變量會保存與此控制器相關的"view(視圖)"的文件名,不包括.php後綴 * */ public $template = 'news'; /** * 此方法爲route.php默認調用 * * @param array $getVars 傳入到index.php的GET變量數組 */ public function main(array $getVars) { $newsModel = new News_Model; //獲取一片文章 $article = $newsModel->get_article($getVars['article']); //創建一個視圖,並傳入該控制器的template變量 $view = new View_Model($this->template); //把文章數據賦給視圖模板 $view->assign('title' , $article['title']); $view->assign('content' , $article['content']); } } ?>
再加載頁面,你就能夠看到你的視圖模板中的變量,已經被正確的替換掉了。好了,你的簡單的MVC框架已經搭建好了,下面我會繼續講《開發自己PHP MVC框架(二)》