Фронт-контроллер

Дата обновления перевода 2022-01-09

Фронт-контроллер

До этих пор наше приложение было простым, так как имело только одну страницу. Чтобы придать немного остроты, давайте пошалим, и добавим ещё одну страницу, которая будет прощаться:

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();

Как вы можете увидеть сами, большая часть кода точно такая же, как и тот, что вы написали на первой страницу. Давайте извлечём общий код, который мы можем разделить между всеми нашими страницами. Общий код звучит, как хорошая идея для создания нашего первого "настоящего" фреймворка!

PHP-способ произведения реорганизации скорее всего заключался бы в создании файла включения:

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();

Давайте увидим его в действии:

1
2
3
4
5
6
7
// framework/index.php
require_once __DIR__.'/init.php';

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

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

И для страницы "Прощания":

1
2
3
4
5
// framework/bye.php
require_once __DIR__.'/init.php';

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

Мы действительно переместили большую часть общего кода в центральную часть, но это не кажется хорошей абстракцией, правда же? У нас всё ещё есть метод send() для всех страниц, наши страницы не выглядят как шаблоны, и мы всё ещё не может правильно тестировать этот код.

Более того, добавление новой страницы означает, что нам нужно создать новый PHP-скрипт, имя которого раскрывается конечному пользователю через URL (http://127.0.0.1:4321/bye.php): существует прямая связь между PHP именем скрипта и клиентским URL. Это так, потому что развёртывание запроса производится напрямую веб-сервером. Может быть хорошей идеей переместить это развёртывание в наш код для большей гибкости. Этого можно легко достичь маршрутизируя все клиентские запросы по одному PHP-скрипту.

Tip

Раскрытие одного PHP-скрипта конечному пользователю - это схема дизайна, которая называется "фронт контроллер".

Такой скрипт может выглядеть следующим образом:

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();

А вот, например, новый скрипт hello.php:

1
2
3
// framework/hello.php
$name = $request->attributes->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));

В скрипте front.php, $map ассоциирует пути URL с соответствующим им путям PHP-скриптов.

В качестве бонуса, если клиент запросит путь, который не определён в карте URL, мы возвращаем пользовательскую страницу 404; теперь вы контролируете ваш веб-сайт.

Чтобы получить доступ к странице, вы теперь должны использовать скрипт front.php:

  • http://127.0.0.1:4321/front.php/hello?name=Fabien
  • http://127.0.0.1:4321/front.php/bye

/hello и /bye - это пути страницы.

Tip

Больщинство веб-серверов вроде Apache или nginx могут переписать входящие URL и удалить скрипт фронт контроллера, чтобы ваши пользователи могли ввести http://127.0.0.1:4321/hello?name=Fabien, что выглядит намного лучше.

Фокус заключается в использовании метода Request::getPathInfo(), который возвращает пусть Запроса, удаляя имя скрипта фронт контроллера, включая его под-каталоги (только если это необходимо, смотрите совет выше).

Tip

Вам даже не нужно настраивать веб-сервер для тестирования кода. Вместо этого, замените вызов $request = Request::createFromGlobals(); на что-то вроде $request = Request::create('/hello?name=Fabien');, где аргумент - это путь URL, который вы хотите симулировать.

Теперь, когда веб-сервер всегда получает доступ к одному и тому же скрипту (front.php) для всех страниц, мы можем больше защитить код, переместив все другие PHP-файлы вне корневого веб-каталога:

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

Теперь, сконфигурируйте ваш корневой каталог веб-сервера так, чтобы они указывал на web/, и все другие файлы больше не будут доступны из клиента.

Чтобы протестировать ваши изменения в браузере (http://localhost:4321/hello/?name=Fabien), запустите Локальный веб-сервер Symfony:

1
$ symfony server:start --port=4321 --passthru=front.php

Note

Для того, чтобы эта новая структура работала, вам нужно настроить некоторые пути в различных PHP-файлах; изменения остаются в качестве упражнения для читателя.

Последнее, что повторяется на каждой странице - это вызов к 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');
}

// ...

А скрипт hello.php теперь может быть конвертирован в шаблон:

1
2
3
4
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->attributes->get('name', 'World') ?>

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

У нас есть первая версия нашего фреймворка:

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();

Добавление новой страницы - это двушаговый процесс: добавьте запись к карте и создайте PHP-шаблон в src/pages/. Из шаблона, получите данные Запроса через переменную $request и подстройте заголовки Ответа через переменную $response.

Note

Если вы решите остановиться на этом, то вы скорее всего сможете улучшить ваш фреймворк, путём извлечения карты URL в файл конфигурации.