| 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 |