Path::normalize()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 2
nop 2
crap 3
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
        $joins = array_map('strval', is_array($paths) ? $paths : func_get_args());
65 45
        $parts = self::getParts($joins);
66 42
        $absolute = self::isAbsolute($joins[0]);
67 42
        $root = $absolute ? array_shift($parts) . DIRECTORY_SEPARATOR : '';
68 42
        $parts = self::resolve($parts, $absolute);
69
70 39
        return self::buildPath($root, $parts);
71
    }
72
73
    /**
74
     * Builds the final path from the root and path parts.
75
     * @param string $root Root path
76
     * @param string[] $parts The path parts
77
     * @return string The final built path
78
     */
79 39
    private static function buildPath($root, array $parts)
80
    {
81 39
        if ($parts === []) {
82 15
            return $root === '' ? '.' : $root;
83
        }
84
85 33
        return $root . implode(DIRECTORY_SEPARATOR, $parts);
86
    }
87
88
    /**
89
     * Merges the paths and returns the individual parts.
90
     * @param string[] $paths Array of paths
91
     * @return string[] Parts in the paths merged into a single array
92
     * @throws \InvalidArgumentException If no paths have been provided
93
     */
94 45
    private static function getParts(array $paths)
95
    {
96 45
        if ($paths === []) {
97 3
            throw new \InvalidArgumentException('You must provide at least one path');
98
        }
99
100 42
        return preg_split('# *[/\\\\]+ *#', trim(implode('/', $paths), ' '));
101
    }
102
103
    /**
104
     * Tells if the path is an absolute path.
105
     * @param string $path The file system path to test
106
     * @return bool True if the path is an absolute path, false if not
107
     */
108 42
    private static function isAbsolute($path)
109
    {
110 42
        $path = trim($path);
111
112 42
        if ($path === '') {
113 6
            return false;
114
        }
115
116 39
        $length = strcspn($path, '/\\');
117
118 39
        return $length === 0 || $path[$length - 1] === ':';
119
    }
120
121
    /**
122
     * Resolves parent directory references and removes redundant entries.
123
     * @param string[] $parts List of parts in the the path
124
     * @param bool $absolute Whether the path is an absolute path or not
125
     * @return string[] Resolved list of parts in the path
126
     */
127 42
    private static function resolve(array $parts, $absolute)
128
    {
129 42
        $resolved = [];
130
131 42
        foreach ($parts as $path) {
132 42
            if ($path === '..') {
133 12
                self::resolveParent($resolved, $absolute);
134 42
            } elseif (self::isValidPath($path)) {
135 40
                $resolved[] = $path;
136 12
            }
137 14
        }
138
139 39
        return $resolved;
140
    }
141
142
    /**
143
     * Tells if the part of the path is valid and not empty.
144
     * @param string $path Part of the path to check for redundancy
145
     * @return bool True if the path is valid and not empty, false if not
146
     * @throws \InvalidArgumentException If the path contains invalid characters
147
     */
148 42
    private static function isValidPath($path)
149
    {
150 42
        if (strpos($path, ':') !== false) {
151 3
            throw new \InvalidArgumentException('Invalid path character ":"');
152
        }
153
154 42
        return $path !== '' && $path !== '.';
155
    }
156
157
    /**
158
     * Resolves the relative parent directory for the path.
159
     * @param string[] $parts Path parts to modify
160
     * @param bool $absolute True if dealing with absolute path, false if not
161
     */
162 12
    private static function resolveParent(array & $parts, $absolute)
163
    {
164 12
        $count = count($parts);
165
166 12
        if ($absolute || ($count > 0 && $parts[$count - 1] !== '..')) {
167 12
            array_pop($parts);
168 12
            return;
169
        }
170
171 3
        $parts[] = '..';
172 3
    }
173
}
174