Completed
Push — next ( eb81f5...f4bd62 )
by Riikka
01:48
created

Path   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 0
dl 0
loc 167
ccs 50
cts 50
cp 1
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A normalize() 0 10 3
A join() 0 16 4
A buildPath() 0 8 3
A getParts() 0 8 2
A isAbsolute() 0 12 3
A resolve() 0 14 4
A isValidPath() 0 8 3
A resolveParent() 0 9 4
1
<?php
2
3
namespace Riimu\Kit\PathJoin;
4
5
/**
6
 * Cross-platform library for normalizing and joining file system paths.
7
 * @author Riikka Kalliomäki <[email protected]>
8
 * @copyright Copyright (c) 2014-2017 Riikka Kalliomäki
9
 * @license http://opensource.org/licenses/mit-license.php MIT License
10
 */
11
class Path
12
{
13
    /**
14
     * Normalizes the provided file system path.
15
     *
16
     * Normalizing file system paths means that all forward and backward
17
     * slashes in the path will be replaced with the system directory separator
18
     * and multiple directory separators will be condensed into one.
19
     * Additionally, all `.` and `..` directory references will be resolved in
20
     * the returned path.
21
     *
22
     * Note that if the normalized path is not an absolute path, the resulting
23
     * path may begin with `..` directory references if it is not possible to
24
     * resolve them simply by using string handling. You should also note that
25
     * if the resulting path would result in an empty string, this method will
26
     * return `.` instead.
27
     *
28
     * If the `$prependDrive` option is enabled, the normalized path will be
29
     * prepended with the drive name on Windows platforms using the current
30
     * working directory, if the path is an absolute path that does not include
31
     * a drive name.
32
     *
33
     * @param string $path File system path to normalize
34
     * @param bool $prependDrive True to prepend drive name to absolute paths
35
     * @return string The normalizes file system path
36
     */
37 12
    public static function normalize($path, $prependDrive = true)
38
    {
39 12
        $path = self::join((string) $path);
40
41 12
        if ($prependDrive && $path[0] === DIRECTORY_SEPARATOR) {
42 3
            return strstr(getcwd(), DIRECTORY_SEPARATOR, true) . $path;
43
        }
44
45 9
        return $path;
46
    }
47
48
    /**
49
     * Joins the provided file systems paths together and normalizes the result.
50
     *
51
     * The paths can be provided either as multiple arguments to this method
52
     * or as an array. The paths will be joined using the system directory
53
     * separator and the result will be normalized similar to the normalization
54
     * method (the drive letter will not be prepended however).
55
     *
56
     * Note that unless the first path in the list is an absolute path, the
57
     * entire resulting path will be treated as a relative path.
58
     *
59
     * @param string[]|string $paths File system paths to join
60
     * @return string The joined file system paths
61
     */
62 45
    public static function join($paths)
63
    {
64 45
        $arguments = is_array($paths) ? $paths : func_get_args();
65 45
        $joins = [];
66
67 45
        foreach ($arguments as $path) {
68 42
            $joins[] = (string) $path;
69 15
        }
70
71 45
        $parts = self::getParts($joins);
72 42
        $absolute = self::isAbsolute($joins[0]);
73 42
        $root = $absolute ? array_shift($parts) . DIRECTORY_SEPARATOR : '';
74 42
        $parts = self::resolve($parts, $absolute);
75
76 39
        return self::buildPath($root, $parts);
77
    }
78
79
    /**
80
     * Builds the final path from the root and path parts.
81
     * @param string $root Root path
82
     * @param string[] $parts The path parts
83
     * @return string The final built path
84
     */
85 39
    private static function buildPath($root, array $parts)
86
    {
87 39
        if ($parts === []) {
88 15
            return $root === '' ? '.' : $root;
89
        }
90
91 33
        return $root . implode(DIRECTORY_SEPARATOR, $parts);
92
    }
93
94
    /**
95
     * Merges the paths and returns the individual parts.
96
     * @param string[] $paths Array of paths
97
     * @return string[] Parts in the paths merged into a single array
98
     * @throws \InvalidArgumentException If no paths have been provided
99
     */
100 45
    private static function getParts(array $paths)
101
    {
102 45
        if ($paths === []) {
103 3
            throw new \InvalidArgumentException('You must provide at least one path');
104
        }
105
106 42
        return preg_split('# *[/\\\\]+ *#', trim(implode('/', $paths), ' '));
107
    }
108
109
    /**
110
     * Tells if the path is an absolute path.
111
     * @param string $path The file system path to test
112
     * @return bool True if the path is an absolute path, false if not
113
     */
114 42
    private static function isAbsolute($path)
115
    {
116 42
        $path = trim($path);
117
118 42
        if ($path === '') {
119 6
            return false;
120
        }
121
122 39
        $length = strcspn($path, '/\\');
123
124 39
        return $length === 0 || $path[$length - 1] === ':';
125
    }
126
127
    /**
128
     * Resolves parent directory references and removes redundant entries.
129
     * @param string[] $parts List of parts in the the path
130
     * @param bool $absolute Whether the path is an absolute path or not
131
     * @return string[] Resolved list of parts in the path
132
     */
133 42
    private static function resolve(array $parts, $absolute)
134
    {
135 42
        $resolved = [];
136
137 42
        foreach ($parts as $path) {
138 42
            if ($path === '..') {
139 12
                self::resolveParent($resolved, $absolute);
140 42
            } elseif (self::isValidPath($path)) {
141 38
                $resolved[] = $path;
142 12
            }
143 14
        }
144
145 39
        return $resolved;
146
    }
147
148
    /**
149
     * Tells if the part of the path is valid and not empty.
150
     * @param string $path Part of the path to check for redundancy
151
     * @return bool True if the path is valid and not empty, false if not
152
     * @throws \InvalidArgumentException If the path contains invalid characters
153
     */
154 42
    private static function isValidPath($path)
155
    {
156 42
        if (strpos($path, ':') !== false) {
157 3
            throw new \InvalidArgumentException('Invalid path character ":"');
158
        }
159
160 42
        return $path !== '' && $path !== '.';
161
    }
162
163
    /**
164
     * Resolves the relative parent directory for the path.
165
     * @param string[] $parts Path parts to modify
166
     * @param bool $absolute True if dealing with absolute path, false if not
167
     */
168 12
    private static function resolveParent(& $parts, $absolute)
169
    {
170 12
        if ($absolute || ($parts && $parts[count($parts) - 1] !== '..')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
171 12
            array_pop($parts);
172 12
            return;
173
        }
174
175 3
        $parts[] = '..';
176 3
    }
177
}
178