Компонент HttpFoundation

Дата обновления перевода: 2021-12-26

Компонент HttpFoundation

Компонент HttpFoundation определяет объектно-ориентированный слой спецификации HTTP.

В PHP, запрос представлен некоторыми глобальными переменными ($_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, ...), а ответ генерируется некоторыми функциям (echo, header(), setcookie(), ...).

Компонент Symfony HttpFoundation заменяет эти глобальные переменные и функции объектно-ориентироанным слоем.

Установка

1
$ composer require symfony/http-kernel

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

See also

Эта статья объясняет как использовать функции HttpFoundation как независимого компонента в любом приложении PHP. В приложениях Symfony уже всё настроено и готово к использованию. Прочитайте статью Контроллер для понимания как использовать эти функции при создании контроллеров.

Запрос

Наиболее распространённый способ создать запрос - основать его на текущих глобальных переменных PHP c createFromGlobals():

1
2
3
use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

Что почти эквивалентно более многословному, но также более гибкому, вызову __construct():

1
2
3
4
5
6
7
8
$request = new Request(
    $_GET,
    $_POST,
    [],
    $_COOKIE,
    $_FILES,
    $_SERVER
);

Оценка данных запроса

Объект Запроса содержит информацию о запросе клиента. К этой информации можно получить доступ через несколько публичных свойств:

  • request: эквивалент $_POST;
  • query: эквивалент $_GET ($request->query->get('name'));
  • cookies: эквивалент $_COOKIE;
  • attributes: эквивалента нет - используется вашим приложением для хранения других данных (см. below);
  • files: эквивалент $_FILES;
  • server: эквивалент $_SERVER;
  • headers: наиболее эквивалентно субнабору $_SERVER ($request->headers->get('User-Agent')).

Каждое свойство - это экземпляр ParameterBag (или его подкласс), с классом содержания данных:

Все экземпляры ParameterBag имеют методы для извлечения и обновления данных:

all()
Возвращает параметры.
keys()
Возвращает ключи параметра.
replace()
Заменяет текущие параметры новым набором.
add()
Добавляет параметры.
get()
Возвращает параметр по имени.
set()
Устаналивает параметр по имени.
has()
Возвращает true, если параметр определён.
remove()
Удаляет параметр.

Экземпляр ParameterBag также имеет некоторые методы для фильтрации значений ввода:

getAlpha()
Возвращает алфавитные символы значения параметра;
getAlnum()
Возвращает алфавитные символы и цифры значения параметра;
getBoolean()
Возвращает значение параметра преобразованное в булево значение;
getDigits()
Возвращает цифры значения параметра;
getInt()
Возвращает значение параметра, преобразованное в число;
filter()
Фильтрует параметр, используя функцию filter_var.

Все геттеры имеют до двух аргументов: первый - это имя параметра, а второй - значение по умолчанию, которое нужно вернуть, если параметр не существует:

1
2
3
4
5
6
7
8
9
10
// строка запроса - '?foo=bar'

$request->query->get('foo');
// возвращает 'bar'

$request->query->get('bar');
// возвращает null

$request->query->get('bar', 'baz');
// возвращает 'baz'

Когда PHP импортирует запрос на запрос, он обрабатывает параметры запроса как foo[bar]=baz, особенным способом, создавая массив. Метод get() не поддерживает возвращение массивов, поэтому вам нужно использовать следующий код:

1
2
3
4
5
6
7
8
9
10
11
// строка запроса - '?foo[bar]=baz'

// не используйте $request->query->get('foo'); вместо этого, используйте следующее:
$request->query->all()['foo'];
// возвращает ['bar' => 'baz']

$request->query->get('foo[bar]');
// возвращает null

$request->query->get('foo')['bar'];
// возвращает 'baz'

5.1

Метод поддержки массивов get() устарел в Symfony 5.1.

Благодаря публичному свойству attributes, вы можете хранить дополнительные данные в запросе, который также является экземпляром ParameterBag. Это в основном используется для присоединения информации, которая принадлежит Запросу и должна быть доступна из множества точек вашего приложения.

Наконец, сырые данные, отправленные в теле запроса, могут быть доступны, используя getContent():

1
$content = $request->getContent();

Например, это может быть полезно для обработки XML-строки, отправленной приложению удалённым сервисом, использующим метод HTTP POST.

Если тело запроса - это JSON-строка, доступ к нему можно получить используя toArray():

1
$data = $request->toArray();

5.2

Метод toArray() был представлен в Symfony 5.2.

Идентификация запроса

В вашем приложении,вам нужен способ идентифицировать запрос; в большинстве случаев, это делается через "путь информации" запроса, который доступен через метод getPathInfo():

1
2
3
// для запроса к http://example.com/blog/index.php/post/hello-world
// путь информации - "/post/hello-world"
$request->getPathInfo();

Симуляция запроса

Вместо создания запроса, основанного на глобальных PHP, вы также можете симулировать запрос:

1
2
3
4
5
$request = Request::create(
    '/hello-world',
    'GET',
    array('name' => 'Fabien')
);

Метод create() создаёт запрос, основанный на URI, методе и некоторых параметрах (параметры запроса или запроса (query), в зависимости от HTTP метода); и конечно, вы также можете переопределить все другие переменные (по умолчанию, Symfony создаёт разумные значения по умолчанию для всех глобальных переменных PHP).

Основываясь на таком запросе, вы можете переопределить глобальные переменные PHP через overrideGlobals():

1
$request->overrideGlobals();

Tip

Вы также можете дублировать существующий запрос через duplicate() или изменить кучу параметров единственным вызовом к initialize().

Доступ к сессии

Если у вас есть сессия, присоединённая к запросу, вы можете получить к ней доступ через метод getSession() классов Request или RequestStack; hasPreviousSession() сообщает вам, содержит ли запрос сессию, которая была запущена в одном из предыдущих запросов.

Обработка заголовков HTTP

Обработка заголовков HTTP - это нетривиальная задача из-за экранирования и обработки пробельных символов внитри заголовков. Symfony предоставляет класс HeaderUtils, который позволяет абстрагировать эту сложность и определяет несколько методов для наиболее частых задач:

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
use Symfony\Component\HttpFoundation\HeaderUtils;

// Разбивает заголовок HTTP с помощью одного или нескольких разделителей
HeaderUtils::split('da, en-gb;q=0.8', ',;')
// => [['da'], ['en-gb','q=0.8']]

// Объединяет массив массивов в один ассоциативный массив
HeaderUtils::combine(array(array('foo', 'abc'), array('bar')))
// => array('foo' => 'abc', 'bar' => true)

// Объединяет ассоциативный массив в строку для использования в заголовке HTTP
HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',');
// => 'foo=abc, bar, baz="a b c"'

// Экранирует строку, если необходимо
HeaderUtils::quote('foo "bar"')
// => 'foo \"bar\"'

// Деэкранирует строку
HeaderUtils::unquote('foo \"bar\"')
// => 'foo "bar"'

// Аналищирует строку запросу, но оставляет точки (PHP parse_str() заменяет '.' на '_')
HeaderUtils::parseQuery('foo[bar.baz]=qux');
// => ['foo' => ['bar.baz' => 'qux']]

5.2

Метод parseQuery() был представлен в Symfony 5.2.

Доступ к данным заголовков Accept-*

Вы можете с лёгкостью получить доступ к базовым данным, извлечённым из заголовков Accept-*, используя следующие методы:

getAcceptableContentTypes()
Возвращает список приемлемых типов содержания, в порядке снижения качества.
getLanguages()
Возвращает список приемлемых языков, в порядке снижения качества.
getCharsets()
Возвращает список приемлемых наборов символов, в порядке снижения качества.
getEncodings()
Возвращает список приемлемых кодировок, в порядке снижения качества.

Если вам нужно получить полный доступ к проанализированным данным из Accept, Accept-Language, Accept-Charset или Accept-Encoding, вы можете использовать класс утилиты AcceptHeader:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\HttpFoundation\AcceptHeader;

$acceptHeader = AcceptHeader::fromString($request->headers->get('Accept'));
if ($acceptHeader->has('text/html')) {
    $item = $acceptHeader->get('text/html');
    $charset = $item->getAttribute('charset', 'utf-8');
    $quality = $item->getQuality();
}

// Приемлемые объекты заголовков сортируются в порядке снижения качества
$acceptHeaders = AcceptHeader::fromString($request->headers->get('Accept'))
    ->all();

Также поддерживаются значения по умолчанию, которые могут быть опционально включены в заголовки Accept-*:

1
2
3
4
5
$acceptHeader = 'text/plain;q=0.5, text/html, text/*;q=0.8, */*;q=0.3';
$accept = AcceptHeader::fromString($acceptHeader);

$quality = $accept->get('text/xml')->getQuality(); // $quality = 0.8
$quality = $accept->get('application/xml')->getQuality(); // $quality = 0.3

Анонимизация IP-адресов

Все чаще возникает потребность приложений соответствовать регуляциям защити пользователей - анонимизации IP-адресов до логирования и их сохранения в целях анализа. Используйте метод anonymize() из IpUtils чтобы сделать это:

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\IpUtils;

$ipv4 = '123.234.235.236';
$anonymousIpv4 = IpUtils::anonymize($ipv4);
// $anonymousIpv4 = '123.234.235.0'

$ipv6 = '2a01:198:603:10:396e:4789:8e99:890f';
$anonymousIpv6 = IpUtils::anonymize($ipv6);
// $anonymousIpv6 = '2a01:198:603:10::'

Доступ к другим данным

Класс Request имеет множество других методов, которые вы можете использовать для доступа к информации запроса. Посмотрите на API Запроса, чтобы узнать больше о них.

Переопределение запроса

Класс Request не должен быть переопределён, так как это объект данных, который представляет HTTP сообщение. Но при перемещении из системы наследования, добавление методов или изменение некоторого поведения по умолчанию может помочь. В этом случае, зарегистрируйте вызываемое PHP, которое может создать экземпляр вашего класса Request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use App\Http\SpecialRequest;
use Symfony\Component\HttpFoundation\Request;

Request::setFactory(function (
    array $query = [],
    array $request = [],
    array $attributes = [],
    array $cookies = [],
    array $files = [],
    array $server = [],
    $content = null
) {
    return new SpecialRequest(
        $query,
        $request,
        $attributes,
        $cookies,
        $files,
        $server,
        $content
    );
});

$request = Request::createFromGlobals();

Ответ

Объект Response содержит всю информацию, которую нужно отпрвить обратно клиенту из заданного запроса. Конструктор имеет до трёх аргументов: содержимое ответа, статус-код и массив HTTP-заголовков:

1
2
3
4
5
6
7
use Symfony\Component\HttpFoundation\Response;

$response = new Response(
    'Content',
    Response::HTTP_OK,
    ['content-type' => 'text/html']
);

Эту информацию можно также изменять после создания объекта Ответ:

1
2
3
4
5
6
$response->setContent('Hello World');

// публичный атрибут заголовка - ResponseHeaderBag
$response->headers->set('Content-Type', 'text/plain');

$response->setStatusCode(Response::HTTP_NOT_FOUND);

При установке Content-Type Ответа, вы можете установить набор символов, но лучше устанавливать его через метод setCharset():

1
$response->setCharset('ISO-8859-1');

Отметьте, что по умолчанию, Symfony предполагает, что ваши Ответы зашифрованы с помощью UTF-8.

Отправка ответа

До отправки Ответа вы можете по опционально вызвать метод prepare(), чтобы исправить любую несовместимость со спецификацией HTTP (например, неправильный заголовок Content-Type):

1
$response->prepare($request);

Отправка ответа клиенту в таком случае заключается в простом вызове send():

1
$response->send();

Установка куки

Cookie ответа могут быть изменены через публичный атрибут headers:

1
2
3
use Symfony\Component\HttpFoundation\Cookie;

$response->headers->setCookie(Cookie::create('foo', 'bar'));

Метод setCookie() берёт экземпляр Cookie в качестве аргумента.

Вы можете очистить cookie методом clearCookie().

В дополнение к методу Cookie::create(), вы можете создать объект Cookie из исходного значения заголовка используя метод fromString(). Вы также можете использовать методы with*(), чтобы изменить какое-то свойство Cookie (или создать весь Cookie используя свободный интерфейс). Каждый метод with*() возвращает новый объект с модифицированным свойством:

1
2
3
4
5
$cookie = Cookie::create('foo')
    ->withValue('bar')
    ->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
    ->withDomain('.example.com')
    ->withSecure(true);

5.1

Методы with*() были представлены в Symfony 5.1.

Управление HTTP-кешем

Класс Response имеет богатый набор методов для управления HTTP-заголовками, относящимися к кешу:

Note

Методы setExpires(), setLastModified() и setDate() рринимают любой объект, релизующий \DateTimeInterface, включая неизменные объекты дат.

Метод setCache() может быть использован дляустановки наиболее используемой кещ-информации в одном вызове метода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$response->setCache([
    'must_revalidate'  => false,
    'no_cache'         => false,
    'no_store'         => false,
    'no_transform'     => false,
    'public'           => true,
    'private'          => false,
    'proxy_revalidate' => false,
    'max_age'          => 600,
    's_maxage'         => 600,
    'immutable'        => true,
    'last_modified'    => new \DateTime(),
    'etag'             => 'abcdef',
]);

5.1

Директивы must_revalidate, no_cache, no_store, no_transform and proxy_revalidate были представлены в Symfony 5.1.

Чтобы проверить, соответствуют ли валидаторы Ответа (ETag, Last-Modified) условному значению, указанному в Запросе клиента, используйте метод isNotModified():

1
2
3
if ($response->isNotModified($request)) {
    $response->send();
}

Если Ответ не был изменён, он устанавливает статус-код 304 и удаляет настоящее содержимое ответа.

Перенаправление пользователя

Чтобы перенаправить клиента по другому URL, вы можете использовать класс RedirectResponse:

1
2
3
use Symfony\Component\HttpFoundation\RedirectResponse;

$response = new RedirectResponse('http://example.com/');

Потоковая передача ответа

Класс StreamedResponse позволяет вам создавать поток с Ответом для клиента. Содержимое ответа представляется PHP вызываемым, а не строкой:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpFoundation\StreamedResponse;

$response = new StreamedResponse();
$response->setCallback(function () {
    var_dump('Hello World');
    flush();
    sleep(2);
    var_dump('Hello World');
    flush();
});
$response->send();

Note

Функция flush() не сбрасывает буферизацию. Если ob_start() был вызван до этого, или включена опция output_buffering php.ini, то вы должны вызывать ob_flush() до flush().

Кроме того, PHP не единственный слой, буферизирующий вывод. Ваш веб-сервер может также использовать буфер, в зависимости от конфигурации. Некоторые серверы, вроде Nginx, позволяют вам отключать буферизацию на уровне конфигурации или добавлять спеицальный заголовок HTTP в ответе:

1
2
// отключает буферизацию FastCGI в Nginx только для этого ответа
$response->headers->set('X-Accel-Buffering', 'no');

Подача файлов

При отправке файла вы должны добавлять заголовок Content-Disposition к вашему ответу. И хотя создание этого заголовка для базовых загрузок файлов - это просто, использование не ASCII имён файлов требует больших усилий. makeDisposition() абстрагирует тяжелую работу, скрывающуюся за простым API:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

$fileContent = ...; // the generated file content
$response = new Response($fileContent);

$disposition = $response->headers->makeDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'foo.pdf'
);

$response->headers->set('Content-Disposition', $disposition);

Как вариант, если вы подаёте статичный файл, вы можете использовать BinaryFileResponse:

1
2
3
4
use Symfony\Component\HttpFoundation\BinaryFileResponse;

$file = 'path/to/file.txt';
$response = new BinaryFileResponse($file);

BinaryFileResponse автоматически обработает заголовки Range и If-Range из запроса. Он также поддерживаетX-Sendfile (см. Nginx и Apache). Чтобы воспользоваться этим, вам нужно определить, стоит ли доверять заголовку X-Sendfile-Type и вызвать trustXSendfileTypeHeader(), если стоит:

1
BinaryFileResponse::trustXSendfileTypeHeader();

Note

BinaryFileResponse будет обрабатывать X-Sendfile только если присутствует определенный заголовок. Для Apache, это по умолчанию не так.

Чтобы добавить заголовок, используйте модуль Apache mod_headers и добавьте следующее в конфигурацию Apache:

1
2
3
4
5
6
7
8
9
10
<IfModule mod_xsendfile.c>
  # This is already present somewhere...
  XSendFile on
  XSendFilePath ...some path...

  # This needs to be added:
  <IfModule mod_headers.c>
    RequestHeader set X-Sendfile-Type X-Sendfile
  </IfModule>
</IfModule>

С BinaryFileResponse вы можете продолжать устанавливать Content-Type отправленного файла, или изменять его Content-Disposition:

1
2
3
4
5
6
// ...
$response->headers->set('Content-Type', 'text/plain');
$response->setContentDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'filename.txt'
);

Файл можно удалить после отправки запроса методом deleteFileAfterSend(). Пожалуйста, заметьте, что это не работает, если установлен заголовок X-Sendfile.

Если размер поданого файла неизвестен (например, потому что он создаётся на лету, или потому что в нём зарегистрирован фильтр потока PHP и т.д.), то вы можете передать экземпляр Stream в BinaryFileResponse. Это отключит обработку Range и Content-Length, переключившись на механизм передачи данных chunked encoding:

1
2
3
4
5
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\Stream;

$stream  = new Stream('path/to/stream');
$response = new BinaryFileResponse($stream);

Note

Если вы только создали файл во время этого же запроса, файл может быть отправлен без содержания. Это может произойти в связи со статистикой кешированного файла, которая возвращает ноль в качестве размера файла. Чтобы исправить эту проблему, вызовите clearstatcache(true, $file) с путём к бинарному файлу.

Создание JSON-ответа

Любой тип ответа может быть создан через класс Response, путём установки правильного содержания и заголовков. JSON ответ может выглядеть так:

1
2
3
4
5
6
7
use Symfony\Component\HttpFoundation\Response;

$response = new Response();
$response->setContent(json_encode([
    'data' => 123,
]));
$response->headers->set('Content-Type', 'application/json');

Также существует полезный класс JsonResponse, который может сделать это ешё проще:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\HttpFoundation\JsonResponse;

// если вы знаете, какие данные отправлять при создании запроса
$response = new JsonResponse(['data' => 123]);

// если вы не знаете, какие данные отправлять при создании запроса
$response = new JsonResponse();
// ...
// cконфигурировать любые пользовательские опции шифрования (если необходимо, должно быть вызвано до "setData()")
//$response->setEncodingOptions(JsonResponse::DEFAULT_ENCODING_OPTIONS | \JSON_PRESERVE_ZERO_FRACTION);
$response->setData(['data' => 123]);

// если данные для отправки уже зашифрованы в JSON
$response = JsonResponse::fromJsonString('{ "data": 123 }');

Класс JsonResponse устанавливает заголовок Content-Type в application/json и шифрует ваши данные в JSON при необходимости.

Caution

Чтобы избежать XSSI перехвата JSON, вам стоит передать ассоциативный массив в JsonResponse в качестве крайнего массива, а не индексированного массива, чтобы финальный результат был объектом (например, {"object": "not inside an array"}) вместо массива (например, [{"object": "inside an array"}]). Прочтите справочник OWASP, чтобы узнать больше.

Только методы, отвечающие на запросы GET уязвимы к XSSI 'перехвату JSON'. Методы, отвечающие на запросы POST остаются неуязвимыми.

Обратный вызов JSONP

Есди вы используете JSONP, вы можете установить функцию обратного вызова, в которую должны быть переданы данные:

1
$response->setCallback('handleResponse');

В этом случае, заголовок Content-Type будет text/javascript, а содержание ответа будет выглядеть так:

1
handleResponse({'data': 123});

Сессия

Информация сессии хранится в отдельном документе: Управление сессиями.

Параметр безопасного контента

Некоторые веб-сайты имеют "безопасный" режим, чтобы помочь тем, кто не хочет видеть контент, который может их оскорбить. Спецификация RFC 8674 определяет способ, которым агенты пользователя запрашивают безопасный контент у сервера.

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

Symfony предлагает два метода взаимодействия с этим параметром:

5.1

Методы preferSafeContent() и setContentSafe() были представлены в Symfony 5.1.

Следующий пример показывает, как определить, что агент пользователя предпочитает "безопасный" контент:

1
2
3
4
5
6
7
if ($request->preferSafeContent()) {
    $response = new Response($alternativeContent);
    // сообщает пользователю, что мы учли его предпочтения
    $response->setContentSafe();

    return $response;
}

Генерирование относительных и абсолютных URL

5.4

Функция генерирования относительных и абсолютных URL была представлена в Symfony 5.4.

Генерирование абсолютных и относительных URL для заданного пути - распространенная необходимость в некоторых приложениях. В шаблонах Twig вы можете использовать функции absolute_url() и relative_path(), чтобы сделать это.

Класс UrlHelper предоставляет те же функции для PHP-кода через методы getAbsoluteUrl() и getRelativePath(). Вы можете внедрить это в качестве сервиса где угодно в вашем приложении:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Normalizer/UserApiNormalizer.php
namespace App\Normalizer;

use Symfony\Component\HttpFoundation\UrlHelper;

class UserApiNormalizer
{
    private UrlHelper $urlHelper;

    public function __construct(UrlHelper $urlHelper)
    {
        $this->urlHelper = $urlHelper;
    }

    public function normalize($user)
    {
        return [
            'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()),
        ];
    }
}

Узнайте больше