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

Дата оновлення перекладу 2023-06-22

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

До цих пір наш додаток був простим, так як мав лише одну сторінку. Щоб додати трохи гостроти, давайте попустуємо і додамо ще одну сторінку, яка буде прощатися:

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

Давайте побачимо його в дії:

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

І для сторінки "прощання":

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->query->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->query->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 у файл конфігурації.