如果你有使用其它 PHP 框架的習慣,肯定會覺得我們的框架還少了點什麼。
到目前為止,我們的框架還沒有很複雜的邏輯,如果未來有需要加入比較複雜的邏輯時,我們可能會被迫將邏輯寫在模板中。
這顯然不是個好主意,寫過原生 PHP 的人大多都經歷過 PHP 程式碼與 HTML 混雜在一起時的夢魘。
有鑑於此,我們需要再建立一個新的層(layer),將我們的邏輯放在裡面,通常這個層被稱為控制器(Controller)。
目前我們的框架還很小,而且只有一個邏輯:渲染模板。
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
$request = Request::createFromGlobals();
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', ['name' => 'World']));
$routers->add('bye', new Route('/bye'));
$context = (new RequestContext())->fromRequest($request);
$matcher = new UrlMatcher($routers, $context);
function render(Request $request): Response
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include __DIR__ . "/../Bach/templates/$_route.php";
return new Response(ob_get_clean());
}
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func('render', $request);
} catch (ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
$response->prepare($request)->send();
我們嘗試將渲染模板的邏輯分離出來建立一個函式,命名為 render()
,並且利用 Request
實體預留的 attributes
屬性存放 $matcher
解析出來的變數。
既然可以利用 call_user_func()
動態地呼叫函式,我們何不試著把函式名稱放在 Route
實體中?
// rafax/public/index.php
<?php
// ...
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', [
'name' => 'World',
'_controller' => 'render',
]));
$routers->add('bye', new Route('/bye', [
'_controller' => 'render',
]));
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func($request->attributes->get('_controller'), $request)
}
// ...
除了在 Route
實例中的 _controller
屬性使用字串外,你可能會更偏好使用一個 callback
,如此一來能對 Request
與 Response
做更詳細的操作。
// rafax/public/index.php
<?php
// ...
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', [
'name' => 'World',
'_controller' => function (Request $request) {
return render($request);
},
]));
$routers->add('bye', new Route('/bye', [
'_controller' => function (Request $request) {
// 在 $request 中加入 $foo = 'bar' 的變數
$request->attributes->set('foo', 'bar');
$response = render($request);
// 變更 $response 的 Header
$response->headers->set('Content-Type', 'text/plain');
return $response;
},
]));
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func($request->attributes->get('_controller'), $request)
}
// ...
我們把渲染層邏輯成功抽離後,再嘗試著加入一個新的邏輯:取得現在的 unix timestamp
並且轉為日期字串格式。
// rafax/public/index.php
<?php
// ...
$routers = new RouteCollection();
$routers->add('time', new Route('/time', [
'_controller' => function (Request $request) {
$time = new DateTime();
$response = new Response(
"Timestamp: {$time->getTimestamp()}\nDatetime: {$time->format('Y-m-d H:i:s')}"
);
$response->headers->set('Content-Type', 'text/plain');
return $response;
},
]));
// ...
雖然我們新加了一層 Controller,框架的功能變得比以往強大,但同時也變得比以往來得凌亂。
此時我們就要靠著重構來強化框架的系統性。
// rafax/Bach/app/Http/Controllers/HelloController
<?php
namespace Bach\Http\Controllers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function render(Request $request): string
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include __DIR__ . "/../../../templates/$_route.php";
return new Response(ob_get_clean());
}
}
// rafax/public/index.php
<?php
// ...
$routers->add('hello', new Route('/hello/{name}', [
'name' => 'World',
'_controller' => [new HelloController(), 'render'],
]));
$routers->add('bye', new Route('/bye', [
'_controller' => [new HelloController(), 'render'],
]));
// ...
// rafax/Bach/app/Http/Controllers/TimeController
<?php
namespace Bach\Http\Controllers;
use Datetime;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TimeController
{
public function time(Request $request): Response
{
$time = new DateTime();
$response = new Response(
"Timestamp: {$time->getTimestamp()}\nDatetime: {$time->format('Y-m-d H:i:s')}"
);
$response->headers->set('Content-Type', 'text/plain');
return $response;
}
}
// rafax/public/index.php
<?php
// ...
$routers->add('time', new Route('/time', [
'_controller' => [new TimeController(), 'time'],
]));
// ...
經過重構後,會發現我們的框架整齊多了,而且易於閱讀。
其實截至目前為止,我們的框架已經可以建立簡單的網站,同時也是個簡易的 MVC 架構--請原諒我這麼形容,雖然 MVC 這個詞不太精準,但畢竟它對於推廣框架有一定程度的幫助。