The HttpKernel Component: the Controller Resolver

You might think that our framework is already pretty solid and you are probably right. But let's see how we can improve it nonetheless.

私たちのフレームワークはすでにかなりしっかりしていると思うかもしれませんが、おそらくその通りです。しかし、それにもかかわらず、それを改善する方法を見てみましょう。

Right now, all our examples use procedural code, but remember that controllers can be any valid PHP callbacks. Let's convert our controller to a proper class:

現時点では、すべての例で手続き型コードを使用していますが、コントローラーは有効な PHP コールバックであれば何でもよいことに注意してください。コントローラーを適切なクラスに変換しましょう。
1
2
3
4
5
6
7
8
9
10
11
class LeapYearController
{
    public function index($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.');
    }
}

Update the route definition accordingly:

それに応じてルート定義を更新します。
1
2
3
4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
    'year' => null,
    '_controller' => [new LeapYearController(), 'index'],
]));

The move is pretty straightforward and makes a lot of sense as soon as you create more pages but you might have noticed a non-desirable side effect... The LeapYearController class is always instantiated, even if the requested URL does not match the leap_year route. This is bad for one main reason: performance-wise, all controllers for all routes must now be instantiated for every request. It would be better if controllers were lazy-loaded so that only the controller associated with the matched route is instantiated.

移動は非常に簡単で、ページを作成するとすぐに意味を成しますが、望ましくない副作用に気付いたかもしれません... LeapYearController クラスは、要求された URL が jump_year ルートと一致しない場合でも、常にインスタンス化されます。これは 1 つの主な理由で悪いです: パフォーマンスの観点から、すべてのルートのすべてのコントローラーをすべてのリクエストに対してインスタンス化する必要があります。一致したルートに関連付けられたコントローラーのみがインスタンス化されるように、コントローラーが遅延ロードされた方がよいでしょう。

To solve this issue, and a bunch more, let's install and use the HttpKernel component:

この問題やその他の問題を解決するために、HttpKernel コンポーネントをインストールして使用しましょう。
1
$ composer require symfony/http-kernel

The HttpKernel component has many interesting features, but the ones we need right now are the controller resolver and argument resolver. A controller resolver knows how to determine the controller to execute and the argument resolver determines the arguments to pass to it, based on a Request object. All controller resolvers implement the following interface:

HttpKernel コンポーネントには多くの興味深い機能がありますが、今必要なのはコントローラー リゾルバーと引数リゾルバーです。コントローラー リゾルバーは、実行するコントローラーを決定する方法を認識しており、引数リゾルバーは、Request オブジェクトに基づいて、コントローラーに渡す引数を決定します。すべてのコントローラー リゾルバーは、次のインターフェイスを実装します。
1
2
3
4
5
6
7
namespace Symfony\Component\HttpKernel\Controller;

// ...
interface ControllerResolverInterface
{
    public function getController(Request $request);
}

The getController() method relies on the same convention as the one we have defined earlier: the _controller request attribute must contain the controller associated with the Request. Besides the built-in PHP callbacks, getController() also supports strings composed of a class name followed by two colons and a method name as a valid callback, like 'class::method':

getController() メソッドは、以前に定義したものと同じ規則に依存しています。_controller 要求属性には、要求に関連付けられたコントローラーが含まれている必要があります。組み込みの PHP コールバックに加えて、getController() は、「class::method」のように、有効なコールバックとして、クラス名の後に 2 つのコロンとメソッド名で構成される文字列もサポートします。
1
2
3
4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
    'year' => null,
    '_controller' => 'LeapYearController::index',
]));

To make this code work, modify the framework code to use the controller resolver from HttpKernel:

このコードを機能させるには、HttpKernel の controllerresolver を使用するようにフレームワーク コードを変更します。
1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpKernel;

$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();

$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);

Note

ノート

As an added bonus, the controller resolver properly handles the error management for you: when you forget to define a _controller attribute for a Route for instance.

追加のボーナスとして、コントローラー リゾルバーはエラー管理を適切に処理します。たとえば、ルートの _controller 属性を定義するのを忘れた場合などです。

Now, let's see how the controller arguments are guessed. getArguments() introspects the controller signature to determine which arguments to pass to it by using the native PHP reflection. This method is defined in the following interface:

では、コントローラーの引数がどのように推測されるかを見てみましょう。 getArguments() はコントローラーの署名をイントロスペクトし、ネイティブ PHP リフレクションを使用してどの引数を渡すかを決定します。このメソッドは、次のインターフェースで定義されています。
1
2
3
4
5
6
7
namespace Symfony\Component\HttpKernel\Controller;

// ...
interface ArgumentResolverInterface
{
    public function getArguments(Request $request, $controller);
}

The index() method needs the Request object as an argument. getArguments() knows when to inject it properly if it is type-hinted correctly:

index() メソッドは Request オブジェクトを引数として必要とします。getArguments() は、正しくタイプヒントされていれば、適切に注入するタイミングを認識しています。
1
2
3
4
public function index(Request $request)

// won't work
public function index($request)

More interesting, getArguments() is also able to inject any Request attribute; if the argument has the same name as the corresponding attribute:

さらに興味深いことに、getArguments() は任意の Requestattribute を注入することもできます。引数が対応する属性と同じ名前の場合:
1
public function index($year)

You can also inject the Request and some attributes at the same time (as the matching is done on the argument name or a type hint, the arguments order does not matter):

Request といくつかの属性を同時に注入することもできます (マッチングは引数名または型ヒントで行われるため、引数の順序は関係ありません)。
1
2
3
public function index(Request $request, $year)

public function index($year, Request $request)

Finally, you can also define default values for any argument that matches an optional attribute of the Request:

最後に、リクエストのオプション属性に一致する任意の引数のデフォルト値を定義することもできます:
1
public function index($year = 2012)

Let's inject the $year request attribute for our controller:

コントローラーに $year リクエスト属性を挿入しましょう。
1
2
3
4
5
6
7
8
9
10
11
class LeapYearController
{
    public function index($year)
    {
        if (is_leap_year($year)) {
            return new Response('Yep, this is a leap year!');
        }

        return new Response('Nope, this is not a leap year.');
    }
}

The resolvers also take care of validating the controller callable and its arguments. In case of a problem, it throws an exception with a nice message explaining the problem (the controller class does not exist, the method is not defined, an argument has no matching attribute, ...).

リゾルバーは、コントローラー呼び出し可能オブジェクトとその引数の検証も処理します。問題が発生した場合は、問題を説明する適切なメッセージと共に例外をスローします (コントローラー クラスが存在しない、メソッドが定義されていない、引数に一致する属性がない、など)。

Note

ノート

With the great flexibility of the default controller resolver and argument resolver, you might wonder why someone would want to create another one (why would there be an interface if not?). Two examples: in Symfony, getController() is enhanced to support controllers as services; and getArguments() provides an extension point to alter or enhance the resolving of arguments.

デフォルトのコントローラーリゾルバーと引数リゾルバーの優れた柔軟性により、なぜ誰かが別のものを作成したいのか疑問に思うかもしれません (そうでない場合、なぜインターフェイスがあるのでしょうか?)。 2 つの例: Symfony では、getController() はコントローラーをサービスとしてサポートするように拡張されています。また、getArguments() は引数の解決を変更または拡張するための拡張ポイントを提供します。

Let's conclude with the new 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
34
35
36
37
38
39
40
41
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;

function render_template(Request $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);

$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));

    $controller = $controllerResolver->getController($request);
    $arguments = $argumentResolver->getArguments($request, $controller);

    $response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->send();

Think about it once more: our framework is more robust and more flexible than ever and it still has less than 50 lines of code.

もう一度考えてみてください。私たちのフレームワークはこれまで以上に堅牢で柔軟性があり、コードはまだ 50 行未満です。