Path   D
last analyzed

Complexity

Total Complexity 91

Size/Duplication

Total Lines 632
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 91
lcom 1
cbo 0
dl 0
loc 632
ccs 230
cts 230
cp 1
rs 4.7423
c 0
b 0
f 0

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
B parse() 0 18 8
A extractScheme() 0 8 2
A stripScheme() 0 4 2
A containsRoot() 0 13 4
A getPlatformBySeparator() 0 12 3
A detectPlatform() 0 4 1
B tryAdapt() 0 12 5
A adapt() 0 8 2
B normalize() 0 24 6
A enumerate() 0 14 2
A iterator() 0 4 1
A getParents() 0 6 1
A isAbsolute() 0 4 1
A isRelative() 0 4 1
A isRoot() 0 4 2
B isDescendantOf() 0 21 6
A isAncestorOf() 0 4 1
A isChildOf() 0 9 2
A isParentOf() 0 4 1
A isSiblingOf() 0 8 3
A getParent() 0 9 2
A resolve() 0 10 2
B relativize() 0 33 6
C compareTo() 0 29 8
A compareStrings() 0 10 4
A equals() 0 4 1
A getScheme() 0 4 1
A getRoot() 0 4 1
A getSegments() 0 4 1
A getSeparator() 0 4 1
A withScheme() 0 6 1
A withoutScheme() 0 4 1
A withRoot() 0 6 1
A withoutRoot() 0 4 1
A assemble() 0 11 3
A __toString() 0 4 1
A toPlatformString() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Path 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 Path, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AmaTeam\Pathetic;
4
5
use ArrayIterator;
6
use Iterator;
7
use RuntimeException;
8
9
/**
10
 * This class represents abstract filesystem path - regardless of underlying
11
 * operating system. It is represented just as (sometimes) scheme, (sometimes)
12
 * root point, and a list of segments of which path consists. It provides lots
13
 * of auxiliary methods (like hierarchy comparison or paths resolution) that
14
 * help to deal with paths in an OS-independent way, which, i hope, will result
15
 * in easier pathwork in any library.
16
 *
17
 * @author Etki <[email protected]>
18
 */
19
class Path
20
{
21
    const PLATFORM_UNIX = 'unix';
22
    const PLATFORM_WINDOWS = 'windows';
23
24
    /**
25
     * Separator that is used to render platform-independent representations.
26
     */
27
    const SEPARATOR = '/';
28
29
    /**
30
     * Path scheme, if any was used.
31
     *
32
     * @var string|null
33
     */
34
    private $scheme;
35
36
    /**
37
     * Path root. May be null (relative path), empty string (unix root or
38
     * windows path \like\that) or drive name (windows-only).
39
     *
40
     * @var string|null
41
     */
42
    private $root;
43
44
    /**
45
     * List of path segments.
46
     *
47
     * @var string[]
48
     */
49
    private $segments;
50
51
    /**
52
     * Separator that will be used to assemble path back.
53
     *
54
     * @var string
55
     */
56
    private $separator;
57
58
    /**
59
     * @param string|null $scheme
60
     * @param string|null $root
61
     * @param string[] $segments
62
     * @param string $separator
63
     */
64 98
    public function __construct($scheme, $root, array $segments, $separator)
65
    {
66 98
        $this->scheme = $scheme;
67 98
        $this->root = $root;
68 98
        $this->segments = $segments;
69 98
        $this->separator = $separator;
70 98
    }
71
72
    /**
73
     * Parses provided path into {@link Path} instance, acting as a public
74
     * constructor for whole Path class.
75
     *
76
     * @param string $input Path to be parsed
77
     * @param string|null $platform Platform used to determine correct
78
     * algorithm. If omitted/set to null, will be detected automatically.
79
     *
80
     * @return Path
81
     */
82 98
    public static function parse($input, $platform = null)
83
    {
84 98
        $platform = $platform ?: static::detectPlatform();
85 98
        $separator = $platform === static::PLATFORM_UNIX ? '/' : '\\';
86 98
        $scheme = static::extractScheme($input);
87 98
        $scheme = empty($scheme) ? null : $scheme;
88 98
        $path = static::stripScheme($input, $scheme);
89 98
        if ($platform === static::PLATFORM_WINDOWS) {
90 34
            $path = str_replace('\\', '/', $path);
91 34
        }
92 98
        $segments = $path === '' ? [] : explode('/', $path);
93 98
        $root = null;
94 98
        if (static::containsRoot($segments, $platform)) {
95 37
            $root = array_shift($segments);
96 37
            $segments = $segments === [''] ? [] : $segments;
97 37
        }
98 98
        return new static($scheme, $root, $segments, $separator);
99
    }
100
101 98
    protected static function extractScheme($input)
102
    {
103 98
        $position = strpos($input, '://');
104 98
        if ($position === false) {
105 89
            return null;
106
        }
107 10
        return substr($input, 0, $position);
108
    }
109
110 98
    protected static function stripScheme($input, $scheme)
111
    {
112 98
        return empty($scheme) ? $input : substr($input, strlen($scheme) + 3);
113
    }
114
115 98
    protected static function containsRoot(array $segments, $platform)
116
    {
117 98
        if (empty($segments)) {
118 7
            return false;
119
        }
120 94
        if ($segments[0] === '') {
121 23
            return true;
122
        }
123 76
        if ($platform === static::PLATFORM_UNIX) {
124 48
            return false;
125
        }
126 28
        return strpos($segments[0], ':') > 0;
127
    }
128
129
    /**
130
     * Returns platform matching the separator. Exists for 100% coverage :P
131
     *
132
     * @param string $separator
133
     * @return string
134
     */
135 32
    public static function getPlatformBySeparator($separator)
136
    {
137
        switch ($separator) {
138 32
            case '/':
139 31
                return static::PLATFORM_UNIX;
140 2
            case '\\':
141 1
                return static::PLATFORM_WINDOWS;
142 1
            default:
143 1
                $format = 'Unknown directory separator %s';
144 1
                throw new RuntimeException(sprintf($format, $separator));
145 1
        }
146
    }
147
148
    /**
149
     * Detects current platform.
150
     *
151
     * @return string
152
     */
153 31
    public static function detectPlatform()
154
    {
155 31
        return static::getPlatformBySeparator(DIRECTORY_SEPARATOR);
156
    }
157
158
    /**
159
     * Converts input into path. Throws runtime exception if that's not
160
     * possible.
161
     *
162
     * @param Path|string|mixed $input
163
     * @return Path|null
164
     */
165 45
    protected function tryAdapt($input)
166
    {
167 45
        if ($input instanceof Path) {
168 43
            return $input;
169
        }
170 3
        if (is_object($input)) {
171 1
            if (method_exists($input, '__toString')) {
172 1
                $input = $input->__toString();
173 1
            }
174 1
        }
175 3
        return is_string($input) ? static::parse($input) : null;
176
    }
177
178
    /**
179
     * Converts input into path. Throws runtime exception if that's not
180
     * possible.
181
     *
182
     * @param Path|string $input
183
     * @return Path
184
     */
185 35
    protected function adapt($input)
186
    {
187 35
        $input = $this->tryAdapt($input);
188 35
        if ($input === null) {
189 1
            throw new RuntimeException('Invalid input provided');
190
        }
191 34
        return $input;
192
    }
193
194
    /**
195
     * Creates new path, destroying as much empty ('') and dot-entries
196
     * ('.', '..') as possible, so path 'node/./../leaf' will be truncated
197
     * just to leaf, but 'node/../../leaf' would result in '../leaf'.
198
     *
199
     * @return Path
200
     */
201
    public function normalize()
202
    {
203
        /**
204
         * @param string[] $carrier
205
         * @param string $segment
206
         * @return string[]
207
         */
208 44
        $reducer = function ($carrier, $segment) {
209 43
            if ($segment === '' || $segment === '.') {
210 6
                return $carrier;
211
            }
212 43
            if ($segment === '..' && !empty($carrier) && end($carrier) !== '..') {
213 18
                array_pop($carrier);
214 18
                return $carrier;
215
            }
216 43
            $carrier[] = $segment;
217 43
            return $carrier;
218 44
        };
219 44
        $copy = clone $this;
220
        /** @var string[] $segments */
221 44
        $segments = array_reduce($this->segments, $reducer, []);
222 44
        $copy->segments = $segments;
223 44
        return $copy;
224
    }
225
226
    /**
227
     * Returns hierarchy branch from the topmost node (root, if present) down
228
     * to current node, so `/a/b/c` will result in `/a`, `  /a/b` and `/a/b/c`,
229
     * and `a/b` in `a` and `a/b`.
230
     *
231
     * @return Path[]
232
     */
233 5
    public function enumerate()
234
    {
235 5
        $accumulator = [];
236 5
        $results = [];
237 5
        $normalized = $this->normalize();
238 5
        foreach ($normalized->segments as $segment) {
239 5
            $copy = clone $normalized;
240 5
            $copy->segments = $accumulator;
241 5
            $results[] = $copy;
242 5
            $accumulator[] = $segment;
243 5
        }
244 5
        $results[] = $this;
245 5
        return $results;
246
    }
247
248
    /**
249
     * Returns iterator that will iterate hierarchy branch according to
250
     * {@link #enumerate} rules.
251
     *
252
     * @return Iterator Iterator that emits Path instances.
253
     */
254 5
    public function iterator()
255
    {
256 5
        return new ArrayIterator($this->enumerate());
257
    }
258
259
    /**
260
     * Returns list of path parents, starting from the topmost one and going
261
     * down one by one, similar to {@link #enumerate}.
262
     *
263
     * @return Path[]
264
     */
265 5
    public function getParents()
266
    {
267 5
        $enumeration = $this->enumerate();
268 5
        array_pop($enumeration);
269 5
        return $enumeration;
270
    }
271
272
    /**
273
     * @return bool
274
     */
275 17
    public function isAbsolute()
276
    {
277 17
        return $this->root !== null;
278
    }
279
280
    /**
281
     * @return bool
282
     */
283 9
    public function isRelative()
284
    {
285 9
        return $this->root === null;
286
    }
287
288
    /**
289
     * @return bool
290
     */
291 26
    public function isRoot()
292
    {
293 26
        return $this->root !== null && empty($this->segments);
294
    }
295
296
    /**
297
     * Returns true if $other is located on the same hierarchy branch higher
298
     * that current path.
299
     *
300
     * If path schemes differ, or not both paths are relative/absolute, this
301
     * method will instantly return false.
302
     *
303
     * @param Path|string $other
304
     * @return bool
305
     */
306 12
    public function isDescendantOf($other)
307
    {
308 12
        $other = $this->adapt($other)->normalize();
309 12
        $current = $this->normalize();
310 12
        if ($current->scheme !== $other->scheme) {
311 1
            return false;
312
        }
313 11
        if ($current->root !== $other->root) {
314 1
            return false;
315
        }
316 10
        if (sizeof($current->segments) <= sizeof($other->segments)) {
317 9
            return false;
318
        }
319 9
        $limit = sizeof($other->segments);
320 9
        for ($i = 0; $i < $limit; $i++) {
321 6
            if ($other->segments[$i] !== $current->segments[$i]) {
322 1
                return false;
323
            }
324 6
        }
325 8
        return true;
326
    }
327
328
    /**
329
     * Returns true if $other is located on the same hierarchy branch deeper
330
     * that current path.
331
     *
332
     * If path schemes differ, or not both paths are relative/absolute, this
333
     * method will instantly return false.
334
     *
335
     * @param Path|string $other
336
     * @return bool
337
     */
338 11
    public function isAncestorOf($other)
339
    {
340 11
        return $this->adapt($other)->isDescendantOf($this);
341
    }
342
343
    /**
344
     * Returns true if $other is direct parent of current path.
345
     *
346
     * If path schemes differ, or not both paths are relative/absolute, this
347
     * method will instantly return false.
348
     *
349
     * @param Path|string $other
350
     * @return bool
351
     */
352 13
    public function isChildOf($other)
353
    {
354 13
        $other = $this->adapt($other)->normalize();
355 12
        $current = $this->normalize();
356 12
        if (sizeof($other->segments) !== sizeof($current->segments) - 1) {
357 11
            return false;
358
        }
359 9
        return $current->isDescendantOf($other);
360
    }
361
362
    /**
363
     * Returns true if $other is direct child of current path.
364
     *
365
     * If path schemes differ, or not both paths are relative/absolute, this
366
     * method will instantly return false.
367
     *
368
     * @param Path|string $other
369
     * @return bool
370
     */
371 12
    public function isParentOf($other)
372
    {
373 12
        return $this->adapt($other)->isChildOf($this);
374
    }
375
376
    /**
377
     * Returns true if $other is located on the smae hierarchy level.
378
     *
379
     * If path schemes differ, or not both paths are relative/absolute, this
380
     * method will instantly return false.
381
     *
382
     * @param Path|string $other
383
     * @return bool
384
     */
385 11
    public function isSiblingOf($other)
386
    {
387 11
        $other = $this->adapt($other);
388 11
        if ($other->isRoot()) {
389 1
            return $this->isRoot() && $this->root === $other->root;
390
        }
391 10
        return $other->getParent()->isParentOf($this);
392
    }
393
394
    /**
395
     * Returns direct parent of current path. In case of call on root node
396
     * exception will be thrown.
397
     *
398
     * @return Path
399
     */
400 16
    public function getParent()
401
    {
402 16
        if ($this->isRoot()) {
403 1
            throw new RuntimeException('Root cannot have parent node');
404
        }
405 15
        $copy = clone $this;
406 15
        $copy->segments[] = '..';
407 15
        return $copy->normalize();
408
    }
409
410
    /**
411
     * Resolves other path against current one (or, in other words, prepends
412
     * current path to provided one). If provided path is absolute, it is
413
     * returned as-is, otherwise it is appended to current one:
414
     *
415
     * /a/b resolve /c/d => /b/c
416
     * /a/b resolve c/d => /a/b/c/d
417
     * a/b resolve c/d => a/b/c/d
418
     *
419
     * @param Path|string $other
420
     *
421
     * @return Path
422
     */
423 8
    public function resolve($other)
424
    {
425 8
        $other = $this->adapt($other);
426 8
        if ($other->isAbsolute()) {
427 4
            return $other;
428
        }
429 4
        $copy = clone $this;
430 4
        $copy->segments = array_merge($copy->segments, $other->segments);
431 4
        return $copy;
432
    }
433
434
    /**
435
     * Construct a relative path, trying to subtract current path from provided
436
     * one (thus providing a path required to traverse from current one to
437
     * provided one). If called with an absolute path against relative or vice
438
     * versa, will return other path as is.
439
     *
440
     * @param Path|string $other
441
     *
442
     * @return Path
443
     */
444 14
    public function relativize($other)
445
    {
446 14
        $other = $this->adapt($other);
447 14
        if ($other->root !== $this->root) {
448 4
            return $other;
449
        }
450 10
        $current = $this->normalize();
451 10
        $other = $other->normalize();
452 10
        $counter = 0;
453 10
        $count = sizeof($current->segments);
454 10
        for ($i = 0; $i < $count; $i++) {
455 10
            if (!isset($other->segments[$i])) {
456 2
                break;
457
            }
458 10
            if ($current->segments[$i] !== $other->segments[$i]) {
459 4
                break;
460
            }
461 10
            $counter++;
462 10
        }
463
        /** @var string[] $traversal */
464 10
        $traversal = [];
465 10
        if ($count - $counter > 0) {
466 6
            $traversal = array_fill(0, $count - $counter, '..');
467 6
        }
468
        /** @var string[] $slice */
469 10
        $slice = array_slice($other->segments, $counter);
470
        /** @var string[] $segments */
471 10
        $segments = array_merge($traversal, $slice);
472 10
        $copy = clone $this;
473 10
        $copy->root = null;
474 10
        $copy->segments = $segments;
475 10
        return $copy;
476
    }
477
478
    /**
479
     * Lexicographically compares one path to another.
480
     *
481
     * @param Path|string|null $other
482
     * @return int
483
     */
484 10
    public function compareTo($other)
485
    {
486 10
        $other = $this->tryAdapt($other);
487 10
        if ($other === null) {
488 1
            return 1;
489
        }
490 9
        $other = $other->normalize();
491 9
        $current = $this->normalize();
492 9
        $schemes = $this->compareStrings($current->scheme, $other->scheme);
493 9
        if ($schemes !== 0) {
494 1
            return $schemes;
495
        }
496 8
        $roots = $this->compareStrings($current->root, $other->root);
497 8
        if ($roots !== 0) {
498 1
            return $roots;
499
        }
500 7
        $limit = max(sizeof($current->segments), sizeof($other->segments));
501 7
        for ($i = 0; $i < $limit; $i++) {
502 7
            if (!isset($current->segments[$i])) {
503 1
                return -1;
504 7
            } else if (!isset($other->segments[$i])) {
505 1
                return 1;
506 7
            } else if ($current->segments[$i] === $other->segments[$i]) {
507 7
                continue;
508
            }
509 2
            return strcmp($current->segments[$i], $other->segments[$i]);
510
        }
511 3
        return 0;
512
    }
513
514
    /**
515
     * Helper method for {@link #compareTo()}
516
     *
517
     * @param string|null $left
518
     * @param string|null $right
519
     * @return int
520
     */
521 9
    protected function compareStrings($left, $right)
522
    {
523 9
        if ($left === null) {
524 8
            return $right === null ? 0 : -1;
525
        }
526 4
        if ($right === null) {
527 2
            return 1;
528
        }
529 2
        return strcmp($left, $right);
530
    }
531
532
    /**
533
     * Compares current path to provided one and tells if they are equal.
534
     *
535
     * @param Path|string|null $other
536
     * @return bool
537
     */
538 10
    public function equals($other)
539
    {
540 10
        return $this->compareTo($other) === 0;
541
    }
542
543
    /**
544
     * @return string|null
545
     */
546 16
    public function getScheme()
547
    {
548 16
        return $this->scheme;
549
    }
550
551
    /**
552
     * @return string|null
553
     */
554 16
    public function getRoot()
555
    {
556 16
        return $this->root;
557
    }
558
559
    /**
560
     * @return string[]
561
     */
562 18
    public function getSegments()
563
    {
564 18
        return $this->segments;
565
    }
566
567
    /**
568
     * @return string
569
     */
570 15
    public function getSeparator()
571
    {
572 15
        return $this->separator;
573
    }
574
575
    /**
576
     * @param string|null $scheme
577
     * @return Path
578
     */
579 1
    public function withScheme($scheme)
580
    {
581 1
        $copy = clone ($this);
582 1
        $copy->scheme = $scheme;
583 1
        return $copy;
584
    }
585
586
    /**
587
     * @return Path
588
     */
589 1
    public function withoutScheme()
590
    {
591 1
        return $this->withScheme(null);
592
    }
593
594
    /**
595
     * @param string $root
596
     * @return Path
597
     */
598 1
    public function withRoot($root)
599
    {
600 1
        $copy = clone $this;
601 1
        $copy->root = $root;
602 1
        return $copy;
603
    }
604
605
    /**
606
     * @return Path
607
     */
608 1
    public function withoutRoot()
609
    {
610 1
        return $this->withRoot(null);
611
    }
612
613
    /**
614
     * Assembles string representation using provided separator.
615
     *
616
     * @param string $separator
617
     * @return string
618
     */
619 43
    protected function assemble($separator)
620
    {
621 43
        $builder = '';
622 43
        if (!empty($this->scheme)) {
623 2
            $builder .= $this->scheme . '://';
624 2
        }
625 43
        if ($this->root !== null) {
626 12
            $builder .= $this->root . $separator;
627 12
        }
628 43
        return $builder . implode($separator, $this->segments);
629
    }
630
631
    /**
632
     * Returns platform-independent representation.
633
     *
634
     * @return string
635
     */
636 43
    public function __toString()
637
    {
638 43
        return $this->assemble(static::SEPARATOR);
639
    }
640
641
    /**
642
     * Returns representation for target platform.
643
     *
644
     * @return string
645
     */
646 13
    public function toPlatformString()
647
    {
648 13
        return $this->assemble($this->separator);
649
    }
650
}
651