Оглавление
До сих пор наше приложение было довольно простым, поскольку содержало только одну страницу.
Что бы немного все усложнить, добавим другую страницу, говорящую «goodbye»:
<?php
// framework/bye.php
require_once __DIR__.'/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response('Goodbye!');
$response->send();
Как видите, большая часть кода точно такая же как и для первой страницы.
Давайте отдельно выделим общий код для всех наших страниц.
«Выделение общего кода», звучит как отличный план для нашего первого «настоящего» фреймворка!
В рамках PHP рефакторинг будет состоять из создания подключаемого файла:
<?php
// framework/init.php
require_once __DIR__.'/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
Посмотрим на это в действии:
<?php
// framework/index.php
require_once __DIR__.'/init.php';
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
$response->send();
И для страницы "Goodbye":
<?php
// framework/bye.php
require_once __DIR__.'/init.php';
$response->setContent('Goodbye!');
$response->send();
Итак, мы собрали общий код в одном месте. Но это не выглядит как хороший уровень абстракции, не так ли?
Во-первых, у нас все еще есть вызов метода send() на всех страницах, по-этому наши страницы не похожи на обычные шаблоны, и мы до сих пор не можем «правильно» тестировать код.
Другая серьёзная проблема заключается в том, что добавление новой страницы означает что мы должны создать новый PHP скрипт,
название которого будет передаваться в URL(http://example.com/bye.php) и вызываться веб-сервером.
Хорошей идеей будет перенести вызов нужного скрипта в наш код для лучшей гибкости.
Этого можно достичь перенаправляя все запросы на один PHP скрипт — front controller.
- Перенаправление всех запросов на один скрипт это паттерн проектирования Front Controller
Сам скрипт может выглядеть так:
<?php
// framework/front.php
require_once __DIR__.'/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
$map = array(
'/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:
<?php
// framework/hello.php
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
В скрипте front.php, $map связывает пути в URL соответсвующим путям к файлам скриптов.
Как бонус, если клиент запрашивает путь, который не определен в массиве соответсвий, мы возвращаем 404 ошибку.
Теперь вы сами контролируете сайт.
Теперь для доступа к страницам вы должны использовать скрипт front controller'а front.php:
/hello и /bye это «пути» страниц.
Подсказка.
Большинство веб-серверов, таких как Apache или nginx, способны переписывать
запрашиваемые URL страниц тем самым убирая из них название файла front controller'а (front.php),
так что вашим пользователям будет достаточно набрать example.com/hello?name=Fabien, что выглядит значительно лучше.
Итак, фокус в использовании метода Request::getPathInfo() который возвращает часть пути из "Request"
удаляя из него название файла front controller'а включая все поддиректории (только если потребуется — см. подсказку выше).
Вам даже не нужно настраивать веб-сервер. Просто
замените вызов $request = Request::createFromGlobals(); на что-то типа $request = Request::create('/hello?name=Fabien');,
где аргументом является запрос, который вы хотите имитировать.
Теперь, когда все запросы к нашим страницам идут через один скрипт (front.php),
мы можем способствовать безопасности перенеся все остальные PHP файлы вне корневой веб-директории:
example.com +-- composer.json ¦ src ¦ +-- autoload.php ¦ L-- pages ¦ +-- hello.php ¦ L-- bye.php +-- vendor L-- web L-- front.php
Сейчас, настройте ваш веб сервер так, что бы директория web/ была корневой для него.
Все, другие файлы больше не доступны для клиента.
Для работы данной структуры директорий, вам нужно будет изменить пути в некоторых файлах проекта;
эти изменения будут домашней работой для читателя.
Последнее что повторяется на кажлой странице, это вызов setContent().
Сейчас мы можем превратить все наши страницы в «шаблоны» просто делая вывод контента
и вызывать setContent() прямо из скрипта front controller'а:
<?php
// 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 можно переделать в шаблон:
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->get('name', 'World') ?>
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
На сегодняшний день наш фреймворк выглядит так:
<?php
// example.com/web/front.php
require_once __DIR__.'/../src/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
$map = array(
'/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" посредством переменной $request
и мы можем манипулировать заголовками ответа("Response") через переменную $response.
Если вы решите остановиться сейчас, хорошей идеей будет вынести массив соответсвий URL в конфигурационный файл.