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(int): (string|false|null)|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|false|null)  $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
64            return '';
65        }
66    }
67
68    public function close(): void
69    {
70        $this->detach();
71    }
72
73    public function detach()
74    {
75        $this->tellPos = 0;
76        $this->source = null;
77
78        return null;
79    }
80
81    public function getSize(): ?int
82    {
83        return $this->size;
84    }
85
86    public function tell(): int
87    {
88        return $this->tellPos;
89    }
90
91    public function eof(): bool
92    {
93        return $this->source === null;
94    }
95
96    public function isSeekable(): bool
97    {
98        return false;
99    }
100
101    public function rewind(): void
102    {
103        $this->seek(0);
104    }
105
106    public function seek($offset, $whence = SEEK_SET): void
107    {
108        throw new \RuntimeException('Cannot seek a PumpStream');
109    }
110
111    public function isWritable(): bool
112    {
113        return false;
114    }
115
116    public function write($string): int
117    {
118        throw new \RuntimeException('Cannot write to a PumpStream');
119    }
120
121    public function isReadable(): bool
122    {
123        return true;
124    }
125
126    public function read($length): string
127    {
128        $data = $this->buffer->read($length);
129        $readLen = strlen($data);
130        $this->tellPos += $readLen;
131        $remaining = $length - $readLen;
132
133        if ($remaining) {
134            $this->pump($remaining);
135            $data .= $this->buffer->read($remaining);
136            $this->tellPos += strlen($data) - $readLen;
137        }
138
139        return $data;
140    }
141
142    public function getContents(): string
143    {
144        $result = '';
145        while (!$this->eof()) {
146            $result .= $this->read(1000000);
147        }
148
149        return $result;
150    }
151
152    /**
153     * @return mixed
154     */
155    public function getMetadata($key = null)
156    {
157        if (!$key) {
158            return $this->metadata;
159        }
160
161        return $this->metadata[$key] ?? null;
162    }
163
164    private function pump(int $length): void
165    {
166        if ($this->source !== null) {
167            do {
168                $data = ($this->source)($length);
169                if ($data === false || $data === null) {
170                    $this->source = null;
171
172                    return;
173                }
174                $this->buffer->write($data);
175                $length -= strlen($data);
176            } while ($length > 0);
177        }
178    }
179}
180