Lines 90.56% 48 / 53
Methods 70.58% 12 / 17
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 __toString 100.00% 3 / 3 100.00% 1 / 1 2
 close 100.00% 4 / 4 100.00% 1 / 1 1
 detach 100.00% 6 / 6 100.00% 1 / 1 1
 getSize 100.00% 3 / 3 100.00% 1 / 1 2
 tell 0.00% 0 / 1 0.00% 0 / 1 2
 eof 100.00% 1 / 1 100.00% 1 / 1 1
 isSeekable 100.00% 1 / 1 100.00% 1 / 1 1
 seek 0.00% 0 / 1 0.00% 0 / 1 2
 rewind 0.00% 0 / 1 0.00% 0 / 1 2
 isWritable 100.00% 1 / 1 100.00% 1 / 1 1
 write 0.00% 0 / 1 0.00% 0 / 1 2
 isReadable 100.00% 1 / 1 100.00% 1 / 1 1
 read 100.00% 4 / 4 100.00% 1 / 1 1
 getContents 100.00% 4 / 4 100.00% 1 / 1 1
 getMetadata 66.66% 2 / 3 0.00% 0 / 1 2.15
 getOutput 100.00% 17 / 17 100.00% 1 / 1 6
23final class LazyStream implements StreamInterface
24{
25    /** @var callable|null */
26    private $callback;
27
28    /** @var string|null Cached output after first invocation */
29    private ?string $output = null;
30
31    private bool $called = false;
32    private int $position = 0;
33
34    public function __construct(callable $callback)
35    {
36        $this->callback = $callback;
37    }
38
39    public function __toString(): string
40    {
41        // String casting must never raise an exception.
42        try {
43            return $this->getOutput();
44        } catch (\Throwable) {
45            return '';
46        }
47    }
48
49    public function close(): void
50    {
51        $this->callback = null;
52        $this->output = null;
53        $this->called = false;
54        $this->position = 0;
55    }
56
57    public function detach(): ?callable
58    {
59        $callback = $this->callback;
60        $this->callback = null;
61        $this->output = null;
62        $this->called = false;
63        $this->position = 0;
64        return $callback;
65    }
66
67    public function getSize(): ?int
68    {
69        // Size is unknown until the callback has been invoked — invoking it
70        // here would defeat lazy streaming (callers such as CurlHandler call
71        // getSize() before sending). Null means "unknown".
72        if (!$this->called) {
73            return null;
74        }
75
76        return strlen($this->output ?? '');
77    }
78
79    public function tell(): int
80    {
81        return $this->position;
82    }
83
84    public function eof(): bool
85    {
86        return $this->position >= strlen($this->getOutput());
87    }
88
89    public function isSeekable(): bool
90    {
91        return false;
92    }
93
94    public function seek(int $offset, int $whence = SEEK_SET): void
95    {
96        throw new RuntimeException('LazyStream is not seekable');
97    }
98
99    public function rewind(): void
100    {
101        throw new RuntimeException('LazyStream is not seekable');
102    }
103
104    public function isWritable(): bool
105    {
106        return false;
107    }
108
109    public function write(string $string): int
110    {
111        throw new RuntimeException('LazyStream is not writable');
112    }
113
114    public function isReadable(): bool
115    {
116        return true;
117    }
118
119    public function read(int $length): string
120    {
121        $output = $this->getOutput();
122        $data = substr($output, $this->position, $length);
123        $this->position += strlen($data);
124        return $data;
125    }
126
127    public function getContents(): string
128    {
129        $output = $this->getOutput();
130        $data = substr($output, $this->position);
131        $this->position = strlen($output);
132        return $data;
133    }
134
135    public function getMetadata(?string $key = null)
136    {
137        if ($key === null) {
138            return [];
139        }
140        return null;
141    }
142
143    /**
144     * Invoke the callback and cache the output.
145     *
146     * Only invokes the callback once; subsequent calls return the cached output.
147     *
148     * @return string The cached output
149     */
150    private function getOutput(): string
151    {
152        if ($this->called) {
153            return $this->output ?? '';
154        }
155
156        $this->called = true;
157
158        if ($this->callback === null) {
159            return '';
160        }
161
162        $callback = $this->callback;
163
164        ob_start();
165        try {
166            $result = $callback();
167        } catch (\Throwable $e) {
168            // Close the buffer so callers are not left with a dangling
169            // output buffer, then rethrow.
170            ob_end_clean();
171            throw $e;
172        }
173        $buffered = ob_get_clean();
174
175        $this->output = ($buffered !== false && $buffered !== '')
176            ? $buffered
177            : ($result ?? '');
178
179        $this->callback = null;
180        return $this->output;
181    }
182}