Фронт-контролер
Дата оновлення перекладу 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 у файл конфігурації.