The Front Controller

Up until now, our application is simplistic as there is only one page. To spice things up a little bit, let's go crazy and add another page that says goodbye:

これまでは、1 ページしかないため、アプリケーションは単純化されていました。さよならを言うページをもう 1 つ追加してみましょう。
1
2
3
4
5
6
7
8
9
10
// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';

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

$request = Request::createFromGlobals();

$response = new Response('Goodbye!');
$response->send();

As you can see for yourself, much of the code is exactly the same as the one we have written for the first page. Let's extract the common code that we can share between all our pages. Code sharing sounds like a good plan to create our first "real" framework!

ご覧のとおり、コードの大部分は、最初のページで記述したものとまったく同じです。すべてのページで共有できる共通コードを抽出しましょう。コード共有は、最初の「本物の」フレームワークを作成するための良い計画のように思えます!

The PHP way of doing the refactoring would probably be the creation of an include file:

リファクタリングを行う PHP の方法は、おそらく aninclude ファイルの作成です。
1
2
3
4
5
6
7
8
// framework/init.php
require_once __DIR__.'/vendor/autoload.php';

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

$request = Request::createFromGlobals();
$response = new Response();

Let's see it in action:

実際に見てみましょう:
1
2
3
4
5
6
7
// framework/index.php
require_once __DIR__.'/init.php';

$name = $request->query->get('name', 'World');

$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();

And for the "Goodbye" page:

そして「さようなら」ページの場合:
1
2
3
4
5
// framework/bye.php
require_once __DIR__.'/init.php';

$response->setContent('Goodbye!');
$response->send();

We have indeed moved most of the shared code into a central place, but it does not feel like a good abstraction, does it? We still have the send() method for all pages, our pages do not look like templates and we are still not able to test this code properly.

確かに、共有コードのほとんどを中央の場所に移動しましたが、それは良い抽象化のように感じませんか?すべてのページにまだ send() メソッドがありますが、ページはテンプレートのようには見えず、このコードを適切にテストできません。

Moreover, adding a new page means that we need to create a new PHP script, the name of which is exposed to the end user via the URL (http://127.0.0.1:4321/bye.php). There is a direct mapping between the PHP script name and the client URL. This is because the dispatching of the request is done by the web server directly. It might be a good idea to move this dispatching to our code for better flexibility. This can be achieved by routing all client requests to a single PHP script.

さらに、新しいページを追加するということは、新しい PHP スクリプトを作成する必要があることを意味します。その名前は、URL (http://127.0.0.1:4321/bye.php) を介してエンド ユーザーに公開されます。 PHPscript 名とクライアント URL の間に直接マッピングがあります。これは、リクエストのディスパッチが Web サーバーによって直接行われるためです。柔軟性を高めるために、このディスパッチをコードに移動することをお勧めします。これは、すべてのクライアント要求を単一の PHP スクリプトにルーティングすることで実現できます。

Tip

ヒント

Exposing a single PHP script to the end user is a design pattern called the "front controller".

単一の PHP スクリプトをエンド ユーザーに公開するのは、「フロント コントローラー」と呼ばれる設計パターンです。

Such a script might look like the following:

このようなスクリプトは、次のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

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

$request = Request::createFromGlobals();
$response = new Response();

$map = [
    '/hello' => __DIR__.'/hello.php',
    '/bye'   => __DIR__.'/bye.php',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    require $map[$path];
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

And here is for instance the new hello.php script:

たとえば、新しい hello.php スクリプトは次のとおりです。
1
2
3
// framework/hello.php
$name = $request->query->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));

In the front.php script, $map associates URL paths with their corresponding PHP script paths.

front.php スクリプトでは、$map は URL パスを対応する PHP スクリプト パスに関連付けます。

As a bonus, if the client asks for a path that is not defined in the URL map, we return a custom 404 page. You are now in control of your website.

おまけとして、クライアントが URL マップで定義されていないパスを要求した場合、カスタム 404 ページを返します。これで、Web サイトを管理できます。

To access a page, you must now use the front.php script:

ページにアクセスするには、front.php スクリプトを使用する必要があります。
  • http://127.0.0.1:4321/front.php/hello?name=Fabien
    http://127.0.0.1:4321/front.php/hello?name=ファビアン
  • http://127.0.0.1:4321/front.php/bye
    http://127.0.0.1:4321/front.php/さようなら

/hello and /bye are the page paths.

/hello と /bye はページ パスです。

Tip

ヒント

Most web servers like Apache or nginx are able to rewrite the incoming URLs and remove the front controller script so that your users will be able to type http://127.0.0.1:4321/hello?name=Fabien, which looks much better.

Apache や nginx などのほとんどの Web サーバーは、着信 URL を書き換えて、フロント コントローラー スクリプトを削除できるため、ユーザーは http://127.0.0.1:4321/hello?name=Fabien と入力できるようになります。

The trick is the usage of the Request::getPathInfo() method which returns the path of the Request by removing the front controller script name including its sub-directories (only if needed -- see above tip).

秘訣は、サブディレクトリを含むフロント コントローラー スクリプト名を削除することで、リクエストのパスを返す Request::getPathInfo() メソッドの使用です (必要な場合のみ -- 上記のヒントを参照)。

Tip

ヒント

You don't even need to set up a web server to test the code. Instead, replace the $request = Request::createFromGlobals(); call to something like $request = Request::create('/hello?name=Fabien'); where the argument is the URL path you want to simulate.

コードをテストするために Web サーバーをセットアップする必要さえありません。代わりに、$request = Request::createFromGlobals(); を置き換えます。 $request = Request::create('/hello?name=Fabien'); のようなものを呼び出します。ここで、引数はシミュレートする URL パスです。

Now that the web server always accesses the same script (front.php) for all pages, we can secure the code further by moving all other PHP files outside of the web root directory:

Web サーバーは常にすべてのページで同じスクリプト (front.php) にアクセスするため、他のすべての PHP ファイルを Web ルート ディレクトリの外に移動することで、コードをさらに保護できます。
1
2
3
4
5
6
7
8
9
10
11
example.com
├── composer.json
├── composer.lock
├── src
│   └── pages
│       ├── hello.php
│       └── bye.php
├── vendor
│   └── autoload.php
└── web
    └── front.php

Now, configure your web server root directory to point to web/ and all other files will no longer be accessible from the client.

ここで、Web サーバーのルート ディレクトリを web/ を指すように構成すると、クライアントから他のすべてのファイルにアクセスできなくなります。

To test your changes in a browser (http://localhost:4321/hello?name=Fabien), run the Symfony Local Web Server:

ブラウザー (http://localhost:4321/hello?name=Fabien) で変更をテストするには、Symfony ローカル Web サーバーを実行します。
1
$ symfony server:start --port=4321 --passthru=front.php

Note

ノート

For this new structure to work, you will have to adjust some paths in various PHP files; the changes are left as an exercise for the reader.

この新しい構造を機能させるには、さまざまな PHP ファイルのいくつかのパスを調整する必要があります。変更は、読者の演習として残されています。

The last thing that is repeated in each page is the call to setContent(). We can convert all pages to "templates" by echoing the content and calling the setContent() directly from the front controller script:

各ページで最後に繰り返されるのは setContent() の呼び出しです。コンテンツをエコーし​​、フロント コントローラー スクリプトから直接 setContent() を呼び出すことで、すべてのページを「テンプレート」に変換できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// example.com/web/front.php

// ...

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

// ...

And the hello.php script can now be converted to a template:

そして、hello.php スクリプトをテンプレートに変換できるようになりました。
1
2
3
4
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->query->get('name', 'World') ?>

Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>

We have the first 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
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

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

$request = Request::createFromGlobals();
$response = new Response();

$map = [
    '/hello' => __DIR__.'/../src/pages/hello.php',
    '/bye'   => __DIR__.'/../src/pages/bye.php',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

Adding a new page is a two-step process: add an entry in the map and create a PHP template in src/pages/. From a template, get the Request data via the $request variable and tweak the Response headers via the $response variable.

新しいページの追加は 2 段階のプロセスです。マップにエントリを追加し、src/pages/ に PHP テンプレートを作成します。テンプレートから $request 変数を介してリクエスト データを取得し、$response 変数を介してレスポンス ヘッダーを微調整します。

Note

ノート

If you decide to stop here, you can probably enhance your framework by extracting the URL map to a configuration file.

ここで終了する場合は、URL マップを構成ファイルに抽出することで、おそらくフレームワークを拡張できます。