Test Failed
Branch dev (208e24)
by Fike
11:01
created

Path::withRoot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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
    public function __construct($scheme, $root, array $segments, $separator)
65
    {
66
        $this->scheme = $scheme;
67
        $this->root = $root;
68
        $this->segments = $segments;
69
        $this->separator = $separator;
70
    }
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
    public static function parse($input, $platform = null)
83
    {
84
        $platform = $platform ?: static::detectPlatform();
85
        $separator = $platform === static::PLATFORM_UNIX ? '/' : '\\';
86
        $scheme = static::extractScheme($input);
87
        $scheme = empty($scheme) ? null : $scheme;
88
        $path = static::stripScheme($input, $scheme);
89
        if ($platform === static::PLATFORM_WINDOWS) {
90
            $path = str_replace('\\', '/', $path);
91
        }
92
        $segments = $path === '' ? [] : explode('/', $path);
93
        $root = null;
94
        if (static::containsRoot($segments, $platform)) {
95
            $root = array_shift($segments);
96
            $segments = $segments === [''] ? [] : $segments;
97
        }
98
        return new static($scheme, $root, $segments, $separator);
99
    }
100
101
    protected static function extractScheme($input)
102
    {
103
        $position = strpos($input, '://');
104
        if ($position === false) {
105
            return null;
106
        }
107
        return substr($input, 0, $position);
108
    }
109
110
    protected static function stripScheme($input, $scheme)
111
    {
112
        return empty($scheme) ? $input : substr($input, strlen($scheme) + 3);
113
    }
114
115
    protected static function containsRoot(array $segments, $platform)
116
    {
117
        if (empty($segments)) {
118
            return false;
119
        }
120
        if ($segments[0] === '') {
121
            return true;
122
        }
123
        if ($platform === static::PLATFORM_UNIX) {
124
            return false;
125
        }
126
        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
    public static function getPlatformBySeparator($separator)
136
    {
137
        switch ($separator) {
138
            case '/':
139
                return static::PLATFORM_UNIX;
140
            case '\\':
141
                return static::PLATFORM_WINDOWS;
142
            default:
143
                $format = 'Unknown directory separator %s';
144
                throw new RuntimeException(sprintf($format, $separator));
145
        }
146
    }
147
148
    /**
149
     * Detects current platform.
150
     *
151
     * @return string
152
     */
153
    public static function detectPlatform()
154
    {
155
        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 $input
163
     * @return Path|null
164
     */
165
    protected function tryAdapt($input)
166
    {
167
        if ($input instanceof Path) {
168
            return $input;
169
        }
170
        if (is_object($input)) {
171
            if (method_exists($input, '__toString')) {
172
                $input = $input->__toString();
173
            }
174
        }
175
        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
    protected function adapt($input)
186
    {
187
        $input = $this->tryAdapt($input);
188
        if ($input === null) {
189
            throw new RuntimeException('Invalid input provided');
190
        }
191
        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
        $reducer = function ($carrier, $segment) {
209
            if ($segment === '' || $segment === '.') {
210
                return $carrier;
211
            }
212
            if ($segment === '..' && !empty($carrier) && end($carrier) !== '..') {
213
                array_shift($carrier);
214
                return $carrier;
215
            }
216
            $carrier[] = $segment;
217
            return $carrier;
218
        };
219
        $copy = clone $this;
220
        $copy->segments = array_reduce($this->segments, $reducer, []);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_reduce($this->segments, $reducer, array()) of type * is incompatible with the declared type array<integer,string> of property $segments.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
221
        return $copy;
222
    }
223
224
    /**
225
     * Returns hierarchy branch from the topmost node (root, if present) down
226
     * to current node, so `/a/b/c` will result in `/a`, `/a/b` and `/a/b/c`,
227
     * and `a/b` in `a` and `a/b`.
228
     *
229
     * @return Path[]
230
     */
231
    public function enumerate()
232
    {
233
        $accumulator = [];
234
        $results = [];
235
        $normalized = $this->normalize();
236
        foreach ($normalized->segments as $segment) {
237
            $copy = clone $normalized;
238
            $copy->segments = $accumulator;
239
            $results[] = $copy;
240
            $accumulator[] = $segment;
241
        }
242
        $results[] = $this;
243
        return $results;
244
    }
245
246
    /**
247
     * Returns iterator that will iterate hierarchy branch according to
248
     * {@link #enumerate} rules.
249
     *
250
     * @return Iterator Iterator that emits Path instances.
251
     */
252
    public function iterator()
253
    {
254
        return new ArrayIterator($this->enumerate());
255
    }
256
257
    /**
258
     * Returns list of path parents, starting from the topmost one and going
259
     * down one by one, similar to {@link #enumerate}.
260
     *
261
     * @return Path[]
262
     */
263
    public function getParents()
264
    {
265
        $enumeration = $this->enumerate();
266
        array_pop($enumeration);
267
        return $enumeration;
268
    }
269
270
    /**
271
     * @return bool
272
     */
273
    public function isAbsolute()
274
    {
275
        return $this->root !== null;
276
    }
277
278
    /**
279
     * @return bool
280
     */
281
    public function isRelative()
282
    {
283
        return $this->root === null;
284
    }
285
286
    /**
287
     * @return bool
288
     */
289
    public function isRoot()
290
    {
291
        return $this->root !== null && empty($this->segments);
292
    }
293
294
    /**
295
     * Returns true if $other is located on the same hierarchy branch higher
296
     * that current path.
297
     *
298
     * If path schemes differ, or not both paths are relative/absolute, this
299
     * method will instantly return false.
300
     *
301
     * @param Path|string $other
302
     * @return bool
303
     */
304
    public function isDescendantOf($other)
305
    {
306
        $other = $this->adapt($other)->normalize();
307
        $current = $this->normalize();
308
        if ($current->scheme !== $other->scheme) {
309
            return false;
310
        }
311
        if ($current->root !== $other->root) {
312
            return false;
313
        }
314
        if (sizeof($current->segments) <= sizeof($other->segments)) {
315
            return false;
316
        }
317
        $limit = sizeof($other->segments);
318
        for ($i = 0; $i < $limit; $i++) {
319
            if ($other->segments[$i] !== $current->segments[$i]) {
320
                return false;
321
            }
322
        }
323
        return true;
324
    }
325
326
    /**
327
     * Returns true if $other is located on the same hierarchy branch deeper
328
     * that current path.
329
     *
330
     * If path schemes differ, or not both paths are relative/absolute, this
331
     * method will instantly return false.
332
     *
333
     * @param Path|string $other
334
     * @return bool
335
     */
336
    public function isAncestorOf($other)
337
    {
338
        return $this->adapt($other)->isDescendantOf($this);
339
    }
340
341
    /**
342
     * Returns true if $other is direct parent of current path.
343
     *
344
     * If path schemes differ, or not both paths are relative/absolute, this
345
     * method will instantly return false.
346
     *
347
     * @param Path|string $other
348
     * @return bool
349
     */
350
    public function isChildOf($other)
351
    {
352
        $other = $this->adapt($other)->normalize();
353
        $current = $this->normalize();
354
        if (sizeof($other->segments) !== sizeof($current->segments) - 1) {
355
            return false;
356
        }
357
        return $current->isDescendantOf($other);
358
    }
359
360
    /**
361
     * Returns true if $other is direct child of current path.
362
     *
363
     * If path schemes differ, or not both paths are relative/absolute, this
364
     * method will instantly return false.
365
     *
366
     * @param Path|string $other
367
     * @return bool
368
     */
369
    public function isParentOf($other)
370
    {
371
        return $this->adapt($other)->isChildOf($this);
372
    }
373
374
    /**
375
     * Returns true if $other is located on the smae hierarchy level.
376
     *
377
     * If path schemes differ, or not both paths are relative/absolute, this
378
     * method will instantly return false.
379
     *
380
     * @param Path|string $other
381
     * @return bool
382
     */
383
    public function isSiblingOf($other)
384
    {
385
        $other = $this->adapt($other);
386
        if ($other->isRoot()) {
387
            return $this->isRoot() && $this->root === $other->root;
388
        }
389
        return $other->getParent()->isParentOf($this);
390
    }
391
392
    /**
393
     * Returns direct parent of current path. In case of call on root node
394
     * exception will be thrown.
395
     *
396
     * @return Path
397
     */
398
    public function getParent()
399
    {
400
        if ($this->isRoot()) {
401
            throw new RuntimeException('Root cannot have parent node');
402
        }
403
        $copy = clone $this;
404
        $copy->segments[] = '..';
405
        return $copy->normalize();
406
    }
407
408
    /**
409
     * Resolves other path against current one (or, in other words, prepends
410
     * current path to provided one). If provided path is absolute, it is
411
     * returned as-is, otherwise it is appended to current one:
412
     *
413
     * /a/b resolve /c/d => /b/c
414
     * /a/b resolve c/d => /a/b/c/d
415
     * a/b resolve c/d => a/b/c/d
416
     *
417
     * @param Path|string $other
418
     *
419
     * @return Path
420
     */
421
    public function resolve($other)
422
    {
423
        $other = $this->adapt($other);
424
        if ($other->isAbsolute()) {
425
            return $other;
426
        }
427
        $copy = clone $this;
428
        $copy->segments = array_merge($copy->segments, $other->segments);
429
        return $copy;
430
    }
431
432
    /**
433
     * Construct a relative path, trying to subtract current path from provided
434
     * one (thus providing a path required to traverse from current one to
435
     * provided one). If called with an absolute path against relative or vice
436
     * versa, will return other path as is.
437
     *
438
     * @param Path|string $other
439
     *
440
     * @return Path
441
     */
442
    public function relativize($other)
443
    {
444
        $other = $this->adapt($other);
445
        if ($other->root !== $this->root) {
446
            return $other;
447
        }
448
        $current = $this->normalize();
449
        $other = $other->normalize();
450
        $counter = 0;
451
        $count = sizeof($current->segments);
452
        for ($i = 0; $i < $count; $i++) {
453
            if (!isset($other->segments[$i])) {
454
                break;
455
            }
456
            if ($current->segments[$i] !== $other->segments[$i]) {
457
                break;
458
            }
459
            $counter++;
460
        }
461
        /** @var string[] $traversal */
462
        $traversal = array_fill(0, $count - $counter, '..');
463
        /** @var string[] $slice */
464
        $slice = array_slice($other->segments, $counter);
465
        $copy = clone $this;
466
        $copy->root = null;
467
        /** @var string[] segments */
468
        $copy->segments = array_merge($traversal, $slice);
469
        return $copy;
470
    }
471
472
    /**
473
     * Lexicographically compares one path to another.
474
     *
475
     * @param Path|string $other
476
     * @return int
477
     */
478
    public function compareTo($other)
479
    {
480
        $other = $this->tryAdapt($other);
481
        if ($other === null) {
482
            return 1;
483
        }
484
        $other = $other->normalize();
485
        $current = $this->normalize();
486
        $schemes = $this->compareStrings($current->scheme, $other->scheme);
487
        if ($schemes !== 0) {
488
            return $schemes;
489
        }
490
        $roots = $this->compareStrings($current->root, $other->root);
491
        if ($roots !== 0) {
492
            return $roots;
493
        }
494
        $limit = max(sizeof($current->segments), sizeof($other->segments));
495
        for ($i = 0; $i < $limit; $i++) {
496
            if (!isset($current->segments[$i])) {
497
                return -1;
498
            } else if (!isset($other->segments[$i])) {
499
                return 1;
500
            } else if ($current->segments[$i] === $other->segments[$i]) {
501
                continue;
502
            }
503
            return strcmp($current->segments[$i], $other->segments[$i]);
504
        }
505
        return 0;
506
    }
507
508
    /**
509
     * Helper method for {@link #compareTo()}
510
     *
511
     * @param string|null $left
512
     * @param string|null $right
513
     * @return int
514
     */
515
    protected function compareStrings($left, $right)
516
    {
517
        if ($left === null) {
518
            return $right === null ? 0 : -1;
519
        }
520
        if ($right === null) {
521
            return 1;
522
        }
523
        return strcmp($left, $right);
524
    }
525
526
    /**
527
     * Compares current path to provided one and tells if they are equal.
528
     *
529
     * @param Path|string|null $other
530
     * @return bool
531
     */
532
    public function equals($other)
533
    {
534
        return $this->compareTo($other) === 0;
0 ignored issues
show
Bug introduced by
It seems like $other defined by parameter $other on line 532 can also be of type null; however, AmaTeam\Pathetic\Path::compareTo() does only seem to accept object<AmaTeam\Pathetic\Path>|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
535
    }
536
537
    /**
538
     * @return string|null
539
     */
540
    public function getScheme()
541
    {
542
        return $this->scheme;
543
    }
544
545
    /**
546
     * @return string|null
547
     */
548
    public function getRoot()
549
    {
550
        return $this->root;
551
    }
552
553
    /**
554
     * @return string[]
555
     */
556
    public function getSegments()
557
    {
558
        return $this->segments;
559
    }
560
561
    /**
562
     * @return string
563
     */
564
    public function getSeparator()
565
    {
566
        return $this->separator;
567
    }
568
569
    /**
570
     * @param string|null $scheme
571
     * @return Path
572
     */
573
    public function withScheme($scheme)
574
    {
575
        $copy = clone ($this);
576
        $copy->scheme = $scheme;
577
        return $copy;
578
    }
579
580
    /**
581
     * @return Path
582
     */
583
    public function withoutScheme()
584
    {
585
        return $this->withScheme(null);
586
    }
587
588
    /**
589
     * @param string $root
590
     * @return Path
591
     */
592
    public function withRoot($root)
593
    {
594
        $copy = clone $this;
595
        $copy->root = $root;
596
        return $copy;
597
    }
598
599
    /**
600
     * @return Path
601
     */
602
    public function withoutRoot()
603
    {
604
        return $this->withRoot(null);
605
    }
606
607
    /**
608
     * Assembles string representation using provided separator.
609
     *
610
     * @param string $separator
611
     * @return string
612
     */
613
    protected function assemble($separator)
614
    {
615
        $builder = '';
616
        if (!empty($this->scheme)) {
617
            $builder .= $this->scheme . '://';
618
        }
619
        if ($this->root !== null) {
620
            $builder .= $this->root . $separator;
621
        }
622
        return $builder . implode($separator, $this->segments);
623
    }
624
625
    /**
626
     * Returns platform-independent representation.
627
     *
628
     * @return string
629
     */
630
    public function __toString()
631
    {
632
        return $this->assemble(static::SEPARATOR);
633
    }
634
635
    /**
636
     * Returns representation for target platform.
637
     *
638
     * @return string
639
     */
640
    public function toPlatformString()
641
    {
642
        return $this->assemble($this->separator);
643
    }
644
}
645