Компонент HttpFoundation

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

Компонент 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', 'значення за замовчуванням, якщо bar не існує');

// вилучіть змінні 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, та інших).