Templating

The astute reader has noticed that our framework hardcodes the way specific "code" (the templates) is run. For simple pages like the ones we have created so far, that's not a problem, but if you want to add more logic, you would be forced to put the logic into the template itself, which is probably not a good idea, especially if you still have the separation of concerns principle in mind.

賢明な読者は、私たちのフレームワークが特定の「コード」(テンプレート) の実行方法をハードコーディングしていることに気付きました。これまでに作成したような単純なページの場合、それは問題ではありませんが、さらにロジックを追加したい場合は、ロジックをテンプレート自体に配置する必要があります。懸念の分離の原則を念頭に置いています。

Let's separate the template code from the logic by adding a new layer: the controller: The controller's mission is to generate a Response based on the information conveyed by the client's Request.

新しいレイヤーを追加して、テンプレート コードをロジックから分離しましょう: コントローラー: コントローラーの使命は、クライアントの要求によって伝達される情報に基づいて応答を生成することです。

Change the template rendering part of the framework to read as follows:

フレームワークのテンプレート レンダリング部分を次のように変更します。
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);
}

As the rendering is now done by an external function (render_template() here), we need to pass to it the attributes extracted from the URL. We could have passed them as an additional argument to render_template(), but instead, let's use another feature of the Request class called attributes: Request attributes is a way to attach additional information about the Request that is not directly related to the HTTP Request data.

レンダリングは外部関数 (ここでは render_template()) によって行われるため、URL から抽出された属性を渡す必要があります。 render_template() への追加の引数としてそれらを渡すこともできましたが、代わりに、attributes と呼ばれる Request クラスの別の機能を使用しましょう: リクエスト属性は、HTTP リクエスト データに直接関係しないリクエストに関する追加情報を添付する方法です。

You can now create the render_template() function, a generic controller that renders a template when there is no specific logic. To keep the same template as before, request attributes are extracted before the template is rendered:

特定のロジックがない場合にテンプレートをレンダリングする汎用コントローラーである render_template() 関数を作成できるようになりました。以前と同じテンプレートを維持するために、テンプレートがレンダリングされる前にリクエスト属性が抽出されます。
1
2
3
4
5
6
7
8
function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);

    return new Response(ob_get_clean());
}

As render_template is used as an argument to the PHP call_user_func() function, we can replace it with any valid PHP callbacks. This allows us to use a function, an anonymous function or a method of a class as a controller... your choice.

render_template は PHP の call_user_func() 関数の引数として使用されるため、これを任意の有効な PHP コールバックに置き換えることができます。これにより、関数、匿名関数、またはクラスのメソッドをコントローラーとして使用できます...お好みで。

As a convention, for each route, the associated controller is configured via the _controller route attribute:

慣習として、ルートごとに、関連付けられたコントローラーは _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);
}

A route can now be associated with any controller and within a controller, you can still use the render_template() to render a template:

ルートは任意のコントローラーに関連付けることができるようになり、コントローラー内では引き続き render_template() を使用してテンプレートをレンダリングできます。
1
2
3
4
5
6
$routes->add('hello', new Routing\Route('/hello/{name}', [
    'name' => 'World',
    '_controller' => function ($request) {
        return render_template($request);
    }
]));

This is rather flexible as you can change the Response object afterwards and you can even pass additional arguments to the template:

これは、後で 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) {
        // $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;
    }
]));

Here is the updated and improved version of our framework:

フレームワークの更新および改善されたバージョンは次のとおりです。
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)
{
    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();

To celebrate the birth of our new framework, let's create a brand new application that needs some simple logic. Our application has one page that says whether a given year is a leap year or not. When calling /is_leap_year, you get the answer for the current year, but you can also specify a year like in /is_leap_year/2009. Being generic, the framework does not need to be modified in any way, create a new app.php file:

新しいフレームワークの誕生を祝うために、簡単なロジックを必要とするまったく新しいアプリケーションを作成しましょう。このアプリケーションには、特定の年がうるう年かどうかを示すページが 1 つあります。 /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
// example.com/src/app.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;

function is_leap_year($year = null)
{
    if (null === $year) {
        $year = 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) {
        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;

The is_leap_year() function returns true when the given year is a leap year, false otherwise. If the year is null, the current year is tested. The controller does little: it gets the year from the request attributes, pass it to the is_leap_year() function, and according to the return value it creates a new Response object.

is_leap_year() 関数は、指定された年がうるう年の場合は true を返し、そうでない場合は false を返します。年が null の場合、現在の年がテストされます。コントローラーはほとんど何もしません: requestattributes から年を取得し、それを is_leap_year() 関数に渡し、戻り値に従って新しい Response オブジェクトを作成します。

As always, you can decide to stop here and use the framework as is; it's probably all you need to create simple websites like those fancy one-page websites and hopefully a few others.

いつものように、ここでやめて、フレームワークをそのまま使用することができます。おそらく、これらの派手な 1 ページの Web サイトや、できれば他のいくつかの Web サイトのような単純な Web サイトを作成するのに必要なのは、これだけです。