Completed
Pull Request — master (#29)
by Robbert
64:19
created

path   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 89.72%

Importance

Changes 15
Bugs 4 Features 2
Metric Value
wmc 34
c 15
b 4
f 2
lcom 1
cbo 0
dl 0
loc 313
ccs 96
cts 107
cp 0.8972
rs 9.2

13 Methods

Rating   Name   Duplication   Size   Complexity  
A parents() 0 18 2
C collapse() 0 36 8
A clean() 0 18 4
A parent() 0 17 4
A head() 0 8 2
A tail() 0 8 2
A diff() 0 16 2
A isChild() 0 4 1
A isAbsolute() 0 4 1
A getSplitPath() 0 4 1
A map() 0 11 2
A reduce() 0 4 1
A search() 0 13 4
1
<?php
2
3
/*
4
 * This file is part of the Ariadne Component Library.
5
 *
6
 * (c) Muze <[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 arc;
13
14
/**
15
 *	Utility methods to handle common path related tasks, cleaning, changing relative to absolute, etc.
16
 */
17
class path
18
{
19
    protected static $collapseCache = array();
20
21
    /**
22
     *	This method returns all the parent paths for a given path, starting at the root and including the
23
     *	given path itself.
24
     *
25
     *	Usage:
26
     *		\arc\path::parents( '/foo/bar/doh/', '/foo/' ); // => [ '/foo/', '/foo/bar/', '/foo/bar/doh/' ]
27
     *
28
     *	@param string $path The path to derive all parent paths from.
29
     *	@param string $root The root or topmost parent to return. Defaults to '/'.
30
     *	@return array Array of all parent paths, starting at the root and ending with the given path.
31
     *		Note: It includes the given path!
32
     */
33 10
    public static function parents($path, $root = '/')
34
    {
35
        // remove root
36 10
        $parents = [];
37
        if(strpos( $path, $root ) === 0 ) {
38 10
            $subpath = substr($path, strlen($root));
39 10
            // returns all parents starting at the root, up to and including the path itself
40
            $prevpath = '';
41 10
            $parents = self::reduce( $subpath, function ($result, $entry) use ($root, &$prevpath) {
42 5
                $prevpath .= $entry . '/';
43
                $result[] = $root . $prevpath;
44 10
45 10
                return $result;
46
            }, [ $root ] );
47 10
        }
48
49
        return $parents;
50
    }
51
52
    /**
53
     *	This method parses a path which may contain '..' or '.' or '//' entries and returns the resulting
54
     *	absolute path.
55
     *
56
     *	Usage:
57
     *		\arc\path::collapse( '../', '/foo/bar/' ); // => '/foo/'
58
     *		\arc\path::collapse( '\\foo\\.\\bar/doh/../' ); // => '/foo/bar/'
59
     *
60
     *	@param string $path The input path, which may be relative. If this path starts with a '/' it is
61
     *		considered to start in the root.
62
     *	@param string $cwd The current working directory. For relative paths this is the starting point.
63 10
     *	@return string The absolute path, without '..', '.' or '//' entries.
64
     */
65
    public static function collapse($path, $cwd = '/')
66 10
    {
67 2
        // removes '.', changes '//' to '/', changes '\\' to '/', calculates '..' up to '/'
68
        if (!isset($path[0])) {
69 10
            return $cwd;
70 2
        }
71 1
        if ($path[0] !== '/' && $path[0] !== '\\') {
72 10
            $path = $cwd . '/' . $path;
73
        }
74 6
        if (isset( self::$collapseCache[$path] )) { // cache hit - so return that
75
            return self::$collapseCache[$path];
76 10
        }
77 10
78 5
        $tempPath = str_replace('\\', '/', (string) $path);
79
        $collapsedPath = self::reduce(
80
            $tempPath,
81 10
            function ($result, $entry) {
82 2
                if ($entry == '..' ) {
83 2
                    $result = dirname( $result );
84 2
                    if (isset($result[1])) { // fast check to see if there is a dirname
85 1
                        $result .= '/';
86 2
                    } else {
87 2
                        $result = '/';
88 10
                    }
89
                } else if ($entry !== '.') {
90 5
                    $result .= $entry .'/';
91 10
                }
92 10
                return $result;
93 5
            },
94
            '/' // initial value, always start paths with a '/'
95 10
        );
96 10
        // store the collapsed path in the cache, improves performance by factor > 10.
97 5
        self::$collapseCache[$path] = $collapsedPath;
98 5
99
        return $collapsedPath;
100 10
    }
101
102 10
    /**
103
     *	This method cleans the input path with the given filter method. You can specify any of the
104
     *	sanitize methods valid for filter_var or you can use your own callback function. By default
105
     *	it url encodes each filename in the path.
106
     *
107
     *	Usage:
108
     *		\arc\path::clean( '/a path/to somewhere/' ); // => '/a%20path/to%20somewhere/'
109
     *
110
     *	@param string $path The path to clean.
111
     *	@param mixed $filter Either one of the sanitize filters for filter_var or a callback method as
112
     *		in \arc\path::map
113
     *	@param mixed $flags Optional list of flags for the sanitize filter.
114
     *	@return string The cleaned path.
115
     */
116
    public static function clean($path, $filter = null, $flags = null)
117
    {
118
        if (is_callable( $filter )) {
119 2
            $callback = $filter;
120
        } else {
121 2
            if (!isset( $filter )) {
122 2
                 $filter = FILTER_SANITIZE_ENCODED;
123 1
            }
124 2
            if (!isset($flags)) {
125 2
                $flags = FILTER_FLAG_ENCODE_LOW | FILTER_FLAG_ENCODE_HIGH;
126 1
            }
127 2
            $callback = function ($entry) use ($filter, $flags) {
128 2
                return filter_var( $entry, $filter, $flags);
129 1
            };
130
        }
131 2
132 2
        return self::map( $path, $callback );
133
    }
134
135 2
    /**
136
     *	Returns either the immediate parent path for the given path, or null if it is outside the
137
     *	root path. Differs with dirname() in that it will not return '/' as a parent of '/', but
138
     *	null instead.
139
         *
140
     *	Usage:
141
     *		\arc\path::parent( '/foo/bar/' ); // => '/foo/'
142
     *
143
     *	@param string $path The path from which to get the parent path.
144
     *	@param string $root Optional root path, defaults to '/'
145
     *	@return string|null The parent of the given path or null if the parent is outside the root path.
146
     */
147
    public static function parent($path, $root = '/')
148
    {
149
        if ($path == $root) {
150 4
            return null;
151
        }
152 4
        $parent = dirname( $path );
153 2
        if (isset($parent[1])) { // fast check to see if there is a dirname
154
            $parent .= '/';
155 4
        }
156 4
        $parent[0] = '/'; // dirname('/something/') returns '\' in windows.
157 4
        if (strpos( $parent, $root ) !== 0) { // parent is outside of the root
158 2
159 4
            return null;
160 4
        }
161
162 2
        return $parent;
163
    }
164
165 4
166
    /**
167
     *  Returns the root entry of the given path.
168
     *
169
     *  Usage:
170
     *    $rootEntry = \arc\path::head( '/root/of/a/path/' ); // => 'root'
171
     *
172
     *  @param string $path The path to get the root entry of.
173
     *  @return string The root entry of the given path, without slashes.
174
     */
175
    public static function head($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
    {
177
        if (!\arc\path::isAbsolute($path)) {
178
            $path = '/' . $path;
179
        }
180
181
        return substr( $path, 1, strpos( $path, '/', 1) - 1 );
182
    }
183
184
    /**
185
     *  Returns the path without its root entry.
186
     *
187
     *  Usage:
188
     *    $remainder = \arc\path::tail( '/root/of/a/path/' ); // => '/of/a/path/'
189
     *
190
     *  @param string $path The path to get the tail of.
191
     *  @return string The path without its root entry.
192
     */
193
    public static function tail($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
194
    {
195
        if (!\arc\path::isAbsolute($path)) {
196
            $path = '/' . $path;
197
        }
198
199
        return substr( $path, strpos( $path, '/', 1) );
200
    }
201
202
    /**
203
     *  Returns the difference between sourcePath and targetPath as a relative path in
204
     *  such a way that if you append the relative path to the source path and collapse
205
     *  that, the result is the targetPath.
206
     *  @param string $targetPath The target path to map to.
207
     *  @param string $sourcePath The source path to start with.
208
     *  @return string The relative path from source to target.
209
     */
210
    public static function diff($sourcePath, $targetPath)
211
    {
212
        $diff = '';
213 6
        $targetPath = \arc\path::collapse( $targetPath );
214
        $sourcePath = \arc\path::collapse( $sourcePath );
215 6
        $commonParent = \arc\path::search( $sourcePath, function ($path) use ($targetPath, &$diff) {
216 6
            if (!\arc\path::isChild( $targetPath, $path )) {
217 6
                $diff .= '../';
218
            } else {
219 6
                return $path;
220 6
            }
221 3
        }, false);
222 6
        $diff .= substr( $targetPath, strlen( $commonParent ) );
223
224 6
        return $diff;
225 6
    }
226
227 6
    /**
228
     *  Returns true if the path is a child or descendant of the parent.
229
     *  @param string $path The path to check
230
     *  @param string $parent The parent to check.
231
     *  @return bool True if path is a child or descendant of parent
232
     */
233
    public static function isChild($path, $parent)
234
    {
235
        return ( strpos( $path, $parent ) === 0 );
236 8
    }
237
238 8
    /**
239
     *  Returns true if the given path starts with a '/'.
240
     * @param  string $path The path to check
241
     * @return bool   True is the path starts with a '/'
242
     */
243
    public static function isAbsolute($path)
244
    {
245
        return $path[0] === '/';
246 8
    }
247
248 8
    protected static function getSplitPath($path)
249
    {
250
        return preg_split('|/|', $path, -1, PREG_SPLIT_NO_EMPTY);
251
    }
252
253 30
    /**
254 30
     *	Applies a callback function to each filename in a path. The result will be the new filename.
255 30
     *
256
     *	Usage:
257
     *		/arc/path::map( '/foo>bar/', function ($filename) {
258
     *			return htmlentities( $filename, ENT_QUOTES );
259
     *		} ); // => '/foo&gt;bar/'
260
     *
261
     *	@param string $path The path to alter.
262
     *	@param Callable $callback
263
     *	@return string A path with all filenames changed as by the callback method.
264
     */
265
    public static function map($path, $callback)
266
    {
267
        $splitPath = self::getSplitPath( $path );
268
        if (count($splitPath)) {
269
            $result = array_map( $callback, $splitPath );
270 4
271
            return '/' . join( $result, '/' ) .'/';
272 4
        } else {
273 4
            return '/';
274 4
        }
275
    }
276 4
277
    /**
278 2
     *	Applies a callback function to each filename in a path, but the result of the callback is fed back
279
     *	to the next call to the callback method as the first argument.
280
     *
281
     *	Usage:
282
     *		/arc/path::reduce( '/foo/bar/', function ($previousResult, $filename) {
283
     *			return $previousResult . $filename . '\\';
284
     *		}, '\\' ); // => '\\foo\\bar\\'
285
     *
286
     *	@param string $path The path to reduce.
287
     *	@param Callable $callback The method to apply to each filename of the path
288
     *	@param mixed $initial Optional. The initial reduced value to start the callback with.
289
     *	@return mixed The final result of the callback method
290
     */
291
    public static function reduce($path, $callback, $initial = null)
292
    {
293
        return array_reduce( self::getSplitPath( $path ), $callback, $initial );
294
    }
295
296 28
    /**
297
     *	Applies a callback function to each parent of a path, in order. Starting at the root by default,
298 28
     *	but optionally in reverse order. Will continue while the callback method returns null, otherwise
299
     *	returns the result of the callback method.
300
     *
301
     *	Usage:
302
     *		$result = \arc\path::search( '/foo/bar/', function ($parent) {
303
     *			if ($parent == '/foo/') { // silly test
304
     *				return true;
305
     *			}
306
     *		});
307
     *
308
     *	@param string $path Each parent of this path will be passed to the callback method.
309
     *	@param Callable $callback The method to apply to each parent
310
     *	@param bool $startAtRoot Optional. If set to false, root will be called last, otherwise first.
311
     *		Defaults to true.
312
     *	@param string $root Optional. Specify another root, no parents above the root will be called.
313
     *		Defaults to '/'.
314
     *	@return mixed The first non-null result of the callback method
315
     */
316
    public static function search($path, $callback, $startAtRoot = true, $root = '/')
317
    {
318
        $parents = self::parents( $path, $root );
319
        if (!$startAtRoot) {
320
            $parents = array_reverse( $parents );
321 8
        }
322
        foreach ($parents as $parent) {
323 8
            $result = call_user_func( $callback, $parent );
324 8
            if (isset( $result )) {
325 8
                return $result;
326 4
            }
327 8
        }
328 8
    }
329
}
330