123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\HttpClient\Response;
- use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
- use Symfony\Contracts\HttpClient\HttpClientInterface;
- use Symfony\Contracts\HttpClient\ResponseInterface;
- /**
- * Allows turning ResponseInterface instances to PHP streams.
- *
- * @author Nicolas Grekas <p@tchwork.com>
- */
- class StreamWrapper
- {
- /** @var resource|string|null */
- public $context;
- /** @var HttpClientInterface */
- private $client;
- /** @var ResponseInterface */
- private $response;
- /** @var resource|null */
- private $content;
- /** @var resource|null */
- private $handle;
- private $blocking = true;
- private $timeout;
- private $eof = false;
- private $offset = 0;
- /**
- * Creates a PHP stream resource from a ResponseInterface.
- *
- * @return resource
- */
- public static function createResource(ResponseInterface $response, HttpClientInterface $client = null)
- {
- if ($response instanceof StreamableInterface) {
- $stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
- if ($response !== ($stack[1]['object'] ?? null)) {
- return $response->toStream(false);
- }
- }
- if (null === $client && !method_exists($response, 'stream')) {
- throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
- }
- if (false === stream_wrapper_register('symfony', __CLASS__, \STREAM_IS_URL)) {
- throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
- }
- try {
- $context = [
- 'client' => $client ?? $response,
- 'response' => $response,
- ];
- return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null;
- } finally {
- stream_wrapper_unregister('symfony');
- }
- }
- public function getResponse(): ResponseInterface
- {
- return $this->response;
- }
- /**
- * @param resource|callable|null $handle The resource handle that should be monitored when
- * stream_select() is used on the created stream
- * @param resource|null $content The seekable resource where the response body is buffered
- */
- public function bindHandles(&$handle, &$content): void
- {
- $this->handle = &$handle;
- $this->content = &$content;
- }
- public function stream_open(string $path, string $mode, int $options): bool
- {
- if ('r' !== $mode) {
- if ($options & \STREAM_REPORT_ERRORS) {
- trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
- }
- return false;
- }
- $context = stream_context_get_options($this->context)['symfony'] ?? null;
- $this->client = $context['client'] ?? null;
- $this->response = $context['response'] ?? null;
- $this->context = null;
- if (null !== $this->client && null !== $this->response) {
- return true;
- }
- if ($options & \STREAM_REPORT_ERRORS) {
- trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
- }
- return false;
- }
- public function stream_read(int $count)
- {
- if (\is_resource($this->content)) {
- // Empty the internal activity list
- foreach ($this->client->stream([$this->response], 0) as $chunk) {
- try {
- if (!$chunk->isTimeout() && $chunk->isFirst()) {
- $this->response->getStatusCode(); // ignore 3/4/5xx
- }
- } catch (ExceptionInterface $e) {
- trigger_error($e->getMessage(), \E_USER_WARNING);
- return false;
- }
- }
- if (0 !== fseek($this->content, $this->offset)) {
- return false;
- }
- if ('' !== $data = fread($this->content, $count)) {
- fseek($this->content, 0, \SEEK_END);
- $this->offset += \strlen($data);
- return $data;
- }
- }
- if (\is_string($this->content)) {
- if (\strlen($this->content) <= $count) {
- $data = $this->content;
- $this->content = null;
- } else {
- $data = substr($this->content, 0, $count);
- $this->content = substr($this->content, $count);
- }
- $this->offset += \strlen($data);
- return $data;
- }
- foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
- try {
- $this->eof = true;
- $this->eof = !$chunk->isTimeout();
- $this->eof = $chunk->isLast();
- if ($chunk->isFirst()) {
- $this->response->getStatusCode(); // ignore 3/4/5xx
- }
- if ('' !== $data = $chunk->getContent()) {
- if (\strlen($data) > $count) {
- if (null === $this->content) {
- $this->content = substr($data, $count);
- }
- $data = substr($data, 0, $count);
- }
- $this->offset += \strlen($data);
- return $data;
- }
- } catch (ExceptionInterface $e) {
- trigger_error($e->getMessage(), \E_USER_WARNING);
- return false;
- }
- }
- return '';
- }
- public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
- {
- if (\STREAM_OPTION_BLOCKING === $option) {
- $this->blocking = (bool) $arg1;
- } elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
- $this->timeout = $arg1 + $arg2 / 1e6;
- } else {
- return false;
- }
- return true;
- }
- public function stream_tell(): int
- {
- return $this->offset;
- }
- public function stream_eof(): bool
- {
- return $this->eof && !\is_string($this->content);
- }
- public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
- {
- if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
- return false;
- }
- $size = ftell($this->content);
- if (\SEEK_CUR === $whence) {
- $offset += $this->offset;
- }
- if (\SEEK_END === $whence || $size < $offset) {
- foreach ($this->client->stream([$this->response]) as $chunk) {
- try {
- if ($chunk->isFirst()) {
- $this->response->getStatusCode(); // ignore 3/4/5xx
- }
- // Chunks are buffered in $this->content already
- $size += \strlen($chunk->getContent());
- if (\SEEK_END !== $whence && $offset <= $size) {
- break;
- }
- } catch (ExceptionInterface $e) {
- trigger_error($e->getMessage(), \E_USER_WARNING);
- return false;
- }
- }
- if (\SEEK_END === $whence) {
- $offset += $size;
- }
- }
- if (0 <= $offset && $offset <= $size) {
- $this->eof = false;
- $this->offset = $offset;
- return true;
- }
- return false;
- }
- public function stream_cast(int $castAs)
- {
- if (\STREAM_CAST_FOR_SELECT === $castAs) {
- $this->response->getHeaders(false);
- return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
- }
- return false;
- }
- public function stream_stat(): array
- {
- try {
- $headers = $this->response->getHeaders(false);
- } catch (ExceptionInterface $e) {
- trigger_error($e->getMessage(), \E_USER_WARNING);
- $headers = [];
- }
- return [
- 'dev' => 0,
- 'ino' => 0,
- 'mode' => 33060,
- 'nlink' => 0,
- 'uid' => 0,
- 'gid' => 0,
- 'rdev' => 0,
- 'size' => (int) ($headers['content-length'][0] ?? -1),
- 'atime' => 0,
- 'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
- 'ctime' => 0,
- 'blksize' => 0,
- 'blocks' => 0,
- ];
- }
- private function __construct()
- {
- }
- }
|