樣板
敏銳的讀者可能已經注意到,我們的框架硬式編碼了特定「程式碼」(樣板)的執行方式。對於像我們目前建立的簡單頁面來說,這不是問題,但如果您想要加入更多邏輯,您將被迫將邏輯放入樣板本身,這可能不是一個好主意,特別是如果您仍然將關注點分離原則放在心上的話。
讓我們透過新增一個新的層級來將樣板程式碼與邏輯分離:控制器:控制器的任務是根據客戶端請求傳達的資訊產生回應。
變更框架的樣板渲染部分,使其讀取如下
1 2 3 4 5 6 7 8 9 10 11
// example.com/web/front.php
// ...
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func('render_template', $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
由於渲染現在是由外部函式(此處為 render_template()
)完成,我們需要將從 URL 擷取的屬性傳遞給它。我們可以將它們作為額外的參數傳遞給 render_template()
,但相反地,讓我們使用 Request
類別的另一個稱為「屬性」的功能:Request 屬性是一種附加關於 Request 的額外資訊的方式,這些資訊與 HTTP Request 資料沒有直接關係。
您現在可以建立 render_template()
函式,這是一個通用控制器,用於在沒有特定邏輯時渲染樣板。為了保持與之前相同的樣板,請求屬性會在渲染樣板之前被擷取。
1 2 3 4 5 6 7 8
function render_template(Request $request): Response
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
return new Response(ob_get_clean());
}
由於 render_template
被用作 PHP call_user_func()
函式的參數,我們可以將其替換為任何有效的 PHP 回呼。這允許我們使用函式、匿名函式或類別的方法作為控制器... 您的選擇。
作為慣例,對於每個路由,關聯的控制器是透過 _controller
路由屬性配置的
1 2 3 4 5 6 7 8 9 10 11 12 13
$routes->add('hello', new Routing\Route('/hello/{name}', [
'name' => 'World',
'_controller' => 'render_template',
]));
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
現在路由可以與任何控制器關聯,並且在控制器內,您仍然可以使用 render_template()
來渲染樣板
1 2 3 4 5 6
$routes->add('hello', new Routing\Route('/hello/{name}', [
'name' => 'World',
'_controller' => function (Request $request): string {
return render_template($request);
}
]));
這相當靈活,因為您可以稍後變更 Response 物件,甚至可以將額外的參數傳遞給樣板
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$routes->add('hello', new Routing\Route('/hello/{name}', [
'name' => 'World',
'_controller' => function (Request $request): Response {
// $foo will be available in the template
$request->attributes->set('foo', 'bar');
$response = render_template($request);
// change some header
$response->headers->set('Content-Type', 'text/plain');
return $response;
}
]));
這是我們框架的更新和改進版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
function render_template(Request $request): Response
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
return new Response(ob_get_clean());
}
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
$response->send();
為了慶祝我們新框架的誕生,讓我們建立一個全新的應用程式,該應用程式需要一些簡單的邏輯。我們的應用程式有一個頁面,說明給定的年份是否為閏年。當呼叫 /is_leap_year
時,您會得到當前年份的答案,但您也可以指定年份,例如 /is_leap_year/2009
。由於是通用的,因此框架不需要以任何方式修改,請建立一個新的 app.php
檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// example.com/src/app.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
function is_leap_year(?int $year = null): bool
{
if (null === $year) {
$year = (int)date('Y');
}
return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100);
}
$routes = new Routing\RouteCollection();
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
'year' => null,
'_controller' => function (Request $request): Response {
if (is_leap_year($request->attributes->get('year'))) {
return new Response('Yep, this is a leap year!');
}
return new Response('Nope, this is not a leap year.');
}
]));
return $routes;
is_leap_year()
函式在給定年份為閏年時傳回 true
,否則傳回 false
。如果年份為 null
,則會測試當前年份。控制器的工作很少:它從請求屬性中取得年份,將其傳遞給 is_leap_year()
函式,並根據傳回值建立新的 Response 物件。
一如既往,您可以決定在此停止並按原樣使用該框架;這可能就是您建立簡單網站(例如那些精美的單頁 網站)以及其他一些網站所需的一切。