Lines 88.46% 46 / 52
Methods 62.50% 10 / 16
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 66.66% 2 / 3 0.00% 0 / 1 2.15
 __toString 100.00% 6 / 6 100.00% 1 / 1 2
 close 100.00% 3 / 3 100.00% 1 / 1 1
 detach 100.00% 5 / 5 100.00% 1 / 1 1
 getSize 100.00% 1 / 1 100.00% 1 / 1 1
 tell 100.00% 1 / 1 100.00% 1 / 1 1
 eof 100.00% 1 / 1 100.00% 1 / 1 2
 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% 13 / 13 100.00% 1 / 1 7
 getContents 90.00% 9 / 10 0.00% 0 / 1 4.02
 getMetadata 66.66% 2 / 3 0.00% 0 / 1 2.15
21final class IteratorStream implements StreamInterface
22{
23    /** @var \Iterator|null */
24    private ?\Iterator $iterator = null;
25
26    private int $position = 0;
27    private bool $exhausted = false;
28
29    public function __construct(Traversable $iterator)
30    {
31        if ($iterator instanceof \IteratorAggregate) {
32            $iterator = $iterator->getIterator();
33        }
34        $this->iterator = $iterator;
35    }
36
37    public function __toString(): string
38    {
39        try {
40            $this->iterator?->rewind();
41            $this->position = 0;
42            $this->exhausted = false;
43            return $this->getContents();
44        } catch (\Throwable) {
45            return '';
46        }
47    }
48
49    public function close(): void
50    {
51        $this->iterator = null;
52        $this->position = 0;
53        $this->exhausted = false;
54    }
55
56    public function detach(): ?\Iterator
57    {
58        $iterator = $this->iterator;
59        $this->iterator = null;
60        $this->position = 0;
61        $this->exhausted = false;
62        return $iterator;
63    }
64
65    public function getSize(): ?int
66    {
67        return null;
68    }
69
70    public function tell(): int
71    {
72        return $this->position;
73    }
74
75    public function eof(): bool
76    {
77        return $this->exhausted || $this->iterator === null;
78    }
79
80    public function isSeekable(): bool
81    {
82        return false;
83    }
84
85    public function seek(int $offset, int $whence = SEEK_SET): void
86    {
87        throw new RuntimeException('IteratorStream is not seekable');
88    }
89
90    public function rewind(): void
91    {
92        throw new RuntimeException('IteratorStream is not seekable');
93    }
94
95    public function isWritable(): bool
96    {
97        return false;
98    }
99
100    public function write(string $string): int
101    {
102        throw new RuntimeException('IteratorStream is not writable');
103    }
104
105    public function isReadable(): bool
106    {
107        return true;
108    }
109
110    public function read(int $length): string
111    {
112        if ($this->exhausted || $this->iterator === null) {
113            return '';
114        }
115
116        $contents = '';
117        while ($this->iterator->valid() && strlen($contents) < $length) {
118            $chunk = (string) $this->iterator->current();
119            $this->iterator->next();
120            $this->position += strlen($chunk);
121            $contents .= $chunk;
122
123            // True streaming: return after the first yielded chunk so the
124            // emitter (Application::executeHttpRequest()) flushes per event
125            // instead of batching until $length bytes accumulate. PSR-7
126            // permits read() to return fewer bytes than requested.
127
128            if ($contents !== '') {
129                break;
130            }
131        }
132
133        if (! $this->iterator->valid()) {
134            $this->exhausted = true;
135        }
136
137        return $contents;
138    }
139
140    public function getContents(): string
141    {
142        if ($this->exhausted || $this->iterator === null) {
143            return '';
144        }
145
146        $contents = '';
147        while ($this->iterator->valid()) {
148            $chunk = (string) $this->iterator->current();
149            $this->iterator->next();
150            $this->position += strlen($chunk);
151            $contents .= $chunk;
152        }
153
154        $this->exhausted = true;
155        return $contents;
156    }
157
158    public function getMetadata(?string $key = null)
159    {
160        if ($key === null) {
161            return [];
162        }
163        return null;
164    }
165}