手作:用纯 PHP 打造 MVC 框架!?

前言

现今相当多的团队都会使用框架开发,原因不外乎是因为框架提供了很多好用的工具可以加速开发,也能确保团队可以在同一个规範下共同协作。当然还有许多其他的优点。

本文旨在介绍自製简易框架的基本逻辑。虽然实务上,基本上不太可能会使用自製的框架来开发。然而,对于新手开发者而言,时常只是使用框架预先包装好的工具开发,却不明白背后的运作逻辑,要做深入的客製化更是难以执行。故本文透过自製简易框架,来理解框架的基本运作逻辑。

本文介绍的自製框架採取 MVC 的架构,谈及的观念与程式码绝大部分来自 Udemy 上的课程 Object Oriented PHP & MVC。本文通篇以解释观念为主,所以对部分程式码进行简化,甚至有些地方只有写注解没有撰写实际上可以运行的程式。

资料夹结构

让我们从一个空的资料夹开始!我们首先建立以下的资料夹和档案。

自製MVC框架/├── app/│   ├── config/│   │   └── config.php│   ├── libraries/│   │   ├── Core.php│   │   ├── Database.php│   │   └── Controller.php│   ├── models/│   ├── views/│   │   ├── inc/│   │   │   ├── header.php│   │   │   └── footer.php│   │   └── pages/│   │       └── index.php│   ├── controllers/│   │   └── Pages.php(预设的 Controller)│   └── bootstrap.php│   └── public/    ├── index.php    ├── css/    │   └── style.css(空档案)    ├── js/    │   └── main.js(空档案)    └── img/

网站入口点

当使用者需要输入的网址为:127.0.0.1/public 时,网站预设执行的位置是 public/index.php。它的程式码只有短短两行,只做两件事,引入 bootstrap.php,然后将 Core 物件实例化(下一节会介绍到 Core)。

public/index.php

<?phprequire_once '../app/bootstrap.php';$init = new Core();

bootstrap.php 被引用进来之后,也只做两件事,第一是引入 config.php 好让程式可以取用里面的全域变数,里面包含一些关于资料库环境变数或资源引用的路径变数等等;第二是引用所有 libraries 里面的档案(下一节会介绍到 libraries)。

app/bootstrap.php

<?phprequire_once 'config/config.php';spl_autoload_register(function($className){    require_once 'libraries/' . $className . '.php';});

app/config/config.php

<?php// Database 的参数,以下为範例define('DB_HOST', '127.0.0.1');define('DB_NAME', 'default');define('DB_USER', 'default');define('DB_PASS', 'secret');// App 根目录,这是引入 app 资料夹里的资源用的define('APPROOT', dirname(dirname(__FILE__)) . '/');// URL 根目录,这是引入 public 资料夹里的资源,或是页面跳转时用的define('URLROOT', 'http://localhost:8000/public/');// 网站名称define('SITENAME', '自製 MVC 框架');

libraries

libraries 是整个框架的核心,共有三支档案:Core.php, Database.php, Controller.php。

Core.php 会在所有 request 发生的时候被实例化,然后进行路由处理。

Core 物件被实例化的过程中,简单来说做了以下几件事:

物件含有三个属性,分别代表「Controller」、「方法」、「参数」,预设的值为「'Pages'」、「'index'」、「空阵列」。对于网址做处理。比如当使用者输入 127.0.0.1/public?url=posts/show/1 时,会解析它然后得到一个 $url 阵列,它的值为 ['posts', 'show', 1]。若使用者输入 127.0.0.1/public,则得到 []。对于 $url 的元素做匹配。阵列中第一个值对应的是 Controller,第二个对应的是该 Controller 里的方法,第三个(或有更多)则对应的是该 Controller 里的该方法要传入的参数。执行!以 ['posts', 'show', 1] 为例,若 Controller Posts.php 存在,且含有一个名为 show 的方法,则会带入参数 1 并执行它。以 [] 为例,则执行预设的 Controller Pages 物件中预设的 index 方法,并且不带参数。

app/libraries/Core.php

<?phpclass Core{    // 预设 Controller 为 Pages    protected $currentController = 'Pages';    // 预设方法为 index    protected $currentMethod = 'index';    // 预设参数为空    protected $params = [];    public function __construct()    {        // 呼叫 getUrl() 取得 $url 阵列        // 将 $url[0] 视为 Controller 的名称        // 检查 $url[0] 是否有对应的 Controller ,即是否存在 $url[0].php 的档案        if(存在)            $currentController = $url[0];        // 引入 Controller        // 实例化 Controller        // $url[1] 视为 Controller 中的方法        // 所以先要检查是否有值,若有,检查该值是否有对应的方法        if(isset($url[1]))            if(method_exists($this->currentController, $url[1]))                $this->currentMethod = $url[1];        // $url 阵列中的第三个值开始,视为带入方法中的参数        // 用 $params 阵列储存所有剩下的值        // 最后透过呼叫 callback 来执行方法        call_user_func_array([$this->currentController, $this->currentMethod], $this->params);    }    public function getUrl()    {        // 从 public?url= 后开始,将 $url 按 / 切分,转换成阵列并回传        // 例如: 使用者输入 127.0.0.1/public?url=posts/show/1        // 则回传 $url 的值为 ['posts', 'show', 1]        // 它将在 __construct() 中依序被解析成 Controller, 方法, 参数    }}

Database.php 比较单纯。简述如下:

Database 物件被实例化时,会启动建构子,透过 PDO 确定成功连线并将之实例化。提供一些方法供 Model 使用,包含:query() 可以下查询语法,作为 prepare statementbind() 绑定变数execute() 执行该 prepare statementgetAll() 取得全部资料getSingle() 取得单笔资料getRowCount() 取得笔数

app/libraries/Database.php

<?phpclass Database{    // 设定资料库的常数来自于 config/config.php    private $host = DB_HOST;    private $dbname = DB_NAME;    private $user = DB_USER;    private $pass = DB_PASS;    // 定义一些操作 Database 的变数,例如:    private $dbh;    private $stmt;    private $error;    public function __construct()    {        // 透过 PDO 建立资料库连线        // 实例化 PDO    }    // Prepare statement with query    public function query($query){...}    // Bind values    public function bind($param, $value, $type = null){...}    // 执行 prepared statement    public function execute(){...}    // 以下是 Model 可以操作资料库的几个预设方法    // 可以自行定义更多需要的或常用的        // 取得资料表的所有资料    public function getAll(){...}    // 取得资料表的单一笔资料    public function getSingle(){...}    // 取得资料表中资料的笔数    public function getRowCount(){...}}

最后是 Controller.php。它只提供两个方法,分别用来载入 Model 和载入 View。

所有其他的 Controller 都要继承 Controller.php。这让我们在自定义的 Controller 中可以轻鬆的建立 Model 物件操作对应的资料库,并且将回传的值(如果有的话)包进阵列塞到 view 里面呈现在网页上。

app/libraries/Controller.php

<?phpclass Controller{    // 载入 model    public function model($model)    {        require_once '../app/models/' . $model . '.php';        return new $model();    }    // 载入 view    // 其中 view 可能有需要从 Controller 带过去的资料,故多了 $data 阵列作为第二个参数    public function view($view, array $data = [])    {        // 如果档案存在就引入它        if(file_exists('../app/views/' . $view . '.php')){            require_once '../app/views/' . $view . '.php';        } else {            die('View does not exist');        }    }}

views

我们在 views 里面建立一个 inc 的资料夹(include 的缩写),并且在里面建立 header.php 和 footer.php 两支档案,来定义好基本 html 架构和引用 css, js 资源。所有其他的 view 都将引入这两支档案,来减少撰写重複的程式码。

app/views/inc/header.php

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <link rel="stylesheet" href="<?php echo URLROOT; ?>/css/style.css">    <title><?php echo SITENAME; ?></title></head><body>

app/views/inc/footer.php

    <script src="<?php echo URLROOT; ?>/js/main.js"></script></body></html>

预设的 Controller 及 view

还记得我们在 app/libraries/Core.php 中定义预设 Controller 为 Pages,且预设方法为 index 吗?所以我们要预先定义好 app/controllers/Pages.php 这支档案,还有 app/views/pages/index.php 页面。

app/controllers/Pages.php

<?phpClass Pages extends Controller{    public function __construct()    {        // 当 Controller 需要操作资料库时,这里可以实例化该 Model。        // 不过这里我们只是要单纯引入 view,所以 __construct 里不需要撰写任何程式码。    }    public function index()    {        // 这里可以引入页面,我们即将在 views 资料夹底下建立一个 pages/index.php 的档案,故可以先写好以下的程式码:        $this->view('pages/index');    }}

在预设的页面里,我们单纯引入上一节的 header.php 跟 footer.php,然后在画面上印出 HELLO WORLD!。

app/views/pages/index.php

<?php require APPROOT . 'views/inc/header.php'; ?><h1>HELLO WORLD!</h1><?php require APPROOT . 'views/inc/footer.php'; ?>

运作流程

以 127.0.0.1/public?url=pages/index 为例,运作流程如下:

使用者输入 127.0.0.1/public?url=pages/index。网站入口点 index.php 被触发, bootstrap.php 被载入,并将 Core 物件实例化。在 Core 物件被实例化时,建构子会先呼叫 getUrl(),pages/index 被解析成 [pages, index],并赋值给 $url。$url[0] 会被当作 Controller 的名称,这里对应到的是 Pages.php,将会实例化 Pages 物件。$url[1] 则是对应到 Pages 物件的 index 方法。所以 Controller Pages 的 index 方法会被执行。在 index 方法中,会呼叫从 Controller.php 继承来的 view 方法,第一个参数填入的是欲呈现的 view 名称,在这边是 pages/index,因为没有需传入的资料,所以不需要第二个参数。View pages/index.php 被引用进来,呈现 HELLO WORLD! 的画面在浏览器上。

基于框架上开发

到这边,我们已经完成框架了。接下来说明如何基于这个我们自製的框架进行开发。

假设我们要开发一个可以发文的相关功能,包含查看全部、新增、查看一笔、修改、删除等五个功能。

这个框架符合 MVC 的架构,而且有发文的相关需求肯定需要和资料库沟通,所以我们可以确定需要建立的档案将会包含 Model、View、Controller。

我们先来谈谈 Model。这边我们建立一个 Post.php,当 Post 物件被实例化时,会透过建构子将 Database 物件实例化,也就是说,它将自动连线完毕并且能够使用我们刚刚在 Database.php 里面定义的那些可以操作资料库的基本方法!

接着我们在 Post 物件里继续撰写需要用到的方法,包含「取得所有文章」、「发布新文章」、「取得特定一则文章」、「更新文章」、「删除文章」。这些方法都是根据基本方法作延伸的。

app/models/Post.php

<?phpclass Post{    private $db;    // 在建构子将 Database 物件实例化    public function __construct()    {        $this->db = new Database;    }    // 取得所有文章    public function getPosts()    {        $query = 'SELECT ...';        $this->db->query($query);        $results = $this->db->getAll();                return $results;    }    // 发布新文章    public function storePost($data)    {        $query = 'INSERT ...';        $this->db->query($query);        $this->db->bind('title', $data['title']);        $this->db->bind('body', $data['body']);        if($this->db->execute()){            return true;        } else{            return false;        }    }    // 取得特定一则文章    public function getPostById($id)    {        $query = 'SELECT ...';        $this->db->query($query);        $this->db->bind('id', $id);        $result = $this->db->getSingle();                return $result;    }    // 更新文章    public function updatePost($data)    {        $query = 'UPDATE ...';        $this->db->query($query);        $this->db->bind('id', $data['id']);        $this->db->bind('title', $data['title']);        $this->db->bind('body', $data['body']);                if($this->db->execute()){            return true;        } else{            return false;        }    }    // 删除文章    public function deletePost($id){        $query = 'DELETE ...';        $this->db->query($query);        $this->db->bind('id', $id);                if($this->db->execute()){            return true;        } else{            return false;        }    }}

接着是 Controller。这次我们需要操作资料库,所以在建构子中呼叫 model('Post') 并赋值给 postModel。这个方法来自于它继承的 Controller.php,用意是将 Model Post 实例化。又因为上面我们知道 Post 被实例化时会将 Database 实例化,且拥有五个操作文章的方法。所以接下来我们就可以使用 postModel 执行这些方法。

再来,我们再增添五个方法:「index」、「create」、「show」、「edit」、「delete」。在这些方法中透过 postModel 对资料库做操作,操作完毕后呼叫 view() 来呈现画面。若有回传值则将回传值塞进 view() 的第二个参数,若无则仅需一个参数。稍微不同的是,由于 delete() 的目的是要删除某一笔文章,所以没有对应的 view,而是重新导向回 posts/index。又因为我们在 Core.php 里定义预设的方法为 index,所以仅需重新导向回 posts 即可。

app/controllers/Posts.php

<?phpclass Posts extends Controller{    // 在建构子中将 Post 物件(Model)实例化    public function __construct()    {        $this->postModel = $this->model('Post');    }    // 取得所有文章    public function index(){        $posts = $this->postModel->getPosts();        $data = [            'posts' => $posts        ];                $this->view('posts/index', $data);    }    // 发布新文章    public function create(){        // 注:基于输入值得验证及安全性,需要对使用者的 post 资料做处理。        // 但是这里省略上述步骤,以观念解释为主。        $data = [            'title' => $_POST['title'],            'body' => $_POST['body'],        ];        $this->view('posts/create', $data);    }    // 取得特定一则文章    public function show($id)    {        $post = $this->postModel->getPostById($id);        $data = [            'post' => $post,            'user' => $user        ];        $this->view('posts/show', $data);    }    // 更新文章    public function edit($id){        // 注:这里跟新增文章相当类似        // 一样省略验证与消毒,以观念解释为主。        $data = [            'title' => trim($_POST['title']),            'body' => trim($_POST['body']),        ];        $this->view('posts/edit', $data);    }    // 删除文章    public function delete($id)    {        // 注:这里一样省略验证与消毒,以观念解释为主。        $this->postModel->deletePost($id)        // 这边预先写了一个全域函式,可以重新导向        redirect('posts');    }}

最后是在 views 要加入几个对应的档案。因为删除并没有对应的页面,所以只需要新增前面四个页面。

app/views/posts/index.php
app/views/posts/create.php
app/views/posts/show.php
app/views/posts/edit.php

以上,我们完成发文相关功能的实作。我们最后再举一个例子帮助大家複习整个运作流程。

使用者输入 127.0.0.1/public?url=posts。网站入口点 index.php 被触发, bootstrap.php 被载入,并将 Core 物件实例化。Core 物件将会解析路由,取得阵列 ['posts']。因为 posts 存在,所以会载入 Controller Posts.php。Posts 物件被实例化时触发建构子,呼叫从 Controller.php 继承来的 model 方法,取得可以操作资料库的 postModel。由于 阵列 ['posts'] 没有提供方法和参数,所以使用预设的 index 方法和空阵列作为参数。Controller Posts 的 index 方法会被执行。在 index 方法中,postModel 将所有资料取出包进阵列,然后呼叫从 Controller.php 继承来的 view 方法,第一个参数填入的是欲呈现的 view 名称,在这边是 posts/index,第二个参数则是方才取出的资料阵列。View posts/index.php 将资料阵列适当的填入相应的 html 位置,呈现予使用者浏览。

参考资源

本文介绍的观念与程式码绝大部分来自 Udemy 上的课程。以下附上课程连结:
Object Oriented PHP & MVC


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章