跳到主要內容

樣板

編輯此頁面

敏銳的讀者可能已經注意到,我們的框架硬式編碼了特定「程式碼」(樣板)的執行方式。對於像我們目前建立的簡單頁面來說,這不是問題,但如果您想要加入更多邏輯,您將被迫將邏輯放入樣板本身,這可能不是一個好主意,特別是如果您仍然將關注點分離原則放在心上的話。

讓我們透過新增一個新的層級來將樣板程式碼與邏輯分離:控制器:控制器的任務是根據客戶端請求傳達的資訊產生回應。

變更框架的樣板渲染部分,使其讀取如下

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 物件。

一如既往,您可以決定在此停止並按原樣使用該框架;這可能就是您建立簡單網站(例如那些精美的單頁 網站)以及其他一些網站所需的一切。

本作品,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本