2021年5月28日 星期五

使用 PHP 實作 MVC 框架(一)

設定目標:
  • 使用 php 程式語言實作 MVC 框架!
    • Model (模組) : 處理大部分的商業邏輯和資料運算邏輯。
    • Controller (控制器) : 負責接收、回應使用者請求,以及準備資料、展示資料等工作。
    • View (檢視) : 負責整理好資料,通過HTML方式回應給使用者。
    • MVC 示意圖如下:
  • 先從定義文件與規範開始,再逐一進入MVC階段!

定義目錄架構與環境設定檔案!
  • 專案根目錄 helloMVC 下的目錄結構:
    helloMVC
       |-- app
       |    |-- Models
       |    |-- Views
       |    |-- Controlles
       |-- config
       |-- kernel
       |-- public
       |-- static
       |-- tmp
       |-- scripts
    
    • app :應用程式目錄
    • config :應用程式組態檔目錄
    • kernal :MVC 核心架構程式碼目錄
    • public :公開網頁檔案目錄,例如:index.php 等 MVC 框架程式進入點!
    • static :靜態網頁目錄,可放置 css 等檔案!
    • tmp :臨時放置檔案目錄
    • scripts : 可執行系統命令工具目錄
  • 撰寫程式風格要求:
    • 資料庫表格名稱:使用小寫的名詞
    • 模組名稱:使用字首小寫名詞 + Model 組合,亦名為「駝峰命名法」,例:carModel !
    • 控制器名稱:使用字首大寫名詞 + Controller 組合,亦名為「雙駝峰命名法」,例:CarController !
    • 核心程式名稱:使用字首大寫名詞,例:Route.php, Controller.php 等等 !
    • 檢視程式名稱:使用控制器分類名稱以及行為動詞,例: Car/move.php !
  • 設定網址目錄轉向!
    • 目的:將程式的入口單一化至 index.php
    • 靜態目錄下的檔案例外!
    • 以 Apache Web Server 為例,編寫 .htaccess , 放置在專案根目錄 helloMVC/public 下:
      #<IfModule mod_rewrite.c>
      # 開啟 Apache Web Server 目錄轉向功能
      RewriteEngine On
      
      # 網址請求路徑如果存在真實檔名或目錄,就可以直接使用
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      
      # 如果網址查看的檔案或目錄不存在,則重定向所有請求到 index.php
      RewriteRule ^(.*)$ index.php?url=$1 [PT,L]
      
      #</IfModule mod_rewrite.c>
      
      • 允許靜態檔案直接存取!
      • 不存在的檔案或是目錄,直接匯入 index.php 檔案,控制程式進出!
      • 可間接最佳化存取網址,有利於 SEO 的使用!
撰寫核心運作檔案!
  1. 撰寫 public 目錄下的 index.php 檔案 !
    <?php
    use kernel\Kernel;
    
    // 設定應用程式目錄為當前目錄
    define('APP_PATH', __DIR__.'../');
    
    // 開啟除錯模式
    define('APP_DEBUG', true);
    
    // 載入框架
    require(APP_PATH.'kernel/kernel.php');
    
    // 產生實例化物件
    (new Kernal())->run();
    
    PS: 不用寫結尾的 ?>
  2. 撰寫 kernel 目錄下的 kernel.php 檔案:
    <?php
    
    namespace kernel;
    use config\Config;
    use config\Router;
    
    require_once(APP_PATH.'config/config.php');
    require_once(APP_PATH.'config/router.php');
    class Kernel {
    
        protected $_config;
        protected $_router;
        
        public function __construct(){
        	$this->_config = new Config();
            $this->_router = new Router($_SERVER);
        }
    	public function run(){
        	spl_autoload_register(array($this, 'loadClass'));
            $this->unregisterGlobals();
            $this->_config->show();
            
            //由路由設定,取出需要使用的控制器
            include ('Router.php');
            $uri = $this->_router->run();
            $controller = 'App\\Controllers\\'.$uri[1];
            //找出控制器後,程式交給控制器執行
            if (!class_exists($controller)){
                exit($controller.'控制器不存在');
            } else {
                (new $controller())->run();
            }        
        }
        //自動加載類別
        public function loadClass($className){
        	$classMap = $this->classMap();
    
            if (isset($classMap[$className])){
                $file = $classMap[$className];
            } elseif (strpos($className, '\\') !== false){
                //包含 app 目錄下的文件檔案
                $file = APP_PATH.str_replace('\\','/',$className).'.php';
                if (!is_file($file)){
                    return ;
                }
            } else {
                return;
            }
            include $file;
        }
        //類別對應命名空間
        public function classMap(){
            return [
                'kernel\Controller' => CORE_PATH.'Controller.php',
                'kernel\Model' => CORE_PATH.'Model.php',
                'kernel\View' => CORE_PATH.'View.php',
                'kernel\Router' => CORE_PATH.'Router.php'
            ];
        }
        //取消全域自定義變數
        public 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]);
                        }
                    }
                }
            }
        }
    }
    
  3. 編寫 config 目錄下的 config.php 檔案:
    <?php
    namespace config;
    
    class Config{
        public function show(){
            $file = fopen("../config/.env",'rb');
            while ((! feof($file)) && ($line = fgets($file))){
                $line = trim($line);
                $info = explode('=',$line);
                if (empty($info[0])){
                    continue;
                }
                define($info[0],$info[1]);
            }
        }
    }
    
    
    PS: 一般程式常用的變數亦可在此設定!
  4. 撰寫環境設定檔,放在 config 目錄下,例:.env 檔案!
    DB_NAME=contact
    DB_USER=hello
    DB_PASSWORD=hello$1213
    DB_HOST=localhost
    
    APP_URL=http://localhost
    CORE_PATH=../kernel/
    
  5. 撰寫框架核心路由程式,放置於 config 目錄下,例:router.php
    <?php
    namespace config;
    
    class Router {
    
        public $request;
        public static $routes = array();
    
        //建構子必需要剖析網址 URI 的部份
        public function __construct(array $request){
            $this->request = basename($request['REQUEST_URI']);
        }
        
        public static function addRoute(string $uri, $controller) : void {
            self::$routes[$uri] = $controller;
        }
    
        public function hasRoute(string $uri) : bool {
            $uri = '/'.$uri;
            return array_key_exists($uri, self::$routes);
        }
        
        public function run(){
            //分析參數
            $uri = array();
            $uri = explode('?',$this->request);
            if (!isset($uri[1])){
                $uri[1] = "";
            }
            if ($this->hasRoute($uri[0])){
                return array($uri[1],(self::$routes['/'.$uri[0]]));
            }
        }
    }
    
  6. 撰寫路由的路徑設定檔,放置於 kernel 目錄下,例:Router.php
    <?php
    use config\Router;
    
    Router::addRoute('/login',LoginController::class);
    Router::addRoute('/',IndexController::class);
    
  7. 撰寫框架核心控制器抽象類別,放置於 kernel 目錄下,例:Controller.php
    <?php
    namespace kernel;
    
    abstract class Controller {
        
        abstract public function run();
        
    }
    
  8. 撰寫一個測試用的 IndexController 控制器,放置於 app/Controllers 目錄下,例:IndexController.php
    <?php
    namespace App\Controllers;
    
    use kernel\Controller;
    use App\Models\indexModel;
    
    class IndexController extends Controller {
    
        public function run(){
        	$user = new indexModule();
            $result = $user->printName();
            print "Hello ,".$result;
        }
    }
    
  9. 撰寫框架核心模組抽象類別,放置於 kernel 目錄下,例:Model.php
    <?php
    namespace kernel;
    
    abstract class Model {
        
        abstract public function __construct();
        abstract public function __destruct();
    }
    
  10. 撰寫一個測試用的模組程式,放置於 app/Models 目錄下,例:indexModel.php
    <?php
    namespace App\Models;
    
    use kernel\Model;
    
    class indexModel extends Model {
        
        protected $name;
        
        public function __construct(){
        	$this->name = "Peter";
        }
        public function printName(){
        	return $this->name;
        }
        public function __destruct(){
        }
    }
    
    PS: 檢查是否成功,可使用 http://helloMVC 檢查!

參考文獻