MockResponse.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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\Response;
  11. use Symfony\Component\HttpClient\Chunk\ErrorChunk;
  12. use Symfony\Component\HttpClient\Chunk\FirstChunk;
  13. use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
  14. use Symfony\Component\HttpClient\Exception\TransportException;
  15. use Symfony\Component\HttpClient\Internal\ClientState;
  16. use Symfony\Contracts\HttpClient\ResponseInterface;
  17. /**
  18. * A test-friendly response.
  19. *
  20. * @author Nicolas Grekas <p@tchwork.com>
  21. */
  22. class MockResponse implements ResponseInterface, StreamableInterface
  23. {
  24. use CommonResponseTrait;
  25. use TransportResponseTrait {
  26. doDestruct as public __destruct;
  27. }
  28. private $body;
  29. private $requestOptions = [];
  30. private $requestUrl;
  31. private $requestMethod;
  32. private static $mainMulti;
  33. private static $idSequence = 0;
  34. /**
  35. * @param string|string[]|iterable $body The response body as a string or an iterable of strings,
  36. * yielding an empty string simulates an idle timeout,
  37. * throwing an exception yields an ErrorChunk
  38. *
  39. * @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
  40. */
  41. public function __construct($body = '', array $info = [])
  42. {
  43. $this->body = is_iterable($body) ? $body : (string) $body;
  44. $this->info = $info + ['http_code' => 200] + $this->info;
  45. if (!isset($info['response_headers'])) {
  46. return;
  47. }
  48. $responseHeaders = [];
  49. foreach ($info['response_headers'] as $k => $v) {
  50. foreach ((array) $v as $v) {
  51. $responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
  52. }
  53. }
  54. $this->info['response_headers'] = [];
  55. self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
  56. }
  57. /**
  58. * Returns the options used when doing the request.
  59. */
  60. public function getRequestOptions(): array
  61. {
  62. return $this->requestOptions;
  63. }
  64. /**
  65. * Returns the URL used when doing the request.
  66. */
  67. public function getRequestUrl(): string
  68. {
  69. return $this->requestUrl;
  70. }
  71. /**
  72. * Returns the method used when doing the request.
  73. */
  74. public function getRequestMethod(): string
  75. {
  76. return $this->requestMethod;
  77. }
  78. /**
  79. * {@inheritdoc}
  80. */
  81. public function getInfo(string $type = null)
  82. {
  83. return null !== $type ? $this->info[$type] ?? null : $this->info;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function cancel(): void
  89. {
  90. $this->info['canceled'] = true;
  91. $this->info['error'] = 'Response has been canceled.';
  92. try {
  93. $this->body = null;
  94. } catch (TransportException $e) {
  95. // ignore errors when canceling
  96. }
  97. $onProgress = $this->requestOptions['on_progress'] ?? static function () {};
  98. $dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0);
  99. $onProgress($this->offset, $dlSize, $this->info);
  100. }
  101. /**
  102. * {@inheritdoc}
  103. */
  104. protected function close(): void
  105. {
  106. $this->inflate = null;
  107. $this->body = [];
  108. }
  109. /**
  110. * @internal
  111. */
  112. public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
  113. {
  114. $response = new self([]);
  115. $response->requestOptions = $options;
  116. $response->id = ++self::$idSequence;
  117. $response->shouldBuffer = $options['buffer'] ?? true;
  118. $response->initializer = static function (self $response) {
  119. return \is_array($response->body[0] ?? null);
  120. };
  121. $response->info['redirect_count'] = 0;
  122. $response->info['redirect_url'] = null;
  123. $response->info['start_time'] = microtime(true);
  124. $response->info['http_method'] = $method;
  125. $response->info['http_code'] = 0;
  126. $response->info['user_data'] = $options['user_data'] ?? null;
  127. $response->info['max_duration'] = $options['max_duration'] ?? null;
  128. $response->info['url'] = $url;
  129. if ($mock instanceof self) {
  130. $mock->requestOptions = $response->requestOptions;
  131. $mock->requestMethod = $method;
  132. $mock->requestUrl = $url;
  133. }
  134. self::writeRequest($response, $options, $mock);
  135. $response->body[] = [$options, $mock];
  136. return $response;
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. protected static function schedule(self $response, array &$runningResponses): void
  142. {
  143. if (!$response->id) {
  144. throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
  145. }
  146. $multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
  147. if (!isset($runningResponses[0])) {
  148. $runningResponses[0] = [$multi, []];
  149. }
  150. $runningResponses[0][1][$response->id] = $response;
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. protected static function perform(ClientState $multi, array &$responses): void
  156. {
  157. foreach ($responses as $response) {
  158. $id = $response->id;
  159. if (null === $response->body) {
  160. // Canceled response
  161. $response->body = [];
  162. } elseif ([] === $response->body) {
  163. // Error chunk
  164. $multi->handlesActivity[$id][] = null;
  165. $multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
  166. } elseif (null === $chunk = array_shift($response->body)) {
  167. // Last chunk
  168. $multi->handlesActivity[$id][] = null;
  169. $multi->handlesActivity[$id][] = array_shift($response->body);
  170. } elseif (\is_array($chunk)) {
  171. // First chunk
  172. try {
  173. $offset = 0;
  174. $chunk[1]->getStatusCode();
  175. $chunk[1]->getHeaders(false);
  176. self::readResponse($response, $chunk[0], $chunk[1], $offset);
  177. $multi->handlesActivity[$id][] = new FirstChunk();
  178. $buffer = $response->requestOptions['buffer'] ?? null;
  179. if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
  180. $response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
  181. }
  182. } catch (\Throwable $e) {
  183. $multi->handlesActivity[$id][] = null;
  184. $multi->handlesActivity[$id][] = $e;
  185. }
  186. } elseif ($chunk instanceof \Throwable) {
  187. $multi->handlesActivity[$id][] = null;
  188. $multi->handlesActivity[$id][] = $chunk;
  189. } else {
  190. // Data or timeout chunk
  191. $multi->handlesActivity[$id][] = $chunk;
  192. }
  193. }
  194. }
  195. /**
  196. * {@inheritdoc}
  197. */
  198. protected static function select(ClientState $multi, float $timeout): int
  199. {
  200. return 42;
  201. }
  202. /**
  203. * Simulates sending the request.
  204. */
  205. private static function writeRequest(self $response, array $options, ResponseInterface $mock)
  206. {
  207. $onProgress = $options['on_progress'] ?? static function () {};
  208. $response->info += $mock->getInfo() ?: [];
  209. // simulate "size_upload" if it is set
  210. if (isset($response->info['size_upload'])) {
  211. $response->info['size_upload'] = 0.0;
  212. }
  213. // simulate "total_time" if it is not set
  214. if (!isset($response->info['total_time'])) {
  215. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  216. }
  217. // "notify" DNS resolution
  218. $onProgress(0, 0, $response->info);
  219. // consume the request body
  220. if (\is_resource($body = $options['body'] ?? '')) {
  221. $data = stream_get_contents($body);
  222. if (isset($response->info['size_upload'])) {
  223. $response->info['size_upload'] += \strlen($data);
  224. }
  225. } elseif ($body instanceof \Closure) {
  226. while ('' !== $data = $body(16372)) {
  227. if (!\is_string($data)) {
  228. throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
  229. }
  230. // "notify" upload progress
  231. if (isset($response->info['size_upload'])) {
  232. $response->info['size_upload'] += \strlen($data);
  233. }
  234. $onProgress(0, 0, $response->info);
  235. }
  236. }
  237. }
  238. /**
  239. * Simulates reading the response.
  240. */
  241. private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
  242. {
  243. $onProgress = $options['on_progress'] ?? static function () {};
  244. // populate info related to headers
  245. $info = $mock->getInfo() ?: [];
  246. $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
  247. $response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
  248. $dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
  249. $response->info = [
  250. 'start_time' => $response->info['start_time'],
  251. 'user_data' => $response->info['user_data'],
  252. 'max_duration' => $response->info['max_duration'],
  253. 'http_code' => $response->info['http_code'],
  254. ] + $info + $response->info;
  255. if (null !== $response->info['error']) {
  256. throw new TransportException($response->info['error']);
  257. }
  258. if (!isset($response->info['total_time'])) {
  259. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  260. }
  261. // "notify" headers arrival
  262. $onProgress(0, $dlSize, $response->info);
  263. // cast response body to activity list
  264. $body = $mock instanceof self ? $mock->body : $mock->getContent(false);
  265. if (!\is_string($body)) {
  266. try {
  267. foreach ($body as $chunk) {
  268. if ('' === $chunk = (string) $chunk) {
  269. // simulate an idle timeout
  270. $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
  271. } else {
  272. $response->body[] = $chunk;
  273. $offset += \strlen($chunk);
  274. // "notify" download progress
  275. $onProgress($offset, $dlSize, $response->info);
  276. }
  277. }
  278. } catch (\Throwable $e) {
  279. $response->body[] = $e;
  280. }
  281. } elseif ('' !== $body) {
  282. $response->body[] = $body;
  283. $offset = \strlen($body);
  284. }
  285. if (!isset($response->info['total_time'])) {
  286. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  287. }
  288. // "notify" completion
  289. $onProgress($offset, $dlSize, $response->info);
  290. if ($dlSize && $offset !== $dlSize) {
  291. throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
  292. }
  293. }
  294. }