Чому для виконання запитів до API слід використовувати HTTP-функції WordPress

Іноді WordPress-сайт повинен взаємодіяти з іншими веб-сервісами. Зазвичай це здійснюється за допомогою HTTP-протоколу. Типовий приклад: ваша установка WordPress зв’язується з серверами wordpress.org для перевірки наявності нових версій плагінів, а також ядра WP.

Нерідко подібну взаємодію можна бачити в плагінах і темах WordPress. Будь-плагін, який взаємодіє із зовнішнім сервісом, буде виконувати деякі HTTP-запити. Додавання передплатників в ваш список Mailchimp, відправка email через Amazon SES, вивантаження зображень в Amazon S3 – для всього цього слід дотримуватися кількох HTTP-запитів.

У цій статті ми детально розглянемо вбудовані HTTP-функції, які пропонує WordPress для виконання зазначених запитів. Також ми торкнемося питання, чому завжди варто звертатися саме до цих функцій.

Однак для початку трохи передісторії.

Помилковий шлях: використання PHP-функцій

PHP пропонує кілька різних способів виконання HTTP-запитів. Ви можете безпосередньо працювати з мережевими функціями для ваших HTTP-запитів, такими як fsockopen(), fread(), fwrite(), але в кінцевому рахунку ви просто втратите час і винайдете велосипед без будь-якої на те причини.

Підхід трохи кращі – використання однієї з функцій читання файлів. Ці функції можуть обробляти оболонку HTTP-протоколу в PHP. До них відноситься кілька громіздка функція fopen(), а також більш прості file() і file_get_contents(). Приклад простого однострочного коду:

// Example code only, don’t do this!
$response = file_get_contents('https://www.google.com/');

Але перш ніж вставляти цей код в свій WordPress-плагін або в дочірню тему, майте на увазі, що багато хостинг-провайдери забороняють таке відкриття URL-адрес. Те, що працює у вашій локальній середовищі розробки, може не працювати у вашому продакшн.

Також варто розуміти, що навіть якщо за допомогою PHP-функцій можна отримати контент з URL-адреси в мережі, це не означає, що такий підхід є найкращим в розробці. Просте, здавалося б, вимога вказати заголовок в HTTP-запиті помітно ускладнить Наведений вище приклад (приклад узятий з Stack Overflow):

// Example code only, don’t do this!
// Create a stream
$opts = array(
  "http" => array(
    "method" => "GET",
    "header" => "Accept-language: enrn",
  )
);
 ​
// DOCS: https://www.php.net/manual/en/function.stream-context-create.php
$context = stream_context_create( $opts );
 ​
// Open the file using the HTTP headers set above
// DOCS: https://www.php.net/manual/en/function.file-get-contents.php
$file = file_get_contents( 'https://www.google.com/', false, $context );

Вже не так просто. При цьому врахуйте, що в даному прикладі немає обробки помилок. Якщо ви не впевнені на 100%, що віддалений сервер дасть коректну відповідь (підказка: у вас не може бути такої впевненості), вам доведеться додати додатковий код для обгортки file_get_contents(), і тільки потім вже код можна буде використовувати в робочому середовищі.

Велика кількість PHP HTTP-бібліотек

Оскільки виконання HTTP-запитів за допомогою сирого PHP є досить незручним процесом, не дивно, що є десятки допоміжних бібліотек, які спрощують створення HTTP-запитів.

Якщо розглядати більш широкий світ PHP, що виходить за межі екосистеми WordPress (де використовується Composer для управління залежностями), можна виділити популярні бібліотеки Guzzle і Httpful для обробки HTTP-запитів. При цьому є й інші хороші рішення. У всіх є свої сильні і слабкі сторони.

У світі WordPress потрібно проявляти деяку обережність при використанні Composer-бібліотек, особливо якщо ви хочете потім передавати код іншим WordPress-юзерам. На те є дві причини.

Перша причина: є реальний ризик того, що ви або ваші користувачі зав’язнете в трясовині залежностей Composer. Таке відбувається, коли будь-який інший плагін в вашій установці WordPress використовує ту ж залежність, що і ваш плагін, однак той плагін працює з іншою версією цієї залежності.

У WordPress використовується детермінований спосіб завантаження плагінів. Технічно можна зламати порядок плагінів, проте робити це не рекомендується. Крім того, будь-які виверти і трюки ви ні використовували, щоб змусити ваш плагін завантажуватися першим, інші розробники можуть надходити рівно таким же чином. Тому ви ніколи не дізнаєтеся, чи буде ваш плагін використовувати передані вами версії залежності, або ж він буде використовувати якусь іншу – можливо, несумісну – версію.

Є способи обійти це, використовуючи плагін Imposter – з його допомогою можна обернути все залежності Composer в ваше власне простір імен PHP. Звичайно, можна цілком піти цим шляхом. Але зауважте, як, здавалося б, проста ідея використовувати пакет Composer для управління HTTP-запитами перетворилася в більш серйозну проблему, яка, швидше за все, істотно вплине на ваш процес складання.

Простий приклад: в плагіні WP Offload Media використовуються сторонні бібліотеки підтримуваних сховищ (Amazon AWS, Digital Ocean і т.д.). Вони, в свою чергу, використовують HTTP-бібліотеку Guzzle. Щоб уникнути проблем із сумісністю з іншими плагінами, в WP Offload Media використовується Imposter для обгортки всіх залежностей в окремий простір імен.

Друга причина, чому не варто використовувати сторонні бібліотеки для HTTP-запитів в WordPress: перестане працювати фільтрація.

Одним з плюсів WordPress є те, що багато в системі можна налаштувати за допомогою дій і фільтрів. Виконання HTTP-запитів не є винятком. Ми розглянемо більш докладно це в наступному розділі. Не варто видаляти цю можливість, якщо у вас на те немає залізобетонної причини.

wp_remote_get, wp_remote_post і т.д.

Протягом останніх 12 років WordPress поставлявся з вбудованими функціями для роботи з HTTP-запитами: wp_remote_get(), wp_remote_post() і wp_remote_head(). За однією функції для кожного дієслова GET, POST и HEAD. Всі три глобальні функції працюють як оболонки для класу WP_Http, який, в свою чергу, використовує бібліотеку Requests.

Виклик wp_remote_get() дуже простий:

$response = wp_remote_get( 'https://www.google.com/' );

Є безліч причин, чому ви повинні віддати перевагу функції з сімейства wp_remote_*. Ось найважливіші з них.

Вони завжди під рукою

Функції wp_remote_* присутні в WP практично з самого появи движка, тому немає абсолютно ніякої необхідності переживати про те, що вони кудись пропадуть.

Деякі основоположні можливості ядра WordPress, такі як установка плагінів і автоматичні оновлення, покладаються на ці функції. Немає ніякої реальної причини додавати сторонні бібліотеки для HTTP-запитів. У вас вже є доступ до перевіреної, надійної бібліотеці в ядрі.

Вони підходять під різні потреби

Ви можете отримати доступ практично до всіх можливостей протоколу HTTP, передавши додаткові аргументи через другий параметр цих функцій.

Ви можете додавати заголовки, змінювати рядок користувацького агента, встановлювати певну версію протоколу HTTP і т.д. Ви можете вказати своє місце розташування для SSL-сертифікатів, відключити їх перевірку, змінити час очікування запиту (тайм-аут) або навіть додати HTTP-дієслова, які вам потрібні. Ми розглянемо додавання оболонок для двох HTTP-дієслів нижче в цьому пості.

Ось приклад, в якому я трохи змінюю HTTP-запит перед його відправкою на віддалений сервер:

$args = array(
  // Increase the timeout from the default of 5 to 10 seconds
  'timeout'    => 10,

  // Overwrite the default: "WordPress/5.8;www.mysite.tld" header:
  'user-agent' => 'My special WordPress installation',

  // Add a couple of custom HTTP headers
  'headers'    => array(
     'X-Custom-Id' => 'ABC123',
     'X-Secret-Thing' => 'secret',
  ),

  // Skip validating the HTTP servers SSL cert;
  'sslverify' => false,
);

$response = wp_remote_get( 'https://www.example.com/', $args );

Повний список допустимих аргументів ніде не задокументовано, але його можна знайти в вихідному файлі для класу HTTP (або в цьому корисному коментарі).

Коротше кажучи, wp_remote_get(), wp_remote_post() і wp_remote_head() здатні обробляти більшість варіантів використання HTTP-запитів. Тому на практиці існує лише малий відсоток ситуацій, коли вам буде потрібно звертатися до сторонньої HTTP-бібліотеці в WordPress.

За допомогою базової бібліотеки Requests можна навіть використовувати вбудовані функції для одночасного виконання декількох запитів, але це виходить за рамки даного поста.

Їх легко налаштувати

Під час виконання HTTP-запиту всі функції wp_remote_ * викликатимуть безліч різних WordPress-фільтрів, які дозволять вам та іншим користувачам задавати точне поведінку або навіть блокувати запити зовнішніх URL-адрес.

Ви можете не тільки детально контролювати свої власні HTTP-запити, але і втручатися в HTTP-запити, що здійснюються іншими плагінами або навіть самим ядром WP.

Уявіть, що ви відповідаєте за кілька внутрішніх установок WordPress у великій компанії. Щоб відповідати прийнятим правилам IT-безпеки, вам необхідно приховати URL-адресу сайту, який автоматично додається в рядок користувацького агента в HTTP-запити. Як цього добитися?

За допомогою ініціювання фільтра http_request_args перед відправкою запиту. Приклад:

function my_http_request_args($args) {
  $args['user-agent'] = 'WordPress';

  return $args;
}
add_filter('http_request_args', 'my_http_request_args');

Деяким власникам сайтів може знадобитися повністю заблокувати HTTP-трафік на зовнішні URL-адреси. Всі запити, які виконуються з використанням wp_remote_get(), wp_remote_post() і wp_remote_head(), будуть враховувати константи WP_HTTP_BLOCK_EXTERNAL і the WP_ACCESSIBLE_HOSTS.

Якщо вони визначені, ці константи дозволяють власникам сайтів блокувати весь вихідний трафік, але при бажанні зробити набір виключень:

define( 'WP_HTTP_BLOCK_EXTERNAL', true );
define( 'WP_ACCESSIBLE_HOSTS', 'api.example.com,www.example.com' );

Запит до будь-якого іншого хосту тепер буде повертати WP_Error:

$response = wp_remote_get( 'https://www.google.com' );

/* Returns:
WP_Error Object
(
    [errors] => Array
    (
      [http_request_not_executed] => Array
      (
        [0] => User has blocked requests through HTTP.
      )
    )
  …
)*/

За допомогою вбудованих HTTP-функцій ви даєте своїм користувачам низькорівневий контроль над HTTP-запитами, не написавши жодного рядка коду для цього. Чудово!

Вони не прив’язані до якого-небудь типу транспорту

Особливість бібліотеки Requests полягає в тому, що вона не залежить від типу транспорту (транспортно-агностична структура). Зокрема, якщо говорити про HTTP-запитів, працездатність бібліотеки не залежить від того, чи встановлений cURL на сервері.

Бібліотека Requests буде використовувати cURL, якщо він встановлений. Але якщо його не буде (в окремих випадках таке зустрічається), то тоді бібліотека відкотиться до використання fsockopen().

У будь-якій ситуації бібліотека Requests виконає свою роботу, тому у вас буде на одну залежність менше.

Що з приводу wp_remote_put і wp_remote_delete?

Якщо ви вже встигли попрацювати з віддаленими API і знаєте, як функціонують HTTP-дієслова, ви, можливо, помітили, що я до сих пір не згадував wp_remote_put() і wp_remote_delete(). Оскільки у нас є вбудовані реалізації для GET, POST і HEAD, очевидно, що повинні бути реалізації і для PUT і DELETE. Вірно?

Ні. Команда WordPress Core ще не реалізувала всі стандартні HTTP-дієслова. Але, можливо, вони з’являться в наступних релізах WP, так як обговорення їх ведеться.

Якщо вам потрібні ці функції вже зараз, ви можете легко додати їх за допомогою класу WP_Http в поєднанні з оболонкою wp_remote_request:

if ( ! function_exists( 'wp_remote_put' ) ) {
  function wp_remote_put($url, $args) {
     $defaults = array('method' => 'PUT');
     $r = wp_parse_args( $args, $defaults );
     return wp_remote_request($url, $r);
  }
}

if ( ! function_exists( 'wp_remote_delete' ) ) {
  function wp_remote_delete($url, $args) {
     $defaults = array('method' => 'DELETE');
     $r = wp_parse_args( $args, $defaults );
     return wp_remote_request($url, $r);
  }
}

Зверніть увагу, що включення цих оголошень функцій в function_exists() дуже важливо, оскільки є цілком реальна ймовірність того, що інший плагін на вашому сайті вже їх реалізував. Також це стане захистом від різних проблем, пов’язаних з можливою появою цих реалізацій в ядрі WP в майбутньому.

Прокоментувати

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *