HttplugClient.php 9.8 KB

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