HttplugClient.php 9.5 KB

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