為什麼要構建自己的 PHP 框架?
現在的 PHP 框架很多,當然不止 PHP ,即使是其他程式語言也有很多框架,這篇文章講 PHP 框架構建是因為我對 PHP 的生態最為熟悉,但這個方法同樣也適用於其他程式語言框架的構建。
框架是為了提升我們的應用開發效率,市面上有很多開源免費的框架給我們使用,我們儘可以拿來用,為什麼還要自己構建一個自己的框架呢?原因就在於市面上的開源框架,是給大部分人用的,給通用專案用的,作為框架的開發者是不知道自己的框架使用者的具體業務的,所以開源框架一定是滿足大部分人的需求,而且力求能夠為開發者提供所有可能用到的功能。
但是對於一個商業專案或者是一個你自己要做的專案也許只能用到框架的很少一部分功能,或者是框架給你提供的東西並不是最符合你自己的需求的,你使用了框架的一部分功能,另一部分根本沒用,這樣使用框架首先是效能上的損失,一些你根本用不到的功能卻要降低你應用的效能顯然不合適的。再就是也許框架提供的功能不是你想要的,或者這個功能這個框架提供的並不是符合你需求的,又或者要使用這部分功能必須按照框架開發者制定的規範來使用,這個規範並符合你的開發哲學。
從哪開始?
各種現代程式語言都有自己的包管理工具,PHP 就是 composer ,利用它我們就可以構建屬於自己的框架了,並能很好的組織我們的框架。
怎麼開始?
我們該怎麼開始構建我們自己的框架呢?從零開始嗎?這個問題沒有標準答案,如果你要做的專案要求很嚴格,從底層開始就要保證專案架構的最穩定可控,那麼建議你從零開始。如果要求不是非常嚴格那麼我們就從那些開發一個應用最基本需要的功能開始,這樣的功能誰提供呢?PHP 有很多微框架,這些框架提供開發一個應用最基礎的功能,我們可以從這裡開始。
首先我們通過 composer init
初始化一個專案:
{
"name": "dongm2ez/m2ez-framework",
"description": "a dongm2ez's framework",
"keywords": ["framework", "m2ez-framework"],
"license": "MIT",
"authors": [
{
"name": "dongm2ez",
"email": "dongm2ez@163.com"
}
],
"require": {}
}
這就是我得到的一個 composer.json
的描述檔案,現在我們就從這裡開始。我的目標是構建一個最符合我開發習慣的框架,讓我的開發效率最高。
我選擇 Slim 框架作為我的框架基礎框架,這是一個微框架,我喜歡它,它足夠簡單,提供了 web 開發 和 API 開發最基礎的功能,而且還有一個原因開發這個框架的作者寫了一本名為《Modern PHP》的書,這本書顛覆了我對 PHP 這個語言的認知,開始喜歡並樂於使用它。
在引入這個框架之前我還要對 PHP 版本做個限制,從我使用 PHP 從 5.2 開始到現在,PHP 已經發展到 PHP 7.2 了,但是我不想再去使用低版本的 PHP,一個是 PHP 低版本馬上將失去官方的支付,另一個是一些 PHP 的新特性我不能使用,而且低版本的效能也是不好的。所以我要將我的框架限制在 PHP 7.0 以上,同時我希望我的框架對中文有更好的支援。
那麼我將更新我的框架 composer.json
檔案:
{
"name": "dongm2ez/m2ez-framework",
"description": "a dongm2ez's framework",
"keywords": ["framework", "m2ez-framework"],
"license": "MIT",
"authors": [
{
"name": "dongm2ez",
"email": "dongm2ez@163.com"
}
],
"require": {
"php": ">=7.0.0",
"ext-mbstring": "*",
"slim/slim": "^3.0"
}
}
這樣我就獲得了一個最基礎的我的框架版本,但是我還沒完成,因為我們沒有定義我的框架目錄結構。我覺得 laravel 框架的目錄劃分是挺讓我喜歡的,但我又不完全喜歡 laravel 的目錄結構,我需要對它進行改造。
├── app
│ ├── Helpers.php
│ ├── Http
│ │ └── Controllers
│ └── Models
├── composer.json
└── tests
我這樣設定我的目錄結構,並更新我的 composer.json
:
{
"name": "dongm2ez/m2ez-framework",
"description": "a dongm2ez's framework",
"keywords": ["framework", "m2ez-framework"],
"license": "MIT",
"type": "project",
"authors": [
{
"name": "dongm2ez",
"email": "dongm2ez@163.com"
}
],
"require": {
"php": ">=7.0.0",
"slim/slim": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "~6.0"
},
"autoload": {
"classmap": [
"app/Models"
],
"psr-4": {
"App\\": "app/"
},
"files": [
"app/Helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
這樣的框架可以訪問嗎,顯然是不行的,我們還要加一些東西讓我們的框架真正可以跑起來,然後在來迭代它。
├── app
│ ├── Helpers.php
│ ├── Http
│ │ └── Controllers
│ └── Models
├── bootstrap
│ ├── app.php
│ └── autoload.php
├── composer.json
├── config
├── public
│ └── index.php
├── routers
└── tests
我們將目錄結構改造成這樣,並編寫一些啟動框架的程式碼到相應的檔案
// public/index.php
<?php
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->run();
// bootstrap/app.php
<?php
$app = new \Slim\App;
return $app;
// bootstrap/autoload.php
<?php
define('M2EZ_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
然後執行 composer install
安裝框架的依賴包,安裝完成後我們的目錄中就會多出一個 vendor
的目錄和 composer.lock
的檔案,此時執行 php -S 0.0.0.0:8080 -t public public/index.php
利用 PHP 自帶的 web 伺服器進行測試,為了這個命令更簡單使用,我們可以將這個命令加到 composer.json
的 script
中。
此時訪問 127.0.0.1:8080
或 localhost:8080
就可以看到如下的頁面:
這說明框架正確啟動了,那麼我們怎麼確定框架工作正常呢,這裡有個簡單方法:
<?php
use Slim\Http\Request;
use Slim\Http\Response;
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->get('/hello/{name}', function (Request $request, Response $response) {
$name = $request->getAttribute('name');
$response->getBody()->write("Hello, $name");
return $response;
});
$app->run();
對 public/index.php
的程式碼進行修改,此時訪問 http://localhost:8080/hello/dongm2ez
,那麼我們就會看到:
測試是成功了,但是我們不能把路由和邏輯都寫到 index.php
檔案裡,因此我們需要程式碼更好的組織。要讓我們的目錄規劃發揮正在的作用。
為了單獨管理路由,我將路由單獨寫在 routers
資料夾中,在資料夾中我們新建兩個 PHP 指令碼檔案,然後在 public/index.php
中加入兩行程式碼:
<?php
require __DIR__ . '/../bootstrap/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
require __DIR__ . '/../routers/web.php';
require __DIR__ . '/../routers/api.php';
$app->run();
變成這樣,這樣我就可以單獨管理 API 和 WEB 專案的路由了,如果有其他路由就也可以 require 更多路由。
<?php
$app->get('/', '\App\Http\Controllers\WelcomeController:index');
而我們的控制器長什麼樣呢,是這個樣子的:
<?php
namespace App\Http\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
class WelcomeController extends Controller
{
public function index(Request $request, Response $response)
{
$response->getBody()->write("Hello, world");
return $response;
}
}
我們知道在現代化的框架中,容器會讓我們很方便,我們的基礎框架 Slim 提供一個容器的實現,當然你也可以使用其他的第三方的,那麼這顯然是我們想要的結果,不是隻能使用框架提供的,我們可以隨時換掉框架的功能,換成我們想要的同樣功能元件。
使用也很簡單,在 app.php
檔案中初始化 Slim 框架時將容器例項傳遞給它就可以了。
<?php
$container = new \Slim\Container;
$app = new \Slim\App($container);
return $app;
還記得上面那個控制器繼承的基類控制器嗎,那也是我自己寫的,裡面可以做一些所有控制器都有可能用的的操作封裝。比如我為了更方便的使用容器,我在基類裡初始化了一個容器例項。
<?php
namespace App\Http\Controllers;
use Interop\Container\ContainerInterface;
abstract class Controller
{
protected $ci;
public function __construct(ContainerInterface $ci)
{
$this->ci = $ci;
}
}
現在我已經有了 路由功能,有了控制器功能,還有請求響應的操作,那麼作為一個完整的框架那麼必須有訪問資料庫的方法。
我很喜歡 Laravel 提供的資料庫 ORM 元件,那麼我就決定使用它了,執行 composer require illuminate/database "~5.5"
,我選擇了最新的 Laravel 長期支援版 ORM 。
我們需要此時在 config
資料夾中新新增一個檔案 databases.php
檔案:
<?php
return [
'settings' => [
'db' => [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'database',
'username' => 'user',
'password' => 'password',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]
],
];
然後修改 public/index.php
:
<?php
require __DIR__ . '/../bootstrap/autoload.php';
$config = require __DIR__ . '/../config/databases.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
require __DIR__ . '/../routers/web.php';
require __DIR__ . '/../routers/api.php';
$app->run();
修改 bootstrap/app.php
:
<?php
$container = new \Slim\Container($config);
$app = new \Slim\App($container);
// Service factory for the ORM
$container['db'] = function ($container) {
$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection($container['settings']['db']);
$capsule->setAsGlobal();
$capsule->bootEloquent();
return $capsule;
};
return $app;
然後我們就可以在控制器中使用 ORM 功能了。
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\DB;
use Interop\Container\ContainerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
class WelcomeController extends Controller
{
public function index(Request $request, Response $response)
{
/** @var DatabaseManager $db */
$db = $this->ci->get('db');
$user = $db->table("user")->first();
var_dump($user);
$response->getBody()->write("Hello, world");
return $response;
}
}
那麼此時這個框架已經是一個可以開發 API 功能的框架了,如果要開發 Web 站我可能還需要加入渲染模板元件,無論是 twig
、Smarty
、Haml
還是 Blade
,全都看你的喜好了。當然我覺得我做到這裡就可以了,夠我用了,因為對於前端我更喜歡用 React
或者是 Vue
去實現它。
需要更簡便的操作 session
、cookie
那麼我們也可以新增相應的元件,各種已經有的框架了都提供這樣的元件,看看你更喜歡哪一個了,現在你的框架你做主,你想新增什麼就可以新增什麼元件,經過這樣的定製的框架一定是最符合你開發需求的。
我這裡只是對已有的元件進行了配置組裝,一旦哪天你發現所有的開源元件都滿足不了你的需求的時候,因為你對你的框架了解,你可以自己造個輪子給自己的框架用,如果你寫的好那麼你也會創造出一個極好用的框架,現在最流行的 PHP 框架,你可以看看它的 composer.json
檔案,它就是在前人的基礎上進行開發維護的,已經有的功能他拿來直接用,覺得別人做的不完善的地方自己造一個輪子給大家用。
而且我這裡也沒有用到太多的設計模式,你還可以改造你的框架,利用PHP的魔術方法,反射,SPL 等等讓你的框架更好,更容易擴充套件,更容易配置。
總結
框架很神祕嗎?看過這篇文章我相信你不會這樣覺得了。
造一個框架很難嗎,是的很難,因為從 0 到 1 任何事都難,但是我們現在還需要從 0 到 1 嗎,基本不需要了!站在巨人身上做事更容易,而且要記住,任何事只有行動起來你就會發現嘗試比躊躇不前更好,從小處開始,做小事,有一天這個小事就變成了大事。不積跬步無以至千里。
Laravel 為什麼流行,因為作者本是一名 .net 開發者,在使用 CI 框架時萌生了想法要做一個更簡潔、靈活的框架,他的思想真的很先進嗎,不一定的,其他開發語言早就有了 Laravel 中的功能,它只是在 PHP 中實現了它們。
以上例子其實告訴我們,不要給自己貼標籤,人生不設限,你不是 PHP 程式設計師,你就是開發者,任何開發相關的東西我們都該去了解和掌握,標籤只能別人給你貼,不要自己給自己貼。
最後,這份程式碼我已經上傳到 GitHub ,如果你有興趣可以 fork 並完善它,開發配置一個自己的框架,以你的需求為出發,選擇你最喜歡的技術和元件。