Total Complexity | 67 |
Total Lines | 346 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like Stream often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Stream, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
58 | class Stream implements StreamInterface |
||
59 | { |
||
60 | /** |
||
61 | * The writable stream modes. |
||
62 | */ |
||
63 | protected const MODES_WRITE = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+']; |
||
64 | |||
65 | /** |
||
66 | * The readable stream modes. |
||
67 | */ |
||
68 | protected const MODES_READ = ['r', 'r+', 'w+', 'a+', 'x+', 'c+']; |
||
69 | |||
70 | /** |
||
71 | * The stream resource |
||
72 | * @var resource|null |
||
73 | */ |
||
74 | protected $resource; |
||
75 | |||
76 | /** |
||
77 | * The stream size |
||
78 | * @var int|null |
||
79 | */ |
||
80 | protected ?int $size; |
||
81 | |||
82 | /** |
||
83 | * Whether the stream is seekable |
||
84 | * @var boolean |
||
85 | */ |
||
86 | protected bool $seekable = false; |
||
87 | |||
88 | /** |
||
89 | * Whether the stream is writable |
||
90 | * @var boolean |
||
91 | */ |
||
92 | protected bool $writable = false; |
||
93 | |||
94 | /** |
||
95 | * Whether the stream is readable |
||
96 | * @var boolean |
||
97 | */ |
||
98 | protected bool $readable = false; |
||
99 | |||
100 | /** |
||
101 | * Create new Stream |
||
102 | * @param string|resource $content the filename or resource instance |
||
103 | * @param string $mode the stream mode |
||
104 | * @param array<string, mixed> $options the stream options |
||
105 | */ |
||
106 | public function __construct( |
||
107 | $content = '', |
||
108 | string $mode = 'r+', |
||
109 | array $options = [] |
||
110 | ) { |
||
111 | if (is_string($content)) { |
||
112 | if (is_file($content) || strpos($content, 'php://') === 0) { |
||
113 | $mode = $this->filterMode($mode); |
||
114 | $resource = fopen($content, $mode); |
||
115 | if ($resource === false) { |
||
116 | throw new RuntimeException(sprintf( |
||
117 | 'Unable to create a stream from file [%s] !', |
||
118 | $content |
||
119 | )); |
||
120 | } |
||
121 | $this->resource = $resource; |
||
122 | } else { |
||
123 | $resource = fopen('php://temp', 'r+'); |
||
124 | if ($resource === false || fwrite($resource, $content) === false) { |
||
125 | throw new RuntimeException( |
||
126 | 'Unable to create a stream from string' |
||
127 | ); |
||
128 | } |
||
129 | $this->resource = $resource; |
||
130 | } |
||
131 | } elseif (is_resource($content)) { |
||
132 | $this->resource = $content; |
||
133 | } else { |
||
134 | throw new InvalidArgumentException( |
||
135 | 'Stream resource must be valid PHP resource' |
||
136 | ); |
||
137 | } |
||
138 | |||
139 | if (isset($options['size']) && is_int($options['size']) && $options['size'] >= 0) { |
||
140 | $this->size = $options['size']; |
||
141 | } else { |
||
142 | $fstat = fstat($this->resource); |
||
143 | if ($fstat === false) { |
||
144 | $this->size = null; |
||
145 | } else { |
||
146 | $this->size = !empty($fstat['size']) ? $fstat['size'] : null; |
||
147 | } |
||
148 | } |
||
149 | |||
150 | $meta = stream_get_meta_data($this->resource); |
||
151 | $this->seekable = isset($options['seekable']) |
||
152 | && is_bool($options['seekable']) |
||
153 | ? $options['seekable'] |
||
154 | : (!empty($meta['seekable']) |
||
155 | ? $meta['seekable'] |
||
156 | : false |
||
157 | ); |
||
158 | |||
159 | if (isset($options['writable']) && is_bool($options['writable'])) { |
||
160 | $this->writable = $options['writable']; |
||
161 | } else { |
||
162 | foreach (static::MODES_WRITE as $mode) { |
||
163 | if (strncmp($meta['mode'], $mode, strlen($mode)) === 0) { |
||
164 | $this->writable = true; |
||
165 | break; |
||
166 | } |
||
167 | } |
||
168 | } |
||
169 | |||
170 | if (isset($options['readable']) && is_bool($options['readable'])) { |
||
171 | $this->readable = $options['readable']; |
||
172 | } else { |
||
173 | foreach (static::MODES_READ as $mode) { |
||
174 | if (strncmp($meta['mode'], $mode, strlen($mode)) === 0) { |
||
175 | $this->readable = true; |
||
176 | break; |
||
177 | } |
||
178 | } |
||
179 | } |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * {@inheritdoc} |
||
184 | */ |
||
185 | public function __toString(): string |
||
186 | { |
||
187 | try { |
||
188 | if ($this->seekable) { |
||
189 | $this->rewind(); |
||
190 | } |
||
191 | return $this->getContents(); |
||
192 | } catch (Exception $e) { |
||
193 | return ''; |
||
194 | } |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * {@inheritdoc} |
||
199 | */ |
||
200 | public function close(): void |
||
201 | { |
||
202 | if ($this->resource !== null && fclose($this->resource)) { |
||
203 | $this->detach(); |
||
204 | } |
||
205 | } |
||
206 | |||
207 | /** |
||
208 | * {@inheritdoc} |
||
209 | */ |
||
210 | public function detach() |
||
211 | { |
||
212 | $resource = $this->resource; |
||
213 | if ($resource !== null) { |
||
214 | $this->resource = null; |
||
215 | $this->size = null; |
||
216 | $this->seekable = false; |
||
217 | $this->writable = false; |
||
218 | $this->readable = false; |
||
219 | } |
||
220 | return $resource; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * {@inheritdoc} |
||
225 | */ |
||
226 | public function getSize(): ?int |
||
227 | { |
||
228 | return $this->size; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * {@inheritdoc} |
||
233 | */ |
||
234 | public function tell(): int |
||
235 | { |
||
236 | if ($this->resource === null) { |
||
237 | throw new RuntimeException('Stream resource is detached'); |
||
238 | } |
||
239 | $position = ftell($this->resource); |
||
240 | if ($position === false) { |
||
241 | throw new RuntimeException('Unable to tell the current position of the stream read/write pointer'); |
||
242 | } |
||
243 | |||
244 | return $position; |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * {@inheritdoc} |
||
249 | */ |
||
250 | public function eof(): bool |
||
251 | { |
||
252 | return $this->resource === null || feof($this->resource); |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * {@inheritdoc} |
||
257 | */ |
||
258 | public function isSeekable(): bool |
||
259 | { |
||
260 | return $this->seekable; |
||
261 | } |
||
262 | |||
263 | /** |
||
264 | * {@inheritdoc} |
||
265 | */ |
||
266 | public function seek(int $offset, int $whence = SEEK_SET): void |
||
267 | { |
||
268 | if ($this->resource === null) { |
||
269 | throw new RuntimeException('Stream resource is detached'); |
||
270 | } |
||
271 | |||
272 | if (!$this->seekable) { |
||
273 | throw new RuntimeException('Stream is not seekable'); |
||
274 | } |
||
275 | |||
276 | if (fseek($this->resource, $offset, $whence) === -1) { |
||
277 | throw new RuntimeException('Can not seek to a position in the stream'); |
||
278 | } |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * {@inheritdoc} |
||
283 | */ |
||
284 | public function rewind(): void |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * {@inheritdoc} |
||
291 | */ |
||
292 | public function isWritable(): bool |
||
293 | { |
||
294 | return $this->writable; |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * {@inheritdoc} |
||
299 | */ |
||
300 | public function write(string $string): int |
||
301 | { |
||
302 | if ($this->resource === null) { |
||
303 | throw new RuntimeException('Stream resource is detached'); |
||
304 | } |
||
305 | |||
306 | if (!$this->writable) { |
||
307 | throw new RuntimeException('Stream is not writable'); |
||
308 | } |
||
309 | $bytes = fwrite($this->resource, $string); |
||
310 | |||
311 | if ($bytes === false) { |
||
312 | throw new RuntimeException('Unable to write data to the stream'); |
||
313 | } |
||
314 | |||
315 | $fstat = fstat($this->resource); |
||
316 | if ($fstat === false) { |
||
317 | $this->size = null; |
||
318 | } else { |
||
319 | $this->size = !empty($fstat['size']) ? $fstat['size'] : null; |
||
320 | } |
||
321 | |||
322 | return $bytes; |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * {@inheritdoc} |
||
327 | */ |
||
328 | public function isReadable(): bool |
||
329 | { |
||
330 | return $this->readable; |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * {@inheritdoc} |
||
335 | */ |
||
336 | public function read(int $length): string |
||
337 | { |
||
338 | if ($this->resource === null) { |
||
339 | throw new RuntimeException('Stream resource is detached'); |
||
340 | } |
||
341 | |||
342 | if (!$this->readable) { |
||
343 | throw new RuntimeException('Stream is not readable'); |
||
344 | } |
||
345 | |||
346 | $data = fread($this->resource, $length); |
||
347 | if ($data === false) { |
||
348 | throw new RuntimeException('Unable to read data from the stream'); |
||
349 | } |
||
350 | |||
351 | return $data; |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * {@inheritdoc} |
||
356 | */ |
||
357 | public function getContents(): string |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * {@inheritdoc} |
||
378 | */ |
||
379 | public function getMetadata(?string $key = null): mixed |
||
380 | { |
||
381 | if ($this->resource === null) { |
||
382 | throw new RuntimeException('Stream resource is detached'); |
||
383 | } |
||
384 | |||
385 | $meta = stream_get_meta_data($this->resource); |
||
386 | if ($key === null) { |
||
387 | return $meta; |
||
388 | } |
||
389 | return !empty($meta[$key]) ? $meta[$key] : null; |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * Check if the given mode is valid |
||
394 | * @param string $mode the mode |
||
395 | * @return string |
||
396 | * @throws InvalidArgumentException |
||
397 | */ |
||
398 | protected function filterMode(string $mode): string |
||
404 | } |
||
405 | } |
||
406 |