ResourceStreamWrapper   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 609
Duplicated Lines 1.31 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 89.68%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 50
c 1
b 1
f 0
lcom 1
cbo 13
dl 8
loc 609
ccs 139
cts 155
cp 0.8968
rs 8.581

26 Methods

Rating   Name   Duplication   Size   Complexity  
A stream_cast() 0 4 1
A stream_set_option() 0 4 1
B register() 0 26 4
A unregister() 0 10 2
A dir_opendir() 0 14 1
A dir_closedir() 0 6 1
A dir_readdir() 0 12 2
A dir_rewinddir() 0 6 1
A mkdir() 0 8 1
A rename() 0 14 1
A rmdir() 0 16 2
A stream_close() 0 6 1
A stream_eof() 0 6 1
A stream_flush() 0 6 1
A stream_lock() 0 7 1
C stream_metadata() 0 34 7
B stream_open() 0 36 5
A stream_read() 0 6 1
A stream_seek() 0 6 1
A stream_stat() 0 6 1
A stream_tell() 0 6 1
A stream_truncate() 0 6 1
A stream_write() 0 6 1
A unlink() 0 8 1
B url_stat() 0 36 5
B getRepository() 8 28 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ResourceStreamWrapper 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ResourceStreamWrapper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the puli/repository package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Repository\StreamWrapper;
13
14
use InvalidArgumentException;
15
use Puli\Repository\Api\Resource\BodyResource;
16
use Puli\Repository\Api\Resource\FilesystemResource;
17
use Puli\Repository\Api\ResourceNotFoundException;
18
use Puli\Repository\Api\ResourceRepository;
19
use Puli\Repository\Api\UnsupportedOperationException;
20
use Puli\Repository\Api\UnsupportedResourceException;
21
use Puli\Repository\RepositoryFactoryException;
22
use Puli\Repository\Resource\Iterator\ResourceCollectionIterator;
23
use Puli\Repository\Uri\Uri;
24
use Webmozart\Assert\Assert;
25
26
/**
27
 * Registers a PHP stream wrapper for a {@link ResourceRepository}.
28
 *
29
 * To register the stream wrapper, call {@link register}:
30
 *
31
 * ```php
32
 * use Puli\Repository\InMemoryRepository;
33
 * use Puli\Repository\StreamWrapper\ResourceStreamWrapper;
34
 *
35
 * $repo = new InMemoryRepository();
36
 *
37
 * ResourceStreamWrapper::register('puli', $repo);
38
 *
39
 * file_get_contents('puli:///css/style.css');
40
 * // => $puliRepo->get('/css/style.css')->getBody()
41
 * ```
42
 *
43
 * The stream wrapper can only be used for reading, not writing.
44
 *
45
 * @since  1.0
46
 *
47
 * @author Bernhard Schussek <[email protected]>
48
 */
49
class ResourceStreamWrapper implements StreamWrapper
50
{
51
    const DEVICE_ASSOC = 'dev';
52
53
    const DEVICE_NUM = 0;
54
55
    const INODE_ASSOC = 'ino';
56
57
    const INODE_NUM = 1;
58
59
    const MODE_ASSOC = 'mode';
60
61
    const MODE_NUM = 2;
62
63
    const NUM_LINKS_ASSOC = 'nlink';
64
65
    const NUM_LINK_NUM = 3;
66
67
    const UID_ASSOC = 'uid';
68
69
    const UID_NUM = 4;
70
71
    const GID_ASSOC = 'gid';
72
73
    const GID_NUM = 5;
74
75
    const DEVICE_TYPE_ASSOC = 'rdev';
76
77
    const DEVICE_TYPE_NUM = 6;
78
79
    const SIZE_ASSOC = 'size';
80
81
    const SIZE_NUM = 7;
82
83
    const ACCESS_TIME_ASSOC = 'atime';
84
85
    const ACCESS_TIME_NUM = 8;
86
87
    const MODIFY_TIME_ASSOC = 'mtime';
88
89
    const MODIFY_TIME_NUM = 9;
90
91
    const CHANGE_TIME_ASSOC = 'ctime';
92
93
    const CHANGE_TIME_NUM = 10;
94
95
    const BLOCK_SIZE_ASSOC = 'blksize';
96
97
    const BLOCK_SIZE_NUM = 11;
98
99
    const NUM_BLOCKS_ASSOC = 'blocks';
100
101
    const NUM_BLOCKS_NUM = 12;
102
103
    /**
104
     * @var array
105
     */
106
    private static $defaultStat = array(
107
        self::DEVICE_ASSOC => -1,
108
        self::DEVICE_NUM => -1,
109
        self::INODE_ASSOC => -1,
110
        self::INODE_NUM => -1,
111
        self::MODE_ASSOC => -1,
112
        self::MODE_NUM => -1,
113
        self::NUM_LINKS_ASSOC => -1,
114
        self::NUM_LINK_NUM => -1,
115
        self::UID_ASSOC => 0,
116
        self::UID_NUM => 0,
117
        self::GID_ASSOC => 0,
118
        self::GID_NUM => 0,
119
        self::DEVICE_TYPE_ASSOC => -1,
120
        self::DEVICE_TYPE_NUM => -1,
121
        self::SIZE_ASSOC => 0,
122
        self::SIZE_NUM => 0,
123
        self::ACCESS_TIME_ASSOC => -1,
124
        self::ACCESS_TIME_NUM => -1,
125
        self::MODIFY_TIME_ASSOC => -1,
126
        self::MODIFY_TIME_NUM => -1,
127
        self::CHANGE_TIME_ASSOC => -1,
128
        self::CHANGE_TIME_NUM => -1,
129
        self::BLOCK_SIZE_ASSOC => -1,
130
        self::BLOCK_SIZE_NUM => -1,
131
        self::NUM_BLOCKS_ASSOC => 0,
132
        self::NUM_BLOCKS_NUM => 0,
133
    );
134
135
    /**
136
     * @var ResourceRepository[]|callable[]
137
     */
138
    private static $repos;
139
140
    /**
141
     * @var resource
142
     */
143
    private $handle;
144
145
    /**
146
     * @var ResourceCollectionIterator
147
     */
148
    private $childIterator;
149
150
    /**
151
     * Registers a repository as PHP stream wrapper.
152
     *
153
     * The resources of the repository can subsequently be accessed with PHP's
154
     * file system by prefixing the resource paths with the registered URI
155
     * scheme:
156
     *
157
     * ```php
158
     * ResourceStreamWrapper::register('puli', $repo);
159
     *
160
     * // /app/css/style.css
161
     * $contents = file_get_contents('puli:///app/css/style.css');
162
     * ```
163
     *
164
     * Instead of passing a repository, you can also pass a callable. The
165
     * callable is executed when the repository is accessed for the first time
166
     * and should return a valid {@link ResourceRepository} instance.
167
     *
168
     * @param string                      $scheme            The URI scheme.
169
     * @param ResourceRepository|callable $repositoryFactory The repository to use.
170
     *
171
     * @throws StreamWrapperException If a repository was previously registered
172
     *                                for the same scheme. Call
173
     *                                {@link unregister()} to unregister the
174
     *                                scheme first.
175
     */
176 54
    public static function register($scheme, $repositoryFactory)
177
    {
178 54
        if (!$repositoryFactory instanceof ResourceRepository
179 54
                && !is_callable($repositoryFactory)) {
180 1
            throw new InvalidArgumentException(sprintf(
181
                'The repository factory should be a callable or an instance '.
182 1
                'of ResourceRepository. Got: %s',
183
                $repositoryFactory
184
            ));
185
        }
186
187 53
        Assert::string($scheme, 'The scheme must be a string. Got: %s');
188 52
        Assert::alnum($scheme, 'The scheme %s should consist of letters and digits only.');
189 51
        Assert::startsWithLetter($scheme, 'The scheme %s should start with a letter.');
190
191 50
        if (isset(self::$repos[$scheme])) {
192 1
            throw new StreamWrapperException(sprintf(
193 1
                'The scheme "%s" has already been registered.',
194
                $scheme
195
            ));
196
        }
197
198 50
        self::$repos[$scheme] = $repositoryFactory;
199
200 50
        stream_wrapper_register($scheme, __CLASS__);
201 50
    }
202
203
    /**
204
     * Unregisters the given scheme.
205
     *
206
     * Unknown schemes are ignored.
207
     *
208
     * @param string $scheme A URI scheme.
209
     */
210 56
    public static function unregister($scheme)
211
    {
212 56
        if (!isset(self::$repos[$scheme])) {
213 6
            return;
214
        }
215
216 50
        unset(self::$repos[$scheme]);
217
218 50
        stream_wrapper_unregister($scheme);
219 50
    }
220
221
    /**
222
     * {@inheritdoc}
223
     *
224
     * @internal
225
     */
226 3
    public function dir_opendir($uri, $options)
227
    {
228 3
        $parts = Uri::parse($uri);
229
230
        // Provoke ResourceNotFoundException if not found
231 3
        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);
232
233 2
        $this->childIterator = new ResourceCollectionIterator(
234 2
            $resource->listChildren(),
235 2
            ResourceCollectionIterator::CURRENT_AS_NAME
236
        );
237
238 2
        return true;
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     *
244
     * @internal
245
     */
246 2
    public function dir_closedir()
247
    {
248 2
        $this->childIterator = null;
249
250 2
        return false;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     *
256
     * @internal
257
     */
258 2
    public function dir_readdir()
259
    {
260 2
        if (!$this->childIterator->valid()) {
261 2
            return false;
262
        }
263
264 2
        $name = $this->childIterator->current();
265
266 2
        $this->childIterator->next();
267
268 2
        return $name;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     *
274
     * @internal
275
     */
276 1
    public function dir_rewinddir()
277
    {
278 1
        $this->childIterator->rewind();
279
280 1
        return true;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     *
286
     * @internal
287
     */
288 1
    public function mkdir($uri, $mode, $options)
289
    {
290 1
        throw new UnsupportedOperationException(sprintf(
291
            'The creation of new directories through the stream wrapper is '.
292 1
            'not supported. Tried to create the directory "%s".',
293
            $uri
294
        ));
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     *
300
     * @internal
301
     */
302 1
    public function rename($uriFrom, $uriTo)
303
    {
304 1
        $parts = Uri::parse($uriFrom);
305
306
        // validate whether the URL exists
307 1
        $this->getRepository($parts['scheme'])->get($parts['path']);
308
309 1
        throw new UnsupportedOperationException(sprintf(
310
            'The renaming of resources through the stream wrapper is not '.
311 1
            'supported. Tried to rename "%s" to "%s".',
312
            $uriFrom,
313
            $uriTo
314
        ));
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     *
320
     * @internal
321
     */
322 1
    public function rmdir($uri, $options)
323
    {
324 1
        $parts = Uri::parse($uri);
325
326
        // validate whether the URL exists
327 1
        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);
328
329 1
        throw new UnsupportedOperationException(sprintf(
330
            'The removal of directories through the stream wrapper is not '.
331 1
            'supported. Tried to remove "%s"%s.',
332
            $uri,
333
            $resource instanceof FilesystemResource
334
                ? sprintf(' which points to "%s"', $resource->getFilesystemPath())
335 1
                : ''
336
        ));
337
    }
338
339
    /**
340
     * {@inheritdoc}
341
     *
342
     * @internal
343
     */
344 2
    public function stream_cast($castAs)
345
    {
346 2
        return $this->handle;
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     *
352
     * @internal
353
     */
354 20
    public function stream_close()
355
    {
356 20
        assert(null !== $this->handle);
357
358 20
        return fclose($this->handle);
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     *
364
     * @internal
365
     */
366 8
    public function stream_eof()
367
    {
368 8
        assert(null !== $this->handle);
369
370 8
        return feof($this->handle);
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     *
376
     * @internal
377
     */
378
    public function stream_flush()
379
    {
380
        assert(null !== $this->handle);
381
382
        return fflush($this->handle);
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     *
388
     * @internal
389
     */
390 2
    public function stream_lock($operation)
391
    {
392 2
        throw new UnsupportedOperationException(
393
            'The locking of files through the stream wrapper is not '.
394 2
            'supported.'
395
        );
396
    }
397
398
    /**
399
     * {@inheritdoc}
400
     *
401
     * @internal
402
     */
403 9
    public function stream_metadata($uri, $option)
404
    {
405
        switch ($option) {
406 9
            case STREAM_META_TOUCH:
407 3
                throw new UnsupportedOperationException(sprintf(
408
                    'Touching files through the stream wrapper is not '.
409 3
                    'supported. Tried to touch "%s".',
410
                    $uri
411
                ));
412
413 6
            case STREAM_META_OWNER:
414 6
            case STREAM_META_OWNER_NAME:
415 2
                throw new UnsupportedOperationException(sprintf(
416
                    'Changing file ownership through the stream wrapper '.
417 2
                    'is not supported. Tried to chown "%s".',
418
                    $uri
419
                ));
420
421 4
            case STREAM_META_GROUP:
422 4
            case STREAM_META_GROUP_NAME:
423 2
                throw new UnsupportedOperationException(sprintf(
424
                    'Changing file groups through the stream wrapper '.
425 2
                    'is not supported. Tried to chgrp "%s".',
426
                    $uri
427
                ));
428
429 2
            case STREAM_META_ACCESS:
430 2
                throw new UnsupportedOperationException(sprintf(
431
                    'Changing file permissions through the stream wrapper '.
432 2
                    'is not supported. Tried to chmod "%s".',
433
                    $uri
434
                ));
435
        }
436
    }
437
438
    /**
439
     * {@inheritdoc}
440
     *
441
     * @internal
442
     */
443 32
    public function stream_open($uri, $mode, $options, &$openedPath)
444
    {
445 32
        if (!preg_match('/^[rbt]+$/', $mode)) {
446 9
            throw new UnsupportedOperationException(sprintf(
447
                'Resources can only be opened for reading. Tried to open "%s" '.
448 9
                'with mode "%s".',
449
                $uri,
450
                $mode
451
            ));
452
        }
453
454 23
        $parts = Uri::parse($uri);
455
456 23
        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);
457
458 21
        if (!$resource instanceof BodyResource) {
459 1
            throw new UnsupportedResourceException(sprintf(
460
                'Can only open file resources for reading. Tried to open "%s" '.
461 1
                'of type %s which does not implement BodyResource.',
462
                $uri,
463
                get_class($resource)
464
            ));
465
        }
466
467 20
        if ($resource instanceof FilesystemResource) {
468 11
            $this->handle = fopen($resource->getFilesystemPath(), 'r', $options & STREAM_USE_PATH) ?: null;
469
470 11
            return null !== $this->handle;
471
        }
472
473 9
        $this->handle = fopen('php://temp', 'r+', $options & STREAM_USE_PATH);
474 9
        fwrite($this->handle, $resource->getBody());
475 9
        rewind($this->handle);
476
477 9
        return true;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     *
483
     * @internal
484
     */
485 8
    public function stream_read($length)
486
    {
487 8
        assert(null !== $this->handle);
488
489 8
        return fread($this->handle, $length);
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     *
495
     * @internal
496
     */
497 6
    public function stream_seek($offset, $whence = SEEK_SET)
498
    {
499 6
        assert(null !== $this->handle);
500
501 6
        return 0 === fseek($this->handle, $offset, $whence);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     *
507
     * @internal
508
     */
509
    public function stream_set_option($option, $arg1, $arg2)
510
    {
511
        // noop
512
    }
513
514
    /**
515
     * {@inheritdoc}
516
     *
517
     * @internal
518
     */
519 4
    public function stream_stat()
520
    {
521 4
        assert(null !== $this->handle);
522
523 4
        return fstat($this->handle);
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     *
529
     * @internal
530
     */
531 6
    public function stream_tell()
532
    {
533 6
        assert(null !== $this->handle);
534
535 6
        return ftell($this->handle);
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     *
541
     * @internal
542
     */
543
    public function stream_truncate($newSize)
544
    {
545
        assert(null !== $this->handle);
546
547
        return ftruncate($this->handle, $newSize);
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     *
553
     * @internal
554
     */
555
    public function stream_write($data)
556
    {
557
        assert(null !== $this->handle);
558
559
        return fwrite($this->handle, $data);
560
    }
561
562
    /**
563
     * {@inheritdoc}
564
     *
565
     * @internal
566
     */
567 2
    public function unlink($uri)
568
    {
569 2
        throw new UnsupportedOperationException(sprintf(
570
            'The removal of files through the stream wrapper is not '.
571 2
            'supported. Tried to remove "%s".',
572
            $uri
573
        ));
574
    }
575
576
    /**
577
     * {@inheritdoc}
578
     *
579
     * @internal
580
     */
581 1
    public function url_stat($uri, $flags)
582
    {
583
        try {
584 1
            $parts = Uri::parse($uri);
585
586 1
            $resource = $this->getRepository($parts['scheme'])->get($parts['path']);
587
588 1
            if ($resource instanceof FilesystemResource) {
589 1
                $path = $resource->getFilesystemPath();
590
591 1
                if ($flags & STREAM_URL_STAT_LINK) {
592
                    return lstat($path);
593
                }
594
595 1
                return stat($path);
596
            }
597
598 1
            $stat = self::$defaultStat;
599
600 1
            $metadata = $resource->getMetadata();
601
602 1
            $stat[self::SIZE_NUM] = $stat[self::SIZE_ASSOC] = $metadata->getSize();
603 1
            $stat[self::ACCESS_TIME_NUM] = $stat[self::ACCESS_TIME_ASSOC] = $metadata->getAccessTime();
604 1
            $stat[self::MODIFY_TIME_NUM] = $stat[self::MODIFY_TIME_ASSOC] = $metadata->getModificationTime();
605
606 1
            return $stat;
607 1
        } catch (ResourceNotFoundException $e) {
608 1
            if ($flags & STREAM_URL_STAT_QUIET) {
609
                // Same result as stat() returns on error
610
                // file_exists() returns false for this resource
611 1
                return false;
612
            }
613
614
            throw $e;
615
        }
616
    }
617
618
    /**
619
     * Constructs (if necessary) and returns the repository for the given scheme.
620
     *
621
     * @param string $scheme A URI scheme.
622
     *
623
     * @return ResourceRepository The resource repository.
624
     *
625
     * @throws RepositoryFactoryException If the callable did not return an
626
     *                                    instance of {@link ResourceRepository}.
627
     * @throws StreamWrapperException     If the scheme is not supported.
628
     */
629 29
    private function getRepository($scheme)
630
    {
631 29
        if (!isset(self::$repos[$scheme])) {
632 1
            throw new StreamWrapperException(sprintf(
633
                'The stream wrapper has not been registered for the scheme "%s". '.
634 1
                'Please call ResourceStreamWrapper::register() first.',
635
                $scheme
636
            ));
637
        }
638
639 28
        if (is_callable(self::$repos[$scheme])) {
640 3
            $callable = self::$repos[$scheme];
641 3
            $result = $callable($scheme);
642
643 3 View Code Duplication
            if (!$result instanceof ResourceRepository) {
644 1
                throw new RepositoryFactoryException(sprintf(
645
                    'The repository factory registered for scheme "%s" should '.
646 1
                    'return a ResourceRepository instance. Got: %s',
647
                    $scheme,
648 1
                    is_object($result) ? get_class($result) : gettype($result)
649
                ));
650
            }
651
652 2
            self::$repos[$scheme] = $result;
653
        }
654
655 27
        return self::$repos[$scheme];
656
    }
657
}
658