PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/HttpClient/Response/StreamWrapper.php

https://github.com/FabienD/symfony
PHP | 302 lines | 217 code | 57 blank | 28 comment | 37 complexity | 88402a8c87bb943f05c75eea62e12ef7 MD5 | raw file
  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\Contracts\HttpClient\Exception\ExceptionInterface;
  12. use Symfony\Contracts\HttpClient\HttpClientInterface;
  13. use Symfony\Contracts\HttpClient\ResponseInterface;
  14. /**
  15. * Allows turning ResponseInterface instances to PHP streams.
  16. *
  17. * @author Nicolas Grekas <p@tchwork.com>
  18. */
  19. class StreamWrapper
  20. {
  21. /** @var resource|string|null */
  22. public $context;
  23. private HttpClientInterface|ResponseInterface $client;
  24. private ResponseInterface $response;
  25. /** @var resource|null */
  26. private $content;
  27. /** @var resource|null */
  28. private $handle;
  29. private bool $blocking = true;
  30. private ?float $timeout = null;
  31. private bool $eof = false;
  32. private int $offset = 0;
  33. /**
  34. * Creates a PHP stream resource from a ResponseInterface.
  35. *
  36. * @return resource
  37. */
  38. public static function createResource(ResponseInterface $response, HttpClientInterface $client = null)
  39. {
  40. if ($response instanceof StreamableInterface) {
  41. $stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  42. if ($response !== ($stack[1]['object'] ?? null)) {
  43. return $response->toStream(false);
  44. }
  45. }
  46. if (null === $client && !method_exists($response, 'stream')) {
  47. throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
  48. }
  49. if (false === stream_wrapper_register('symfony', __CLASS__)) {
  50. throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
  51. }
  52. try {
  53. $context = [
  54. 'client' => $client ?? $response,
  55. 'response' => $response,
  56. ];
  57. return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null;
  58. } finally {
  59. stream_wrapper_unregister('symfony');
  60. }
  61. }
  62. public function getResponse(): ResponseInterface
  63. {
  64. return $this->response;
  65. }
  66. /**
  67. * @param resource|callable|null $handle The resource handle that should be monitored when
  68. * stream_select() is used on the created stream
  69. * @param resource|null $content The seekable resource where the response body is buffered
  70. */
  71. public function bindHandles(&$handle, &$content): void
  72. {
  73. $this->handle = &$handle;
  74. $this->content = &$content;
  75. }
  76. public function stream_open(string $path, string $mode, int $options): bool
  77. {
  78. if ('r' !== $mode) {
  79. if ($options & \STREAM_REPORT_ERRORS) {
  80. trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
  81. }
  82. return false;
  83. }
  84. $context = stream_context_get_options($this->context)['symfony'] ?? null;
  85. $this->client = $context['client'] ?? null;
  86. $this->response = $context['response'] ?? null;
  87. $this->context = null;
  88. if (null !== $this->client && null !== $this->response) {
  89. return true;
  90. }
  91. if ($options & \STREAM_REPORT_ERRORS) {
  92. trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
  93. }
  94. return false;
  95. }
  96. public function stream_read(int $count): string|false
  97. {
  98. if (\is_resource($this->content)) {
  99. // Empty the internal activity list
  100. foreach ($this->client->stream([$this->response], 0) as $chunk) {
  101. try {
  102. if (!$chunk->isTimeout() && $chunk->isFirst()) {
  103. $this->response->getStatusCode(); // ignore 3/4/5xx
  104. }
  105. } catch (ExceptionInterface $e) {
  106. trigger_error($e->getMessage(), \E_USER_WARNING);
  107. return false;
  108. }
  109. }
  110. if (0 !== fseek($this->content, $this->offset)) {
  111. return false;
  112. }
  113. if ('' !== $data = fread($this->content, $count)) {
  114. fseek($this->content, 0, \SEEK_END);
  115. $this->offset += \strlen($data);
  116. return $data;
  117. }
  118. }
  119. if (\is_string($this->content)) {
  120. if (\strlen($this->content) <= $count) {
  121. $data = $this->content;
  122. $this->content = null;
  123. } else {
  124. $data = substr($this->content, 0, $count);
  125. $this->content = substr($this->content, $count);
  126. }
  127. $this->offset += \strlen($data);
  128. return $data;
  129. }
  130. foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
  131. try {
  132. $this->eof = true;
  133. $this->eof = !$chunk->isTimeout();
  134. $this->eof = $chunk->isLast();
  135. if ($chunk->isFirst()) {
  136. $this->response->getStatusCode(); // ignore 3/4/5xx
  137. }
  138. if ('' !== $data = $chunk->getContent()) {
  139. if (\strlen($data) > $count) {
  140. if (null === $this->content) {
  141. $this->content = substr($data, $count);
  142. }
  143. $data = substr($data, 0, $count);
  144. }
  145. $this->offset += \strlen($data);
  146. return $data;
  147. }
  148. } catch (ExceptionInterface $e) {
  149. trigger_error($e->getMessage(), \E_USER_WARNING);
  150. return false;
  151. }
  152. }
  153. return '';
  154. }
  155. public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
  156. {
  157. if (\STREAM_OPTION_BLOCKING === $option) {
  158. $this->blocking = (bool) $arg1;
  159. } elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
  160. $this->timeout = $arg1 + $arg2 / 1e6;
  161. } else {
  162. return false;
  163. }
  164. return true;
  165. }
  166. public function stream_tell(): int
  167. {
  168. return $this->offset;
  169. }
  170. public function stream_eof(): bool
  171. {
  172. return $this->eof && !\is_string($this->content);
  173. }
  174. public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
  175. {
  176. if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
  177. return false;
  178. }
  179. $size = ftell($this->content);
  180. if (\SEEK_CUR === $whence) {
  181. $offset += $this->offset;
  182. }
  183. if (\SEEK_END === $whence || $size < $offset) {
  184. foreach ($this->client->stream([$this->response]) as $chunk) {
  185. try {
  186. if ($chunk->isFirst()) {
  187. $this->response->getStatusCode(); // ignore 3/4/5xx
  188. }
  189. // Chunks are buffered in $this->content already
  190. $size += \strlen($chunk->getContent());
  191. if (\SEEK_END !== $whence && $offset <= $size) {
  192. break;
  193. }
  194. } catch (ExceptionInterface $e) {
  195. trigger_error($e->getMessage(), \E_USER_WARNING);
  196. return false;
  197. }
  198. }
  199. if (\SEEK_END === $whence) {
  200. $offset += $size;
  201. }
  202. }
  203. if (0 <= $offset && $offset <= $size) {
  204. $this->eof = false;
  205. $this->offset = $offset;
  206. return true;
  207. }
  208. return false;
  209. }
  210. public function stream_cast(int $castAs)
  211. {
  212. if (\STREAM_CAST_FOR_SELECT === $castAs) {
  213. $this->response->getHeaders(false);
  214. return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
  215. }
  216. return false;
  217. }
  218. public function stream_stat(): array
  219. {
  220. try {
  221. $headers = $this->response->getHeaders(false);
  222. } catch (ExceptionInterface $e) {
  223. trigger_error($e->getMessage(), \E_USER_WARNING);
  224. $headers = [];
  225. }
  226. return [
  227. 'dev' => 0,
  228. 'ino' => 0,
  229. 'mode' => 33060,
  230. 'nlink' => 0,
  231. 'uid' => 0,
  232. 'gid' => 0,
  233. 'rdev' => 0,
  234. 'size' => (int) ($headers['content-length'][0] ?? -1),
  235. 'atime' => 0,
  236. 'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
  237. 'ctime' => 0,
  238. 'blksize' => 0,
  239. 'blocks' => 0,
  240. ];
  241. }
  242. private function __construct()
  243. {
  244. }
  245. }