Компонент HttpFoundation
Дата обновления перевода 2024-07-19
Компонент HttpFoundation
До того, как нырять в процесс создания фреймворка, давайте вначале сделаем шаг назад и посмотрим на то, почему вы хотите использовать фреймворк вместо того, чтобы оставить ваше старое доброе PHP-приложение в том виде, в котором оно есть. Почему использование фреймворка - это действительно хорошая идея,даже для простейшего отрезка кода, и почему создание фреймворка поверх компонентов Symfony лучше, чем создание фреймворка с нуля.
Note
Мы не будем говорить о традиционных преимуществах использования фреймворка при работе над большими приложениями со множеством разработчиков; Интернет уже имеет предостаточно источников на эту тему.
Даже если "приложение", которое мы написали в предыдущей главе, было достаточно простым, оно страдает от нескольких проблем:
1 2 3 4
// framework/index.php
$name = $_GET['name'];
printf('Hello %s', $name);
Во-первых, если параметр запроса name
не определён в строке запроса URL,
вы полчите PHP-предупреждение; так что давайте исправим это:
1 2 3 4
// framework/index.php
$name = $_GET['name'] ?? 'World';
printf('Hello %s', $name);
Далее, это приложение не защищено. Вы можете в это поверить? Даже этот простой отрезок PHP-кода уязвим к одной из наиболее распространённых пррблем безопасности в Интернете - XSS (Межсайтовый скриптинг). Вот более защищённная версия:
1 2 3 4 5
$name = $_GET['name'] ?? 'World';
header('Content-Type: text/html; charset=utf-8');
printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'));
Note
Как вы могли заметить, защита вашего кода с помощью htmlspecialchars
-
громоздкая и склонная к ошибкам. Это одна из причин, почему использование
шаблонизатора вроде Twig, где автоматическое экранирование подключается
по умолчанию, может быть хорошей идеей (а ясное экранирование также менее
блезненно с использованием простого фильтра e
).
Как вы можете сами увидеть, простой код, который мы написали первым, уже не такой простой, если мы хотим избежать PHP предупреждений/замечаний, и сделать код более безопасным.
Кроме безопасности, этот код даже не леготестируемый. Даже если нет особо что тестировать, мне кажется, что писать модульные тесты для максимально простых отрезков PHP-кода - ненатурально и выглядит отвратно. Вот пробный модульный тест PHPUnit для вышенаписанного кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// framework/test.php
use PHPUnit\Framework\TestCase;
class IndexTest extends TestCase
{
public function testHello(): void
{
$_GET['name'] = 'Fabien';
ob_start();
include 'index.php';
$content = ob_get_clean();
$this->assertEquals('Hello Fabien', $content);
}
}
Note
Если наше приложение было бы чуточку больше, мы бы могли найти даже больше проблем. Если вам интересно, прочтите главу книги .
На данный момент, если вы не убеждены, что безопасность и тестирование действительно две очень хороших причины, чтобы перестать писать код по-старому и принять вместо этого фреймворк (что бы принятие фреймворка не значило в этом контексте), то вы можете перестать читать эту книгу и вернуться к тому коду, над которым работали ранее.
Note
Конечно, использование фреймворка должно дать вам больше, чем просто безопасность и тестируемость, но гораздо важнее помнить, что фреймворк, который вы выберете, должен позволять вам писать лучший код за меньшее время.
ООП с компонентом HttpFoundation
Написание веб-кода заключается во взаимодействии с HTTP. Так что фундаментальный принципы нашего фреймворка должны строиться вокруг HTTP-спецификации.
HTTP-спецификация описывает то, как клиент (например, браузер) взаимодействует с сервером (нашим приложением через веб-сервер). Диалог между клиентом и сервером указывается чётко определёнными сообщениями, запросами и ответами: клиент отправляет запрос серверу и, основываясь на этом запросе, сервер возвращает ответ.
В PHP, запрос представлен глобальными переменными ($_GET
, $_POST
,
$_FILE
, $_COOKIE
, $_SESSION
...), а ответ генерируется функциями
(echo
, header
, setcookie
, ...).
Первый шаг на пути к лучшему коду - это, наверно, использование Объекто-ориентированного подхода; это основная цель компонента Symfony HttpFoundation: заменить глбальные переменные и функции PHP по умолчанию Объектно-ориентированным слоем.
Чтобы использовать этот компонент, добавьте его в качестве зависимости к проекту:
1
$ composer require symfony/http-foundation
Запуск этой команды также автоматически скачает компонент Symfony HttpFoundation
и установит его в каталоге vendor/
. Файлы composer.json
и composer.lock
также будут сгенерированы с содержанием нового требования.
Теперь, давайте перепишем наше приложение, используя классы Request
и
Response
:
1 2 3 4 5 6 7 8 9 10 11 12 13
// framework/index.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$name = $request->query->get('name', 'World');
$response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();
Метод createFromGlobals()
создаёт объект Request
, основанный на текущих
глбальных переменных PHP.
Метод send()
отправляет объект Response
обратно клиенту (он вначале
вывдит HTTP-заголовки, а следом - содержимое).
Tip
До вызова send()
, нам нужно было добавить вызов к методу prepare()
($response->prepare($request);
), чтобы гарантироват, что наш Ответ
соответствует HTTP-спецификации. Если бы мы вызвали страницу с методом
HEAD
, он бы удалил содержимое Ответа.
Главное различие с предыдущим кодом в том, что у вас есть тотальный контроль HTTP-сообщений. Вы можете создать любой запрос, который вы хотите, и вы отвечаете за отправку ответа тогда, когда считаете это нужным.
Note
Мы ещё не ясно установили заголовок Content-Type
в переписанном
коде, так как набор символом объекта Ответ по умолчанию - UTF-8
.
С классом Request
, у вас в руках есть вся ниформация запроса, благодаря
простому и красивому API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// запрашиваемый URI (например, /about) минус любые параметры запроса
$request->getPathInfo();
// извлекает переменные GET и POST, соответственно
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');
// извлекает переменные SERVER
$request->server->get('HTTP_HOST');
// извлекает экземпляр UploadedFile, идентифицированный foo
$request->files->get('foo');
// извлекает значение COOKIE
$request->cookies->get('PHPSESSID');
// извлекает HTTP-заголовок запроса с нормализвованными ключами нижнего регистра
$request->headers->get('host');
$request->headers->get('content_type');
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // массив языков, принятых клиентом
Вы также можете сымитировать запрос:
1
$request = Request::create('/index.php?name=Fabien');
С классом Response
вы можете с лёгкостью подстроить ответ:
1 2 3 4 5 6 7 8
$response = new Response();
$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');
// сконфигурируйте HTTP-заголовки кеша
$response->setMaxAge(10);
Tip
Чтобы отладить ответ, поместите его в строку; он вернёт HTTP-представление ответа (загловки и содержимое).
Последнее, но не менее важное, эти классы, как любой другой класс в коде Symfony, прошли аудит независимой компаниии на предмет проблем безопасности. А то, что это проект с открытым исходным кодом, также означает, что многие другие разработчики по всему миру прочли код и уже исправили потенциальные проблемы безопасности. Когда вы последний раз заказывали професииональный аудит защиты для вашего домашнего фреймворка?
Даже что-то настолько простое, как получение IP-адресов клиентов может быть небезопасно:
1 2 3
if ($myIp === $_SERVER['REMOTE_ADDR']) {
// клиент известен, так что дайте ему больше привелегий
}
Всё отлично работает до тех пор, пока вы не добавите обратный прокси перед серверами производста; на этом этапе, вам нужно будет изменить ваш код, чтобы он работал как на машине разработки (где у вас нет прокси), таки на ваших серверах:
1 2 3
if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) {
// клиент известен, так что дайте ему больше привелегий
}
Использование метода Request::getClientIp()
дало бы вам правильное поведение с
первого момента (а также охватило бы случай со сменой прокси):
1 2 3 4 5
$request = Request::createFromGlobals();
if ($myIp === $request->getClientIp()) {
// клиент известен, так что дайте ему больше привелегий
}
Есть ещё дополнительное преимущество: он безопасен по умолчанию. Что это
значит? Значению $_SERVER['HTTP_X_FORWARDED_FOR']
нельзя доверять, так
как оно может быть изменено конечным пользователем при отсутствии прокси. так
что если вы исползоуете этот код в производстве без прокси, становится до
скуки легко навредить вашей системе. Это не так с методом getClientIp()
,
так как вы должны ясно доверять вашим обратным прокси, вызвав setTrustedProxies()
:
1 2 3 4 5
Request::setTrustedProxies(['10.0.0.1']);
if ($myIp === $request->getClientIp()) {
// клиент известен, так что дайте ему больше привелегий
}
Итак, метод getClientIp()
отлично работает при любых обстоятельствах. Вы
можете использовать его во всех ваших проектах, независимо от цели использования
фреймворка. Если бы вы писали фреймворк с нуля, то вам нужно было бы подумать обо
всех этих случаях самостоятельно. Почему бы не использовать технологию, которая
уже работает?
Note
Если вы хотите узнать больше о компоненте HttpFoundation, вы можете посмотреть
на API Symfony\Component\HttpFoundation
, или прочитать посвящённую
ему документацию.
Верите вы этому, или нет, но у нас есть наш первый фреймворк. Вы можете сейчас остановиться, если хотите. Использование только компонента Symfony HttpFoundation уже позволяет вам писать улучшенный и тестируемый код. Оно также позволяет вам писать код быстрее, так как многие рутинные проблемы уже были решены за вас.
На самом деле, проекты вроде Drupal приняли компонент HttpFoundation; если он подходит им, то он скорее всего подойдёт вам. Не изобретайте колесо заново.
Я почти забыл сказать ещё об одном преимуществе: использование компонента HttpFoundation - это начало лучшего взаимодействия между всеми фреймворками и приложениями, используюшими его (вроде Symfony, Drupal 8, phpBB 3, Laravel и ezPublish 5, и других).