В прошлом году PHP-FIG приняла стандарт PSR-7, описывающий работу с сообщениями HTTP. Хорошая статья об этом стандарте и его применении была на Хабре. И хотя PSR-7 — большой шаг вперёд, ему не хватает логичного продолжения — общего интерфейса клиентов HTTP. Созданием недостающего компонента занялась группа PHP-HTTP.
Проблема
Используя интерфейсы PSR-7 вы можете абстрагироваться от конкретной реализации запросов и ответов HTTP. Но только до тех пор, пока вам не понадобится сделать запрос самостоятельно. Тут вам придётся по-прежнему жёстко привязываться к каким-либо реализациям. При написании приложения это естественно. При написании библиотеки — совсем нехорошо. Например, наш сайт взаимодействует с пятью сторонними службами при помощи официальных и неофициальных библиотек и SDK. И каждая из них использует собственного клиента HTTP. Кто-то Guzzle, кто-то cURL, кто-то просто file_get_contents. Пять разных клиентов HTTP в одном приложении! У каждого свои особенности, свои ограничения, свои возможности настройки. Было бы здорово заменить этот зоопарк одним единственным клиентом, который использовался бы всеми компонентами приложения и библиотеками?
Httplug
Основная разработка группы — набор интерфейсов Httplug, позволяющий библиотекам абстрагироваться от конкретного клиента HTTP, используемого в приложении. Уже есть несколько реализаций клиентов (сокеты, cURL) и адаптеров (Guzzle, React). Кроме того в рамках проекта разработано множество вспомогательных пакетов, включая пакет для Symfony.
И что же это всё даёт разработчикам?
Применение в библиотеках
Если вы пишете библиотеку, которая должна выполнять запросы HTTP, у вас больше нет необходимости привязываться к конкретному клиенту. Вместо этого в ''composer.json'' можно указать:
{
"require": {
"php-http/client-implementation": "^1.0"
},
"require-dev": {
"php-http/curl-client": "^1.4"
}
}
php-http/client-implementation
указывает, что вашей библиотеке требуется клиент HTTP, php-http/curl-client
— любая на выбор реализация, которую можно будет использовать во время отладки и которая подтянет все необходимые для разработки интерфейсы и классы. Для отладки также может потребоваться реализация PSR-7, например guzzlehttp/psr7.
Предположим, что ваша библиотека должна работать с неким API, и главный компонент — клиент этого API:
class ApiClient
{
/**
* Клиент HTTP.
*/
private $httpClient;
/**
* Фабрика запросов HTTP.
*/
private $requestFactory;
public function __construct(HttpClient $httpClient, RequestFactory $requestFactory)
{
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
}
Здесь:
- HttpClient — собственно, клиент HTTP (пакет php-http/httplug);
- RequestFactory — фабрика, позволяющая создавать запросы PSR-7 (пакет php-http/message-factory).
Теперь, когда вам надо сделать запрос, можно написать что-то такое:
/**
* @param string $uri
* @param string $payload тело запроса
*/
public function apiCall($uri, $payload)
{
$request = $this->requestFactory->createRequest('POST', $uri, ['content-type' => 'foo/bar'], $payload);
$response = $this->httpClient->sendRequest($request);
// ...
}
Вот, по большому счёту, и всё, что требуется. Теперь ваша библиотека не зависит от конкретного клиента, и её пользователь сможет выбрать тот, который ему больше подходит. Посмотрим, как это делается.
Использование в приложении
Для использования описанной выше библиотеки разработчику приложения понадобится выбрать одну из реализаций клиента, реализацию PSR-7 и подключить их вместе с вашей библиотекой:
$ composer require php-http/guzzle6-adapter
$ composer require ваша/библиотека
guzzle6-adapter
автоматически подтянет guzzlehttp/psr7
, поэтому отдельно его указывать необязательно.
Ещё потребуется php-http/message в качестве моста к guzzlehttp/psr7
:
$ composer require php-http/message
Далее нужно создать клиента, адаптер, фабрику запросов и передать два последних в конструктор ApiClient:
use GuzzleHttp\Client as GuzzleClient;
use Http\Adapter\Guzzle6\Client as GuzzleAdapter;
use Http\Message\MessageFactory\GuzzleMessageFactory;
$config = [
// ...
];
$guzzle = new GuzzleClient($config);
$adapter = new GuzzleAdapter($guzzle);
$apiClient = new ApiClient($adapter, new GuzzleMessageFactory);
ApiClient готов к работе. Этот же объект ($adapter) можно передать во все компоненты, которым нужен клиент HTTP.
Если в будущем возникнет надобность заменить Guzzle на что-то другое, то переписать потребуется только вот эту часть кода. Весь остальной код, работающий с HTTP, трогать не придётся.
Что ещё есть интересного?
Полностью с имеющимися пакетами и их возможностями можно ознакомиться в официальной документации, я же отмечу только некоторые интересные на мой взгляд.
Асинхронные запросы
Специальный интерфейс HttpAsyncClient позволяет выполнять запросы асинхронно, используя механизм обещаний.
Автообнаружение реализаций (Discovery)
Система автообнаружения на основе Puli позволяет получать объекты клиента и фабрик без привязки к конкретным реализациям:
$httpClient = HttpClientDiscovery::find();
Система расширений (плагинов)
Позволяет добавлять сквозную функциональность ко всем или только некоторым клиентам HTTP. Вот некоторые примеры:
- кэширование;
- куки;
- преобразование статусов 4xx и 5xx в исключения;
- управление заголовками;
- журналирование;
HttplugBundle
Пакет для Symfony, включающий поддержку нескольких клиентов, расширений и отладочной панели Symfony.
В заключение
Проект находится в начале своего пути, но уже достаточно стабилен для использования в боевых условиях. Ребята ставят перед собой амбициозную цель — добиться принятия их разработок в качестве рекомендации PSR.
Комментарии (27)
IvanPanfilov
29.06.2016 21:08-2зачем нужен адаптер на адаптер?
и в чем проблема использования curl и file_get_open вместе — это встроенные функции
а не какие то библиотеки на сотню классов как guzzloxidmod
29.06.2016 21:19+1а зачем придерживаться стандартов? к черту PSR-7, даешь встроенные GET/POST/REQUEST
IvanPanfilov
30.06.2016 07:52-2какие то примеры реального применения есть этой абстрактной прослойки?
вот я использую http://php.net/manual/ru/book.curl.php и мне хватает — он есть из коробки у всех нормальных провайдеров и в нормальных дистрибутивах
это и есть стандартная библиотека для PHp — т.е. стандарт.
а если кому то нечего делать и вместо решения задач придумывать какие то стандарты — так ради бога.
по молодости тоже страдал этим — писал абстрактные прослойки и адаптеры. но сейчас осознал Истину.
oxidmod
30.06.2016 08:39гномкое слово «Истина». а завтра обнаружиться зеро-дей уявимость в реализации курла под пхп и вам придется на всех своих проектах менять его на чтото другое
IvanPanfilov
30.06.2016 12:47> а завтра обнаружиться зеро-дей уявимость в реализации курла под пхп
во первых тема про http- клиент:
во вторых:
а на что вы поменяете? веть вы используете всего лиш абстрактную оболочку для того же курла.
эта прослойка позволит устранить данную уязвимость?
в третьих ваша прослойка умеет делать такое же при смене «адаптера»?
http://php.net/manual/ru/function.curl-multi-init.phpFesor
01.07.2016 02:00в третьих ваша прослойка умеет делать такое же при смене «адаптера»?
да, так как все альтернативы будут построены либо поверх стримов либо поверх сокетов. А stream/socket_select в PHP тоже есть в стандартной библиотеке.
Mekras
30.06.2016 13:38какие то примеры реального применения есть этой абстрактной прослойки?
Проект молодой, поэтому примеров пока мало. Но мы используем его внутри нашей компании. Сейчас есть штук пять самостоятельных пакетов, использующих Httplug. Возможно со временем выложим их на GitHub.
вот я использую http://php.net/manual/ru/book.curl.php и мне хватает
Всё зависит от того, чем Вы занимаетесь. Мне тоже долгое время хватало. Пока жизнь не столкнула с такой ситуацией, где хватать перестало. Помните Маяковского? Если звёзды зажигают, значит это кому-нибудь нужно? Так и тут, если несколько человек разработали такую библиотеку, значит они видят в ней потребность, значит для их задач просто cURL уже не хватает.
по молодости тоже страдал этим — писал абстрактные прослойки и адаптеры. но сейчас осознал Истину.
Вы уж извините, но уверенность в обладании истиной (и уже тем более с большой буквы) характерна как раз для молодости :-)
Fesor
01.07.2016 02:10но сейчас осознал Истину.
К чертку абстракции, все можно запилить на Bitrix.
IvanPanfilov
02.07.2016 07:24Вы правы — битрикс это CMS — вещь конкретная и полезная.
а Инверсия зависимости что делает полезного?
в случае с http у меня вот нет никаких зависимостей я юзаю стандартную либу curl.
заменять на чтото другое несобираюсь — веть планов по внедрению в PHP более крутых либ вроде нет а если нет альтернатив — то обертки не нужны.Jetmanman
10.11.2016 19:09-1Держи в курсе.
IvanPanfilov
02.07.2016 16:13-1> Скажем нам нужно в десятке разных мест системы взаимодействовать с разными API
ну я создам себе класс или функцию и будут ее использовать — я могу взять при автозагрузке нужную имплементацию — в чем проблема? зачем внедрять какие то абстрактные патерны вроде инверсии зависимостей?
> Собственно я не пытаюсь вас в чем-то переубедить, это ваше мнение. Я только знаю чем грозит такое отношение в контексте средних и больших проектов и в рамках разработки продуктов.
просто надо рабивать на микросервисы — и переписывать высоконагруженные части на GoFesor
02.07.2016 16:57код с высокой связанностью поддерживать одинаково сложно на каком бы языке он небыл написан. А система с использованием микросервисов при низком зацеплении и высокой связанности обойдется вам еще дороже.
Если разработчик не понимает зачем нужны такие "абстрактные паттерны" как "инверсия зависимостей" то он вряд-ли будет в состоянии спроектировать нормально монолит, не то что микросервисы использовать.
IvanPanfilov
04.07.2016 10:26повашему коменту выходит что чтобы писать микросервисы надо знать паттерны типа инверсия зависимостей — я вообще не вижу логики здесь.
oxidmod
04.07.2016 10:35+1а по вашему выходит, что вам вообще не стоит писать код сложней плагинов/модулей для битркса и вп) и то не факт
Fesor
04.07.2016 11:12ну тип того, и это не паттерн а принцип. Такой же как инкапсуляция или там принцип единой ответственности (на микросервисы тоже распространяется). Если вы не видите в этом логики — значит вы не можете применить те же правила на макро уровне (на уровне компонентов и микросервисов), а стало быть либо у вас все неплохо с чутьем (такое тоже бывает) и здравым смыслом, и вы эти паттерны и принципы применяете даже не зная об том, либо все очень плохо или будет очень плохо.
p.s. Вы сами то работали с микросервисной архитектурой? И давайте так, если база данных общая хотя бы для двух микросервисов — это уже не считается.
Mekras
30.06.2016 13:18Это философский вопрос из разряда «А зачем нужны библиотеки»? Как правило для облегчения жизни программиста. Понятно, что всё то, что делает библиотека, можно написать самому. При этом кода будет меньше, чем в библиотеке, да и производительнее вполне может получиться. Но тут есть и оборотная сторона. Во-первых, использование библиотек повышает скорость разработки, что с каждым днём становится всё важнее. Во-вторых, как заметил один римский оратор, человеку свойственно ошибаться. Чем больше программист напишет кода, тем выше вероятность появления ошибки. Повторное использование кода — один из способов понижения этой вероятности.
Ну и если вернуться к Httplug, то особенно начинаешь его ценить по мере усложнения проектов, с которыми работаешь. В начале заметки я уже описал некоторые проблемы, с которыми мы столкнулись. Кроме того, благодаря переводу части компонентов на Httplug, мы получили возможность легко, в одной точке приложения, добавлять нужную нам функциональность. Например журналировать запросы/ответы для отладки и профилирования. Более того, мы можем включать некоторые вещи только в тестовой конфигурации и отключать в боевой. Это очень удобно и бережёт много времени и сил.IvanPanfilov
30.06.2016 17:08тоесть обертка guzzle не позволяет абстрагироваться от конкретной реализации и позволять навешивания логирования и прочих обработчиков — хотя вроде как предназначена как раз для этого
и вы решили вместо патчей в guzzle написать еще одну обертку над оберткой? «we need to go deeper»
тогда я видимо совсем не понимаю в разработке и предназначении опенсорсных библиотек.Mekras
30.06.2016 18:50Guzzle — это реализация. Httplug — интерфейс. Надо объяснять разницу?
И, к слову, написать решил не я. Но задумку считаю нужной.IvanPanfilov
30.06.2016 19:17> Guzzle — это реализация. Httplug — интерфейс.
тоесть это штука ровным счетом ничего не делает правильно?
> Как правило для облегчения жизни программиста
если эта штука — ничего не делает то каким образом она облегчает жизнь программиста?
и для чего такие библиотеки создают?Fesor
01.07.2016 02:07+2если эта штука — ничего не делает то каким образом она облегчает жизнь программиста?
Инверсия зависимости.
AaAAxzz
01.07.2016 15:43Хорошая работа, но все же можно пару вопросов задать по коду? (ибо у меня вызывает сомнения)
1. «HttpClient» это класс или интерфейс? Почему бы не сделать рефакторинг
«HttpClient» => «HttpClientInterface» -> «HttpClientInterface.php»
«HttpAsyncClient» => «HttpAsyncClientInterface» -> «HttpAsyncClientInterface.php»
«RequestFactory» => «RequestFactoryInterface» -> «RequestFactoryInterface.php»
и т.д.
2. Для чего нужен puli? Это работает на windows? (у меня нет, к сожалению — как быть ?)
3. Нашел ошибку в адаптере curl — связано с отправкой асинхронных запросов — если добавить 2 одинаковых URL адреса в очередь (возможно не подряд) то выполнение второго прервется? файл MultiRunner.php строки 109-112
4. Зачем везде свойства помечены как «private»? Думается мне это может помешать, если в рамках проекта вдруг понадобиться сделать extends. Пожалуйста, если не трудно, хотелось бы услышать мнения, может я не понимаю или не прав
5. Также нашел ошибку в curl клиенте — клиент позволяется правильно отправить только GET, POST и HEAD? А что с остальным HTTP, например методы WebDav: PROPFIND, UNLOCK, MOVE, COPY и т.д. такие запросы нельзя отправить?
6. Опять же curl, метод PATCH по спецификации позволяет отправить тело? Curl адаптер не отправляет тело?
Эти вопросы можно пофиксить так:
Файл Client.php метод createCurlOptions (почему бы не сделать метод «protected» вместо «private» ?)
if (in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE', 'CONNECT'])) {
// тело не отправляем
} else {
// ->createCurlBody()
// опцию CURLOPT_INFILESIZE использовать НЕ нужно, чтобы избежать проблем с родными потоками, такими как zlib.
}
Прошу меня извинить, что я пишу это тут.Mekras
01.07.2016 16:431,2. Можно спросить авторов тут.
3. С чего бы ему прерываться?
4. Любые неприватные члены класса фактически становятся частью контракта этого класса, со всеми вытекающими последствиями в виде необходимости контроля данных, обеспечения совместимости между версиями и прочим, что неизбежно приводит к усложнению кода. При том что необходимость наследования этого класса представляется крайне маловероятной, соотношение накладных расходов к полученной выгоде представляется не очень разумным.
5. С чего Вы это взяли?
6. Сходу ответить не готов. Можете написать сюда.tretyakovpe
10.11.2016 15:36-1что непонятно? у вас передача инфы из прошлого в будущее, а тут разговор об обратном. т.е. вы должны получить «флэшбэк» о том, как через несколько секунд будете класть яблоко.
Может эффект дежавю это как раз они, послания из будущего?
Fesor
01.07.2016 17:30«HttpClient» это класс или интерфейс?
Это тип объекта в первую очередь. Вам не должно быть разницы интерфейс это или класс.
Для чего нужен puli? Это работает на windows? (у меня нет, к сожалению — как быть ?)
прочитайте статью (эту и по puli).
Зачем везде свойства помечены как «private»? Думается мне это может помешать, если в рамках проекта вдруг понадобиться сделать extends. Пожалуйста, если не трудно, хотелось бы услышать мнения, может я не понимаю или не прав
потому что не нужно их экстендить.
Как только вы делаете проперти объекта protected вы по сути делаете это частью "публичного" интерфейса библиотеки. А стало быть вы не сможете иметь в любой момент времени "поправить" реализацию. Чем меньше публичного — тем лучше и проще дальше поддерживать код библиотеки.
Опять же curl, метод PATCH по спецификации позволяет отправить тело?
конечно же, это же PATCH. Он "патчит" ресурсы.
GoldJee
10.11.2016 15:48С точки зрения физика это параметр в уравнениях движения. Координата в четырехмерном пространстве, если хотите.
questor
Любопытная инициатива, присмотрюсь.
Irony_mode: Кто первым кинет в комменты картинку «Есть 14 различных стандартов....»? :)
zelenin
здесь речь все-таки не об очередном стандарте, а о попытке абстрагироваться от конкретной реализации