Если вы разрабатываете библиотеки, которые выполняют HTTP-запросы, то для вас тут небольшое руководство о том, как не усложнять жизнь пользователям ваших продуктов.
С появлением концепций бессерверных и микросервисных систем, федеративных механизмов аутентификации (OpenID, SAML, …) и т.д., приём и отправка HTTP запросов приобретает особое значение в веб-приложениях.
Поскольку каждый сервис предоставляет совершенно разные API, как с точки зрения технических деталей (REST, RPC, GraphQL, …) так и в плане содержания (какие поля и какого типа данные открываются), разработчики обычно прибегают к созданию пакета Composer, адаптированного под конкретный сервис. Для каждого из них.
В конце концов всё заканчивается множественными зависимостями, которые требуются для отправки и приёма HTTP сообщений. Если вы были в подобной ситуации, то вы, возможно, знаете, что множество разработчиков попадают в ловушку, которую я называю «принеси свой собственный HTTP клиент» («BYOHC» — “bring your own HTTP client”).
Свой HTTP-клиент
В последние годы группа PHP-FIG прилагает массу усилий для улучшения взаимодействий по протоколу HTTP. Первым результатом этих усилий стал стандарт PSR-7, который определяет интерфейс для HTTP сообщений. Он был принят в 2015 году и быстро получил поддержку и распространение во фреймворках и библиотеках во всей экосистеме PHP. Один шаг сделан — фреймворкам больше не нужно придумывать свои собственные, несовместимые абстракции HTTP.
В настоящее время практически любой HTTP пакет, который вы собираетесь использовать в своём приложении скорее всего опирается на интерфейс PSR-7. И здесь возникает камень преткновения: это всего лишь интерфейсы. Когда пакету необходимо выполнить HTTP запрос, то для этого ему нужно создать объект Request
. Для работы с ним простого интерфейса недостаточно; и поэтому производитель решает пойти по легкому пути и использовать одну конкретную реализацию PSR-7.
У нас есть проблема: если вам необходимо два разных пакета, которые, в свою очередь, требуют две разные реализации PSR-7, то в итоге вы получаете две разные реализации PSR-7 в вашем приложении. Без необходимости.
И, к сожалению, на этом всё не заканчивается. Даже если бы у нас был способ создания HTTP сообщений в совместимом виде, пакет всё равно должен отправлять запрос и получать ответ через HTTP клиент. Опять же, производитель решает использовать свой любимый клиент, а другой производитель выбирает другой, и в итоге у вас в приложении два HTTP клиента. Без необходимости.
Но подождите, и это еще не конец! Более сознательный разработчик может подумать: «Я не хочу жесткую зависимость от одного HTTP-клиента», и он создает интерфейс (который обычно содержит один метод send(RequestInterface): ResponseInterface
), с помощью которого предоставляет адаптер для выбранного им клиента, и на этом всё заканчивается.
В итоге вы получаете приложение, в котором вы можете выбрать один HTTP-клиент, который будет править всеми (ура!), но всё равно должны написать и поддерживать несколько адаптеров, которые, честно говоря, в основном будут отличаться только пространством имен интерфейса, в котором они реализованы. Или вы просто смиритесь с тем, что ваш каталог vendor
загроможден тремя различными HTTP клиентами и двумя различными реализациями PSR-7.
Поговорим о стандартах
Конечно, это не оптимально, и, конечно, разработчики знают об этом. Потребовалось не так много времени, чтобы появилась группа разработчиков и начала работу над определением интерфейса для HTTP клиента. В то время эта деятельность называлась HTTPlug. Она быстро завоевала популярность. Параллельно они решили и другую проблему, определив интерфейсы для фабрик сообщений PSR-7, и реализовали механизм автоматического обнаружения для простоты использования.
Короче говоря, HTTPlug был предложен в PHP-FIG, и в результате у нас появилось ещё два удобных стандарта:
- PSR-17 HTTP Factories, определяющий интерфейсы для PSR-7-совместимых фабрик, и
- PSR-18 HTTP Клиент, определяющий интерфейсы для HTTP клиента и связанные с ним исключения.
В настоящее время следующие зависимости должны быть включены разработчиком при написании пакета, взаимодействующего по HTTP. Так они выглядят в файле composer.json
:
{
"require": {
"psr/http-client": "^1.0",
"psr/http-client-implementation": "*",
"psr/http-factory": "^1.0",
"psr/http-factory-implementation": "*"
}
}
Пакеты psr/http-client
и psr/http-factory
предоставляют интерфейсы, а требование наличия двух соответствующих виртуальных пакетов (psr/http-client-implementation
и psr/http-factory-implementation
) гарантирует, что у пользователя вашего пакета установлена совместимая реализация.
Приведем пример использования этих интерфейсов:
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
final class MyServiceClient
{
private RequestFactoryInterface $requestFactory;
private StreamFactoryInterface $streamFactory;
private ClientInterface $httpClient;
public function __construct(
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
ClientInterface $httpClient
) {
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
$this->httpClient = $httpClient;
}
public function addThing(array $thing): void
{
$body = $this->streamFactory->createStream(json_encode($thing));
$request = $this->requestFactory->createRequest('POST', 'https://api.myservice.com/things')
->withHeader('Content-Type', 'application/json')
->withBody($body);
$response = $this->httpClient->sendRequest($request);
$statusCode = $response->getStatusCode();
if ($statusCode > 500) {
throw new ServiceUnavailableException();
}
if ($statusCode === 500) {
throw new ServerErrorException();
}
if ($statusCode >= 400) {
throw new ClientException();
}
}
}
Это очень схематичная реализация, но я считаю, что в ней заложен правильный посыл (хаха): вы делегируете создание объектов PSR-7 фабрикам PSR-17, и зависите от интерфейса PSR-18 для HTTP клиента. Этот код полагается на то, что ваше приложение предоставит нужные реализации с помощью инъекции зависимостей, и ему совершенно безразлично, что это за объекты, лишь бы они реализовывали требуемые интерфейсы.
Код также иллюстрирует один очень важный аспект спецификации PSR-18: HTTP клиент не выбрасывает исключение для кодов состояния не 2xx. Вы можете быть не в восторге от этого решения, но оно имеет определенный смысл, и, что самое важное, поведение теперь чётко определено. Помните сценарий выше, в котором вам нужно было написать адаптер для интерфейса каждого производителя? Не изучив код, вы не могли знать какое поведения HTTP-клиента они ожидают в подобных ситуациях, и поэтому легко могут возникнуть ошибки. Фух!
Что теперь?
Оба вышеупомянутых стандарта были приняты только в 2018 году, поэтому я полагаю, что поставщикам может потребоваться некоторое время для адаптации. Тем не менее, HTTPlug был довольно быстр в этом: они выпустили версию 2.0, которая унифицирует их клиентский интерфейс HTTP с PSR-18 и добавляет поддержку PSR-17 с помощью механизма автоматического обнаружения. Поэтому, если в пакете присутствует зависимость php-http/httplug:^2.0
, то ваши фабрики PSR-17 и PSR-18-совместимый HTTP-клиент должны прекрасно с ним работать.
Если вы пишете пакет, который должен отправлять (синхронные) HTTP запросы и получать HTTP ответы, я не вижу причин против того, чтобы придерживаться исключительно PSR-17 и PSR-18. Так что, если вы пишете что-то с нуля, здесь нечего обсуждать, просто придерживайтесь PSR с самого начала. Если вы поддерживаете существующий код, пожалуйста, подумайте о рефакторинге, как только вы сможете позволить себе выпустить версию, ломающую обратную совместимость.
Это не значит, что время HTTPlug полностью прошло - хотя я думаю, что он выполнил свою задачу, проложив путь к тому, что в конечном итоге стало широко распространенным стандартом, он также определяет интерфейс для AsyncHttpClient. Это то, что еще не охвачено PSRs, в основном потому, что пока нет стандарта для промисов в PHP. Но это может измениться в будущем.
В качестве последнего замечания, я хотел бы выразить свою бесконечную благодарность людям, стоящим за HTTPlug, и тем, кто сделал возможным продвижение его в PHP-FIG.
Перевод статьи «Don’t bring your own HTTP client» by Jiří Pudil.