Компонент HttpFoundation

Дата обновления перевода 2023-01-18

Компонент 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()
    {
        $_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 также будут сгенерированы с содержанием нового требования.

При установке новой зависимости, Composer также генерирует файл vendor/autoload.php, который позволяет лёгкую автозагрузку любого класса. Без автозагрузки, вам нужно будет запрашивать файл, где определяется класс, до того, как вы сможете его исползовать. Но благодаря PSR-4, мы можем просто позволить Composer и PHP проделать тяжелую работу за нас.

Теперь, давайте перепишем наше приложение, используя классы 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->attributes->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, и других).