CommonResponseTrait.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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\Exception\ClientException;
  12. use Symfony\Component\HttpClient\Exception\JsonException;
  13. use Symfony\Component\HttpClient\Exception\RedirectionException;
  14. use Symfony\Component\HttpClient\Exception\ServerException;
  15. use Symfony\Component\HttpClient\Exception\TransportException;
  16. /**
  17. * Implements common logic for response classes.
  18. *
  19. * @author Nicolas Grekas <p@tchwork.com>
  20. *
  21. * @internal
  22. */
  23. trait CommonResponseTrait
  24. {
  25. /**
  26. * @var callable|null A callback that tells whether we're waiting for response headers
  27. */
  28. private $initializer;
  29. private $shouldBuffer;
  30. private $content;
  31. private $offset = 0;
  32. private $jsonData;
  33. /**
  34. * {@inheritdoc}
  35. */
  36. public function getContent(bool $throw = true): string
  37. {
  38. if ($this->initializer) {
  39. self::initialize($this);
  40. }
  41. if ($throw) {
  42. $this->checkStatusCode();
  43. }
  44. if (null === $this->content) {
  45. $content = null;
  46. foreach (self::stream([$this]) as $chunk) {
  47. if (!$chunk->isLast()) {
  48. $content .= $chunk->getContent();
  49. }
  50. }
  51. if (null !== $content) {
  52. return $content;
  53. }
  54. if (null === $this->content) {
  55. throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
  56. }
  57. } else {
  58. foreach (self::stream([$this]) as $chunk) {
  59. // Chunks are buffered in $this->content already
  60. }
  61. }
  62. rewind($this->content);
  63. return stream_get_contents($this->content);
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. public function toArray(bool $throw = true): array
  69. {
  70. if ('' === $content = $this->getContent($throw)) {
  71. throw new JsonException('Response body is empty.');
  72. }
  73. if (null !== $this->jsonData) {
  74. return $this->jsonData;
  75. }
  76. try {
  77. $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
  78. } catch (\JsonException $e) {
  79. throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
  80. }
  81. if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
  82. throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
  83. }
  84. if (!\is_array($content)) {
  85. throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
  86. }
  87. if (null !== $this->content) {
  88. // Option "buffer" is true
  89. return $this->jsonData = $content;
  90. }
  91. return $content;
  92. }
  93. /**
  94. * {@inheritdoc}
  95. */
  96. public function toStream(bool $throw = true)
  97. {
  98. if ($throw) {
  99. // Ensure headers arrived
  100. $this->getHeaders($throw);
  101. }
  102. $stream = StreamWrapper::createResource($this);
  103. stream_get_meta_data($stream)['wrapper_data']
  104. ->bindHandles($this->handle, $this->content);
  105. return $stream;
  106. }
  107. public function __sleep()
  108. {
  109. throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  110. }
  111. public function __wakeup()
  112. {
  113. throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  114. }
  115. /**
  116. * Closes the response and all its network handles.
  117. */
  118. abstract protected function close(): void;
  119. private static function initialize(self $response): void
  120. {
  121. if (null !== $response->getInfo('error')) {
  122. throw new TransportException($response->getInfo('error'));
  123. }
  124. try {
  125. if (($response->initializer)($response)) {
  126. foreach (self::stream([$response]) as $chunk) {
  127. if ($chunk->isFirst()) {
  128. break;
  129. }
  130. }
  131. }
  132. } catch (\Throwable $e) {
  133. // Persist timeouts thrown during initialization
  134. $response->info['error'] = $e->getMessage();
  135. $response->close();
  136. throw $e;
  137. }
  138. $response->initializer = null;
  139. }
  140. private function checkStatusCode()
  141. {
  142. $code = $this->getInfo('http_code');
  143. if (500 <= $code) {
  144. throw new ServerException($this);
  145. }
  146. if (400 <= $code) {
  147. throw new ClientException($this);
  148. }
  149. if (300 <= $code) {
  150. throw new RedirectionException($this);
  151. }
  152. }
  153. }