Компонент HttpFoundation

Дата оновлення перекладу 2022-04-30

Компонент 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 з 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();

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

Куки відповіді можуть бути змінені через публічний атрибут headers:

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

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

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

Ви можете очистити куки методом clearCookie().

В додаток до методу Cookie::create(), ви можете створити об'єкт Cookie з початкового значення заголовку, використовуючи метод fromString(). Ви також можете використовувати методи with*(), щоб змінити якусь властивість куки (або створити весь куки, використовуючи вільний інтерфейс). Кожний метод 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 та 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>
  # Це вже десь присутнє...
  XSendFile on
  XSendFilePath ...some path...

  # Це необхідно додати:
  <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();
// ...
// сконфігурувати будь-які користувацькі опції шифрування (якщо необхідно, має бути викликане до "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()),
        ];
    }
}

Дізнайтеся більше