Пока что, наше приложение довольно простое и содержит всего одну страничку. Давайте немного посумасбродствуем, и добавим еще одну, чтобы сказать “Пока”.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?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(); |
Как видите, bye.php и index.php содержат много общего кода. Давайте выделим код, который мы можем разделить между обеими страничками. Общий код — звучит как хороший план, для создания нашего первого настоящего фреймворка..
В PHP для такого рефакторинга обычно используется подключаемый файл:
1 2 3 4 5 6 7 8 9 10 11 |
<?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(); |
Посмотрим что получилось:
1 2 3 4 5 6 7 8 9 10 |
<?php // framework/hello.php require_once __DIR__.'/init.php'; $input = $request->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); $response->send(); |
И для странички с прощанием:
1 2 3 4 5 6 7 8 |
<?php // framework/bye.php require_once __DIR__.'/init.php'; $response->setContent('Goodbye!'); $response->send(); |
Мы вынесли общий код в отдельный файл, но… Это не похоже на хорошую абстракцию, не так ли? Во-первых, мы все еще имеем метод Send() на обеих страничках. Во-вторых, наши странички не похожи на шаблоны, и мы все еще не можем правильно тестировать код.
Более того, добавление новой странички, означает, что нам нужно создавать новый PHP скрипт, имя которого конечный юзер видит в URL (http://example.com/bye.php). Т.е. мы имеем прямую связь между PHP скриптом и URL. Это потому, что доставкой запроса напрямую занимается web-сервер. Неплохо было бы взять доставку запроса под свой контроль, ради обеспечения большей гибкости. Этого легко добиться, если производить роутинг всех клиентских запросов в одном PHP скрипте.
Использование одного PHP скрипта для всех пользовательских запросов — это шаблон проектирования “Front controller”
Такой скрипт может выглядеть вот так:
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 26 |
<?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
1 2 3 4 5 6 |
<?php // framework/hello.php $input = $request->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); |
В скрипте front.php, массив $map сопоставляет URL с соответствующими PHP скриптами.
Как бонус, если клиент запрашивает путь, не определенный в массиве $map, мы возвращаем ему нашу кастомизированную страничку 404.
Для доступа к страничкам теперь можно использовать front.php:
http://example.com/front.php/hello?name=Fabien
http://example.com/front.php/bye
/hello и /bye — пути страничек.
Большинство web серверов(в т.ч. Apache и Nginx) могут переписать входящий URL, и удалить из него скрипт front controller, так что ваши пользователи, будут использовать url вида:
http://example.com/hello?name=Fabien.
Вся соль в том, чтобы использовать метод Request::getPathInfo(), который возвращает путь запроса, удалив название скрипта фронт контроллера (включая его поддиректории).
Теперь, когда web сервер всегда запрашивает один и тот же скрипт (front.php) для всех ваших страничек, мы можем немного повысить безопасность приложения, если перенесем все остальные PHP файлы из корневой директории сервера.
1 2 3 4 5 6 7 8 9 10 |
example.com ??? composer.json ? src ? ??? autoload.php ? ??? pages ? ??? hello.php ? ??? bye.php ??? vendor ??? web ??? front.php |
Настройте web сервер так, что бы корневой директорией была директория web/, и все остальные файлы отныне клиенту не доступны.
Что бы заставить работать приложение с новой структурой файлов, вам придется в некоторых местах поменять пути, оставлю эти изменения в качестве вашего домашнего задания. (Если возникнут трудности, можете посмотреть в моем репозитории 🙂 прим.пер.)
Последняя штука, которая повторяется на каждой страничке — вызов метода
setContent()
. Мы можем конвертировать все странички в шаблоны, если будем выводить содержимое и вызывать метод
setContent()
прямо из фронт контроллера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?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 стал настоящим шаблоном:
1 2 3 4 5 |
<!-- example.com/src/pages/hello.php --> <?php $name = $request->get('name', 'World') ?> Hello <?php echo 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 26 27 28 |
<?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(); |
Добавление новой странички происходит в два шага:
- добавить записи в массив $map,
- создать PHP шаблон в /src/pages/.
В шаблоне, мы можем получать данные запроса с помощью переменной $request, и настраивать заголовки ответа с помощью переменной $response.
Если вы решили остановиться здесь, я бы еще посоветовал улучшить ваш фреймворк, вынеся массив $map в файл конфигурации.
К содержанию >>
Оригинал статьи на английском языке >>
Исходный код из статьи >>