Компонент HttpFoundation

До того, как нырять в процесс создания фреймворка, давайте вначале сделаем шаг назад и посмотрим на то, почему вы хотите использовать фреймворк вместо того, чтобы оставить ваше старое доброе PHP-приложение в том виде, в котором оно есть. Почему использование фреймворка - это действительно хорошая идея,даже для простейшего отрезка кода, и почему создание фреймворка поверх компонентов Symfony лучше, чем создание фреймворка с нуля.

Note

Мы не будем говорить об очевидных и традиционных преимуществах использования фреймворка при работе над большими приложениями со множеством разработчиков; Интернет уже имеет предостаточно источников на эту тему.

Даже если "приложение", которое мы написали в предыдущей главе, было достаточно простым, оно страдает от нескольких проблем:

1
2
3
4
// framework/index.php
$input = $_GET['name'];

printf('Hello %s', $input);

Во-первых, если параметр запроса name не определён в строке запроса URL, вы полчите PHP-предупреждение; так что давайте исправим это:

1
2
3
4
// framework/index.php
$input = isset($_GET['name']) ? $_GET['name'] : 'World';

printf('Hello %s', $input);

Далее, это приложение не защищено. Вы можете в это поверить? Даже этот простой отрезок PHP-кода уязвим к одной из наиболее распространённых пррблем безопасности в Интернете - XSS (Межсайтовый скриптинг). Вот более защищённная версия:

1
2
3
4
5
$input = isset($_GET['name']) ? $_GET['name'] : 'World';

header('Content-Type: text/html; charset=utf-8');

printf('Hello %s', htmlspecialchars($input, 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

Если наше приложение было бы чуточку больше, мы бы могли найти даже больше проблем. Если вам интересно, прочтите главу книги Symfony versus Flat PHP.

На данный момент, если вы не убеждены, что безопасность и тестирование действительно две очень хороших причины, чтобы перестать писать код по-старому и принять вместо этого фреймворк (что бы принятие фреймворка не значило в этом контексте), то вы можете перестать читать эту книгу и вернуться к тому коду, над которым работали ранее.

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();

$input = $request->get('name', 'World');

$response = new Response(sprintf('Hello %s', htmlspecialchars($input, 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(array('10.0.0.1'));

if ($myIp === $request->getClientIp()) {
    // клиент известен, так что дайте ему больше привелегий
}

Итак, метод getClientIp() отлично работает при любых обстоятельствах. Вы можете использовать его во всех ваших проектах, независимо от цели использования фреймворка. Если бы вы писали фреймворк с нуля, то вам нужно было бы подумать обо всех этих случаях самостоятельно. Почему бы не использовать технологию, которая уже работает?

Note

Если вы хотите узнать больше о компоненте HttpFoundation, вы можете посмотреть на API HttpFoundation, или прочитать посвящённую ему документацию.

Верите вы этому, или нет, но у нас есть наш первый фреймворк. Вы можете сейчас остановиться, если хотите. Использование только компонента Symfony HttpFoundation уже позволяет вам писать улучшенный и тестируемый код. Оно также позволяет вам писать код быстрее, так как многие рутинные проблемы уже были решены за вас.

На самом деле, проекты вроде Drupal приняли компонент the HttpFoundation; если он подходит им, то он скорее всего подойдёт вам. Не изобретайте колесо заново.

Я почти забыл сказать ещё об одном преимуществе: использование компонента HttpFoundation - это начало лучшего взаимодействия между всеми фреймворками и приложениями, использующими его (вроде Symfony, Drupal 8, phpBB 3, ezPublish 5, Laravel, Silex и другими).

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.