1<?php
2
3declare(strict_types=1);
4
5namespace GuzzleHttp\Psr7;
6
7use Psr\Http\Message\StreamInterface;
8
9/**
10 * Provides a read only stream that pumps data from a PHP callable.
11 *
12 * When invoking the provided callable, the PumpStream will pass the amount of
13 * data requested to read to the callable. The callable can choose to ignore
14 * this value and return fewer or more bytes than requested. Any extra data
15 * returned by the provided callable is buffered internally until drained using
16 * the read() function of the PumpStream. The provided callable MUST return
17 * false when there is no more data to read.
18 */
19final class PumpStream implements StreamInterface
20{
21    /** @var callable|null */
22    private $source;
23
24    /** @var int|null */
25    private $size;
26
27    /** @var int */
28    private $tellPos = 0;
29
30    /** @var array */
31    private $metadata;
32
33    /** @var BufferStream */
34    private $buffer;
35
36    /**
37     * @param callable(int): (string|null|false)  $source  Source of the stream data. The callable MAY
38     *                                                     accept an integer argument used to control the
39     *                                                     amount of data to return. The callable MUST
40     *                                                     return a string when called, or false|null on error
41     *                                                     or EOF.
42     * @param array{size?: int, metadata?: array} $options Stream options:
43     *                                                     - metadata: Hash of metadata to use with stream.
44     *                                                     - size: Size of the stream, if known.
45     */
46    public function __construct(callable $source, array $options = [])
47    {
48        $this->source = $source;
49        $this->size = $options['size'] ?? null;
50        $this->metadata = $options['metadata'] ?? [];
51        $this->buffer = new BufferStream();
52    }
53
54    public function __toString(): string
55    {
56        try {
57            return Utils::copyToString($this);
58        } catch (\Throwable $e) {
59            if (\PHP_VERSION_ID >= 70400) {
60                throw $e;
61            }
62            trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
63            return '';
64        }
65    }
66
67    public function close(): void
68    {
69        $this->detach();
70    }
71
72    public function detach()
73    {
74        $this->tellPos = 0;
75        $this->source = null;
76
77        return null;
78    }
79
80    public function getSize(): ?int
81    {
82        return $this->size;
83    }
84
85    public function tell(): int
86    {
87        return $this->tellPos;
88    }
89
90    public function eof(): bool
91    {
92        return $this->source === null;
93    }
94
95    public function isSeekable(): bool
96    {
97        return false;
98    }
99
100    public function rewind(): void
101    {
102        $this->seek(0);
103    }
104
105    public function seek($offset, $whence = SEEK_SET): void
106    {
107        throw new \RuntimeException('Cannot seek a PumpStream');
108    }
109
110    public function isWritable(): bool
111    {
112        return false;
113    }
114
115    public function write($string): int
116    {
117        throw new \RuntimeException('Cannot write to a PumpStream');
118    }
119
120    public function isReadable(): bool
121    {
122        return true;
123    }
124
125    public function read($length): string
126    {
127        $data = $this->buffer->read($length);
128        $readLen = strlen($data);
129        $this->tellPos += $readLen;
130        $remaining = $length - $readLen;
131
132        if ($remaining) {
133            $this->pump($remaining);
134            $data .= $this->buffer->read($remaining);
135            $this->tellPos += strlen($data) - $readLen;
136        }
137
138        return $data;
139    }
140
141    public function getContents(): string
142    {
143        $result = '';
144        while (!$this->eof()) {
145            $result .= $this->read(1000000);
146        }
147
148        return $result;
149    }
150
151    public function getMetadata($key = null)
152    {
153        if (!$key) {
154            return $this->metadata;
155        }
156
157        return $this->metadata[$key] ?? null;
158    }
159
160    private function pump(int $length): void
161    {
162        if ($this->source) {
163            do {
164                $data = call_user_func($this->source, $length);
165                if ($data === false || $data === null) {
166                    $this->source = null;
167                    return;
168                }
169                $this->buffer->write($data);
170                $length -= strlen($data);
171            } while ($length > 0);
172        }
173    }
174}
175