Оглавление
До сих пор наше приложение было довольно простым, поскольку содержало только одну страницу.
Что бы немного все усложнить, добавим другую страницу, говорящую «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 в конфигурационный файл.