HttplugClient.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient;
  11. use GuzzleHttp\Promise\Promise as GuzzlePromise;
  12. use GuzzleHttp\Promise\RejectedPromise;
  13. use GuzzleHttp\Promise\Utils;
  14. use Http\Client\Exception\NetworkException;
  15. use Http\Client\Exception\RequestException;
  16. use Http\Client\HttpAsyncClient;
  17. use Http\Client\HttpClient as HttplugInterface;
  18. use Http\Discovery\Exception\NotFoundException;
  19. use Http\Discovery\Psr17FactoryDiscovery;
  20. use Http\Message\RequestFactory;
  21. use Http\Message\StreamFactory;
  22. use Http\Message\UriFactory;
  23. use Http\Promise\Promise;
  24. use Nyholm\Psr7\Factory\Psr17Factory;
  25. use Nyholm\Psr7\Request;
  26. use Nyholm\Psr7\Uri;
  27. use Psr\Http\Message\RequestFactoryInterface;
  28. use Psr\Http\Message\RequestInterface;
  29. use Psr\Http\Message\ResponseFactoryInterface;
  30. use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
  31. use Psr\Http\Message\StreamFactoryInterface;
  32. use Psr\Http\Message\StreamInterface;
  33. use Psr\Http\Message\UriFactoryInterface;
  34. use Psr\Http\Message\UriInterface;
  35. use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
  36. use Symfony\Component\HttpClient\Response\HttplugPromise;
  37. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  38. use Symfony\Contracts\HttpClient\HttpClientInterface;
  39. use Symfony\Contracts\HttpClient\ResponseInterface;
  40. use Symfony\Contracts\Service\ResetInterface;
  41. if (!interface_exists(HttplugInterface::class)) {
  42. throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
  43. }
  44. if (!interface_exists(RequestFactory::class)) {
  45. throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
  46. }
  47. /**
  48. * An adapter to turn a Symfony HttpClientInterface into an Httplug client.
  49. *
  50. * Run "composer require nyholm/psr7" to install an efficient implementation of response
  51. * and stream factories with flex-provided autowiring aliases.
  52. *
  53. * @author Nicolas Grekas <p@tchwork.com>
  54. */
  55. final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory, ResetInterface
  56. {
  57. private $client;
  58. private $responseFactory;
  59. private $streamFactory;
  60. /**
  61. * @var \SplObjectStorage<ResponseInterface, array{RequestInterface, Promise}>|null
  62. */
  63. private $promisePool;
  64. private $waitLoop;
  65. public function __construct(HttpClientInterface $client = null, ResponseFactoryInterface $responseFactory = null, StreamFactoryInterface $streamFactory = null)
  66. {
  67. $this->client = $client ?? HttpClient::create();
  68. $this->responseFactory = $responseFactory;
  69. $this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
  70. $this->promisePool = class_exists(Utils::class) ? new \SplObjectStorage() : null;
  71. if (null === $this->responseFactory || null === $this->streamFactory) {
  72. if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
  73. throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
  74. }
  75. try {
  76. $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
  77. $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
  78. $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
  79. } catch (NotFoundException $e) {
  80. throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
  81. }
  82. }
  83. $this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory);
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function sendRequest(RequestInterface $request): Psr7ResponseInterface
  89. {
  90. try {
  91. return $this->waitLoop->createPsr7Response($this->sendPsr7Request($request));
  92. } catch (TransportExceptionInterface $e) {
  93. throw new NetworkException($e->getMessage(), $request, $e);
  94. }
  95. }
  96. /**
  97. * {@inheritdoc}
  98. *
  99. * @return HttplugPromise
  100. */
  101. public function sendAsyncRequest(RequestInterface $request): Promise
  102. {
  103. if (!$promisePool = $this->promisePool) {
  104. throw new \LogicException(sprintf('You cannot use "%s()" as the "guzzlehttp/promises" package is not installed. Try running "composer require guzzlehttp/promises".', __METHOD__));
  105. }
  106. try {
  107. $response = $this->sendPsr7Request($request, true);
  108. } catch (NetworkException $e) {
  109. return new HttplugPromise(new RejectedPromise($e));
  110. }
  111. $waitLoop = $this->waitLoop;
  112. $promise = new GuzzlePromise(static function () use ($response, $waitLoop) {
  113. $waitLoop->wait($response);
  114. }, static function () use ($response, $promisePool) {
  115. $response->cancel();
  116. unset($promisePool[$response]);
  117. });
  118. $promisePool[$response] = [$request, $promise];
  119. return new HttplugPromise($promise);
  120. }
  121. /**
  122. * Resolves pending promises that complete before the timeouts are reached.
  123. *
  124. * When $maxDuration is null and $idleTimeout is reached, promises are rejected.
  125. *
  126. * @return int The number of remaining pending promises
  127. */
  128. public function wait(float $maxDuration = null, float $idleTimeout = null): int
  129. {
  130. return $this->waitLoop->wait(null, $maxDuration, $idleTimeout);
  131. }
  132. /**
  133. * {@inheritdoc}
  134. */
  135. public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
  136. {
  137. if ($this->responseFactory instanceof RequestFactoryInterface) {
  138. $request = $this->responseFactory->createRequest($method, $uri);
  139. } elseif (class_exists(Request::class)) {
  140. $request = new Request($method, $uri);
  141. } elseif (class_exists(Psr17FactoryDiscovery::class)) {
  142. $request = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
  143. } else {
  144. throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
  145. }
  146. $request = $request
  147. ->withProtocolVersion($protocolVersion)
  148. ->withBody($this->createStream($body))
  149. ;
  150. foreach ($headers as $name => $value) {
  151. $request = $request->withAddedHeader($name, $value);
  152. }
  153. return $request;
  154. }
  155. /**
  156. * {@inheritdoc}
  157. */
  158. public function createStream($body = null): StreamInterface
  159. {
  160. if ($body instanceof StreamInterface) {
  161. return $body;
  162. }
  163. if (\is_string($body ?? '')) {
  164. $stream = $this->streamFactory->createStream($body ?? '');
  165. } elseif (\is_resource($body)) {
  166. $stream = $this->streamFactory->createStreamFromResource($body);
  167. } else {
  168. throw new \InvalidArgumentException(sprintf('"%s()" expects string, resource or StreamInterface, "%s" given.', __METHOD__, get_debug_type($body)));
  169. }
  170. if ($stream->isSeekable()) {
  171. $stream->seek(0);
  172. }
  173. return $stream;
  174. }
  175. /**
  176. * {@inheritdoc}
  177. */
  178. public function createUri($uri): UriInterface
  179. {
  180. if ($uri instanceof UriInterface) {
  181. return $uri;
  182. }
  183. if ($this->responseFactory instanceof UriFactoryInterface) {
  184. return $this->responseFactory->createUri($uri);
  185. }
  186. if (class_exists(Uri::class)) {
  187. return new Uri($uri);
  188. }
  189. if (class_exists(Psr17FactoryDiscovery::class)) {
  190. return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
  191. }
  192. throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
  193. }
  194. public function __sleep(): array
  195. {
  196. throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  197. }
  198. public function __wakeup()
  199. {
  200. throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  201. }
  202. public function __destruct()
  203. {
  204. $this->wait();
  205. }
  206. public function reset()
  207. {
  208. if ($this->client instanceof ResetInterface) {
  209. $this->client->reset();
  210. }
  211. }
  212. private function sendPsr7Request(RequestInterface $request, bool $buffer = null): ResponseInterface
  213. {
  214. try {
  215. $body = $request->getBody();
  216. if ($body->isSeekable()) {
  217. $body->seek(0);
  218. }
  219. $options = [
  220. 'headers' => $request->getHeaders(),
  221. 'body' => $body->getContents(),
  222. 'buffer' => $buffer,
  223. ];
  224. if ('1.0' === $request->getProtocolVersion()) {
  225. $options['http_version'] = '1.0';
  226. }
  227. return $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
  228. } catch (\InvalidArgumentException $e) {
  229. throw new RequestException($e->getMessage(), $request, $e);
  230. } catch (TransportExceptionInterface $e) {
  231. throw new NetworkException($e->getMessage(), $request, $e);
  232. }
  233. }
  234. }