1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the webmozart/path-util package. |
5
|
|
|
* |
6
|
|
|
* (c) Bernhard Schussek <[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 Webmozart\PathUtil; |
13
|
|
|
|
14
|
|
|
use InvalidArgumentException; |
15
|
|
|
use RuntimeException; |
16
|
|
|
use Webmozart\Assert\Assert; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Contains utility methods for handling path strings. |
20
|
|
|
* |
21
|
|
|
* The methods in this class are able to deal with both UNIX and Windows paths |
22
|
|
|
* with both forward and backward slashes. All methods return normalized parts |
23
|
|
|
* containing only forward slashes and no excess "." and ".." segments. |
24
|
|
|
* |
25
|
|
|
* @since 1.0 |
26
|
|
|
* |
27
|
|
|
* @author Bernhard Schussek <[email protected]> |
28
|
|
|
* @author Thomas Schulz <[email protected]> |
29
|
|
|
*/ |
30
|
|
|
final class Path |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* The number of buffer entries that triggers a cleanup operation. |
34
|
|
|
*/ |
35
|
|
|
const CLEANUP_THRESHOLD = 1250; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The buffer size after the cleanup operation. |
39
|
|
|
*/ |
40
|
|
|
const CLEANUP_SIZE = 1000; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Buffers input/output of {@link canonicalize()}. |
44
|
|
|
* |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
private static $buffer = array(); |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The size of the buffer. |
51
|
|
|
* |
52
|
|
|
* @var int |
53
|
|
|
*/ |
54
|
|
|
private static $bufferSize = 0; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Canonicalizes the given path. |
58
|
|
|
* |
59
|
|
|
* During normalization, all slashes are replaced by forward slashes ("/"). |
60
|
|
|
* Furthermore, all "." and ".." segments are removed as far as possible. |
61
|
|
|
* ".." segments at the beginning of relative paths are not removed. |
62
|
|
|
* |
63
|
|
|
* ```php |
64
|
|
|
* echo Path::canonicalize("\webmozart\puli\..\css\style.css"); |
65
|
|
|
* // => /webmozart/css/style.css |
66
|
|
|
* |
67
|
|
|
* echo Path::canonicalize("../css/./style.css"); |
68
|
|
|
* // => ../css/style.css |
69
|
|
|
* ``` |
70
|
|
|
* |
71
|
|
|
* This method is able to deal with both UNIX and Windows paths. |
72
|
|
|
* |
73
|
|
|
* @param string $path A path string. |
74
|
|
|
* |
75
|
|
|
* @return string The canonical path. |
76
|
|
|
* |
77
|
|
|
* @since 1.0 Added method. |
78
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
79
|
|
|
* @since 2.1 Added support for `~`. |
80
|
|
|
*/ |
81
|
511 |
|
public static function canonicalize($path) |
82
|
|
|
{ |
83
|
511 |
|
if ('' === $path) { |
84
|
4 |
|
return ''; |
85
|
|
|
} |
86
|
|
|
|
87
|
510 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
88
|
|
|
|
89
|
|
|
// This method is called by many other methods in this class. Buffer |
90
|
|
|
// the canonicalized paths to make up for the severe performance |
91
|
|
|
// decrease. |
92
|
507 |
|
if (isset(self::$buffer[$path])) { |
93
|
243 |
|
return self::$buffer[$path]; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
// Replace "~" with user's home directory. |
97
|
343 |
|
if ('~' === $path[0]) { |
98
|
10 |
|
$path = static::getHomeDirectory().substr($path, 1); |
99
|
|
|
} |
100
|
|
|
|
101
|
343 |
|
$path = str_replace('\\', '/', $path); |
102
|
|
|
|
103
|
343 |
|
list($root, $pathWithoutRoot) = self::split($path); |
104
|
|
|
|
105
|
343 |
|
$parts = explode('/', $pathWithoutRoot); |
106
|
343 |
|
$canonicalParts = array(); |
107
|
|
|
|
108
|
|
|
// Collapse "." and "..", if possible |
|
|
|
|
109
|
343 |
|
foreach ($parts as $part) { |
110
|
343 |
|
if ('.' === $part || '' === $part) { |
111
|
134 |
|
continue; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
// Collapse ".." with the previous part, if one exists |
115
|
|
|
// Don't collapse ".." if the previous part is also ".." |
116
|
334 |
|
if ('..' === $part && count($canonicalParts) > 0 |
117
|
334 |
|
&& '..' !== $canonicalParts[count($canonicalParts) - 1]) { |
118
|
72 |
|
array_pop($canonicalParts); |
119
|
|
|
|
120
|
72 |
|
continue; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
// Only add ".." prefixes for relative paths |
124
|
334 |
|
if ('..' !== $part || '' === $root) { |
125
|
334 |
|
$canonicalParts[] = $part; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
// Add the root directory again |
130
|
343 |
|
self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts); |
131
|
343 |
|
++self::$bufferSize; |
132
|
|
|
|
133
|
|
|
// Clean up regularly to prevent memory leaks |
134
|
343 |
|
if (self::$bufferSize > self::CLEANUP_THRESHOLD) { |
135
|
|
|
self::$buffer = array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true); |
136
|
|
|
self::$bufferSize = self::CLEANUP_SIZE; |
137
|
|
|
} |
138
|
|
|
|
139
|
343 |
|
return $canonicalPath; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Normalizes the given path. |
144
|
|
|
* |
145
|
|
|
* During normalization, all slashes are replaced by forward slashes ("/"). |
146
|
|
|
* Contrary to {@link canonicalize()}, this method does not remove invalid |
147
|
|
|
* or dot path segments. Consequently, it is much more efficient and should |
148
|
|
|
* be used whenever the given path is known to be a valid, absolute system |
149
|
|
|
* path. |
150
|
|
|
* |
151
|
|
|
* This method is able to deal with both UNIX and Windows paths. |
152
|
|
|
* |
153
|
|
|
* @param string $path A path string. |
154
|
|
|
* |
155
|
|
|
* @return string The normalized path. |
156
|
|
|
* |
157
|
|
|
* @since 2.2 Added method. |
158
|
|
|
*/ |
159
|
2 |
|
public static function normalize($path) |
160
|
|
|
{ |
161
|
2 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
162
|
|
|
|
163
|
1 |
|
return str_replace('\\', '/', $path); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Returns the directory part of the path. |
168
|
|
|
* |
169
|
|
|
* This method is similar to PHP's dirname(), but handles various cases |
170
|
|
|
* where dirname() returns a weird result: |
171
|
|
|
* |
172
|
|
|
* - dirname() does not accept backslashes on UNIX |
173
|
|
|
* - dirname("C:/webmozart") returns "C:", not "C:/" |
174
|
|
|
* - dirname("C:/") returns ".", not "C:/" |
175
|
|
|
* - dirname("C:") returns ".", not "C:/" |
176
|
|
|
* - dirname("webmozart") returns ".", not "" |
177
|
|
|
* - dirname() does not canonicalize the result |
178
|
|
|
* |
179
|
|
|
* This method fixes these shortcomings and behaves like dirname() |
180
|
|
|
* otherwise. |
181
|
|
|
* |
182
|
|
|
* The result is a canonical path. |
183
|
|
|
* |
184
|
|
|
* @param string $path A path string. |
185
|
|
|
* |
186
|
|
|
* @return string The canonical directory part. Returns the root directory |
187
|
|
|
* if the root directory is passed. Returns an empty string |
188
|
|
|
* if a relative path is passed that contains no slashes. |
189
|
|
|
* Returns an empty string if an empty string is passed. |
190
|
|
|
* |
191
|
|
|
* @since 1.0 Added method. |
192
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
193
|
|
|
*/ |
194
|
41 |
|
public static function getDirectory($path) |
195
|
|
|
{ |
196
|
41 |
|
if ('' === $path) { |
197
|
1 |
|
return ''; |
198
|
|
|
} |
199
|
|
|
|
200
|
40 |
|
$path = static::canonicalize($path); |
201
|
|
|
|
202
|
|
|
// Maintain scheme |
203
|
39 |
View Code Duplication |
if (false !== ($pos = strpos($path, '://'))) { |
204
|
8 |
|
$scheme = substr($path, 0, $pos + 3); |
205
|
8 |
|
$path = substr($path, $pos + 3); |
206
|
|
|
} else { |
207
|
31 |
|
$scheme = ''; |
208
|
|
|
} |
209
|
|
|
|
210
|
39 |
|
if (false !== ($pos = strrpos($path, '/'))) { |
211
|
|
|
// Directory equals root directory "/" |
212
|
36 |
|
if (0 === $pos) { |
213
|
8 |
|
return $scheme.'/'; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
// Directory equals Windows root "C:/" |
217
|
28 |
|
if (2 === $pos && ctype_alpha($path[0]) && ':' === $path[1]) { |
218
|
7 |
|
return $scheme.substr($path, 0, 3); |
219
|
|
|
} |
220
|
|
|
|
221
|
21 |
|
return $scheme.substr($path, 0, $pos); |
222
|
|
|
} |
223
|
|
|
|
224
|
3 |
|
return ''; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Returns canonical path of the user's home directory. |
229
|
|
|
* |
230
|
|
|
* Supported operating systems: |
231
|
|
|
* |
232
|
|
|
* - UNIX |
233
|
|
|
* - Windows8 and upper |
234
|
|
|
* |
235
|
|
|
* If your operation system or environment isn't supported, an exception is thrown. |
236
|
|
|
* |
237
|
|
|
* The result is a canonical path. |
238
|
|
|
* |
239
|
|
|
* @return string The canonical home directory |
240
|
|
|
* |
241
|
|
|
* @throws RuntimeException If your operation system or environment isn't supported |
242
|
|
|
* |
243
|
|
|
* @since 2.1 Added method. |
244
|
|
|
*/ |
245
|
13 |
|
public static function getHomeDirectory() |
246
|
|
|
{ |
247
|
|
|
// For UNIX support |
248
|
13 |
|
if (getenv('HOME')) { |
249
|
11 |
|
return static::canonicalize(getenv('HOME')); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
// For >= Windows8 support |
253
|
2 |
|
if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) { |
254
|
1 |
|
return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH')); |
255
|
|
|
} |
256
|
|
|
|
257
|
1 |
|
throw new RuntimeException("Your environment or operation system isn't supported"); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Returns the root directory of a path. |
262
|
|
|
* |
263
|
|
|
* The result is a canonical path. |
264
|
|
|
* |
265
|
|
|
* @param string $path A path string. |
266
|
|
|
* |
267
|
|
|
* @return string The canonical root directory. Returns an empty string if |
268
|
|
|
* the given path is relative or empty. |
269
|
|
|
* |
270
|
|
|
* @since 1.0 Added method. |
271
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
272
|
|
|
*/ |
273
|
18 |
|
public static function getRoot($path) |
274
|
|
|
{ |
275
|
18 |
|
if ('' === $path) { |
276
|
1 |
|
return ''; |
277
|
|
|
} |
278
|
|
|
|
279
|
17 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
280
|
|
|
|
281
|
|
|
// Maintain scheme |
282
|
16 |
View Code Duplication |
if (false !== ($pos = strpos($path, '://'))) { |
283
|
5 |
|
$scheme = substr($path, 0, $pos + 3); |
284
|
5 |
|
$path = substr($path, $pos + 3); |
285
|
|
|
} else { |
286
|
11 |
|
$scheme = ''; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
// UNIX root "/" or "\" (Windows style) |
290
|
16 |
|
if ('/' === $path[0] || '\\' === $path[0]) { |
291
|
6 |
|
return $scheme.'/'; |
292
|
|
|
} |
293
|
|
|
|
294
|
10 |
|
$length = strlen($path); |
295
|
|
|
|
296
|
|
|
// Windows root |
297
|
10 |
|
if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { |
298
|
|
|
// Special case: "C:" |
|
|
|
|
299
|
8 |
|
if (2 === $length) { |
300
|
2 |
|
return $scheme.$path.'/'; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
// Normal case: "C:/ or "C:\" |
304
|
6 |
|
if ('/' === $path[2] || '\\' === $path[2]) { |
305
|
6 |
|
return $scheme.$path[0].$path[1].'/'; |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
2 |
|
return ''; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Returns the file name from a file path. |
314
|
|
|
* |
315
|
|
|
* @param string $path The path string. |
316
|
|
|
* |
317
|
|
|
* @return string The file name. |
318
|
|
|
* |
319
|
|
|
* @since 1.1 Added method. |
320
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
321
|
|
|
*/ |
322
|
8 |
|
public static function getFilename($path) |
323
|
|
|
{ |
324
|
8 |
|
if ('' === $path) { |
325
|
1 |
|
return ''; |
326
|
|
|
} |
327
|
|
|
|
328
|
7 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
329
|
|
|
|
330
|
6 |
|
return basename($path); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Returns the file name without the extension from a file path. |
335
|
|
|
* |
336
|
|
|
* @param string $path The path string. |
337
|
|
|
* @param string|null $extension If specified, only that extension is cut |
338
|
|
|
* off (may contain leading dot). |
339
|
|
|
* |
340
|
|
|
* @return string The file name without extension. |
341
|
|
|
* |
342
|
|
|
* @since 1.1 Added method. |
343
|
|
|
* @since 2.0 Method now fails if $path or $extension have invalid types. |
344
|
|
|
*/ |
345
|
20 |
|
public static function getFilenameWithoutExtension($path, $extension = null) |
346
|
|
|
{ |
347
|
20 |
|
if ('' === $path) { |
348
|
1 |
|
return ''; |
349
|
|
|
} |
350
|
|
|
|
351
|
19 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
352
|
18 |
|
Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s'); |
353
|
|
|
|
354
|
17 |
|
if (null !== $extension) { |
355
|
|
|
// remove extension and trailing dot |
356
|
10 |
|
return rtrim(basename($path, $extension), '.'); |
357
|
|
|
} |
358
|
|
|
|
359
|
7 |
|
return pathinfo($path, PATHINFO_FILENAME); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Returns the extension from a file path. |
364
|
|
|
* |
365
|
|
|
* @param string $path The path string. |
366
|
|
|
* @param bool $forceLowerCase Forces the extension to be lower-case |
367
|
|
|
* (requires mbstring extension for correct |
368
|
|
|
* multi-byte character handling in extension). |
369
|
|
|
* |
370
|
|
|
* @return string The extension of the file path (without leading dot). |
371
|
|
|
* |
372
|
|
|
* @since 1.1 Added method. |
373
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
374
|
|
|
*/ |
375
|
51 |
|
public static function getExtension($path, $forceLowerCase = false) |
376
|
|
|
{ |
377
|
51 |
|
if ('' === $path) { |
378
|
1 |
|
return ''; |
379
|
|
|
} |
380
|
|
|
|
381
|
50 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
382
|
|
|
|
383
|
47 |
|
$extension = pathinfo($path, PATHINFO_EXTENSION); |
384
|
|
|
|
385
|
47 |
|
if ($forceLowerCase) { |
386
|
6 |
|
$extension = self::toLower($extension); |
387
|
|
|
} |
388
|
|
|
|
389
|
47 |
|
return $extension; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Returns whether the path has an extension. |
394
|
|
|
* |
395
|
|
|
* @param string $path The path string. |
396
|
|
|
* @param string|array|null $extensions If null or not provided, checks if |
397
|
|
|
* an extension exists, otherwise |
398
|
|
|
* checks for the specified extension |
399
|
|
|
* or array of extensions (with or |
400
|
|
|
* without leading dot). |
401
|
|
|
* @param bool $ignoreCase Whether to ignore case-sensitivity |
402
|
|
|
* (requires mbstring extension for |
403
|
|
|
* correct multi-byte character |
404
|
|
|
* handling in the extension). |
405
|
|
|
* |
406
|
|
|
* @return bool Returns `true` if the path has an (or the specified) |
407
|
|
|
* extension and `false` otherwise. |
408
|
|
|
* |
409
|
|
|
* @since 1.1 Added method. |
410
|
|
|
* @since 2.0 Method now fails if $path or $extensions have invalid types. |
411
|
|
|
*/ |
412
|
30 |
|
public static function hasExtension($path, $extensions = null, $ignoreCase = false) |
413
|
|
|
{ |
414
|
30 |
|
if ('' === $path) { |
415
|
2 |
|
return false; |
416
|
|
|
} |
417
|
|
|
|
418
|
28 |
|
$extensions = is_object($extensions) ? array($extensions) : (array) $extensions; |
419
|
|
|
|
420
|
28 |
|
Assert::allString($extensions, 'The extensions must be strings. Got: %s'); |
421
|
|
|
|
422
|
27 |
|
$actualExtension = self::getExtension($path, $ignoreCase); |
423
|
|
|
|
424
|
|
|
// Only check if path has any extension |
425
|
26 |
|
if (empty($extensions)) { |
426
|
6 |
|
return '' !== $actualExtension; |
427
|
|
|
} |
428
|
|
|
|
429
|
20 |
|
foreach ($extensions as $key => $extension) { |
430
|
20 |
|
if ($ignoreCase) { |
431
|
4 |
|
$extension = self::toLower($extension); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
// remove leading '.' in extensions array |
435
|
20 |
|
$extensions[$key] = ltrim($extension, '.'); |
436
|
|
|
} |
437
|
|
|
|
438
|
20 |
|
return in_array($actualExtension, $extensions); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Changes the extension of a path string. |
443
|
|
|
* |
444
|
|
|
* @param string $path The path string with filename.ext to change. |
445
|
|
|
* @param string $extension New extension (with or without leading dot). |
446
|
|
|
* |
447
|
|
|
* @return string The path string with new file extension. |
448
|
|
|
* |
449
|
|
|
* @since 1.1 Added method. |
450
|
|
|
* @since 2.0 Method now fails if $path or $extension is not a string. |
451
|
|
|
*/ |
452
|
14 |
|
public static function changeExtension($path, $extension) |
453
|
|
|
{ |
454
|
14 |
|
if ('' === $path) { |
455
|
1 |
|
return ''; |
456
|
|
|
} |
457
|
|
|
|
458
|
13 |
|
Assert::string($extension, 'The extension must be a string. Got: %s'); |
459
|
|
|
|
460
|
12 |
|
$actualExtension = self::getExtension($path); |
461
|
11 |
|
$extension = ltrim($extension, '.'); |
462
|
|
|
|
463
|
|
|
// No extension for paths |
464
|
11 |
|
if ('/' === substr($path, -1)) { |
465
|
2 |
|
return $path; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
// No actual extension in path |
469
|
9 |
|
if (empty($actualExtension)) { |
470
|
3 |
|
return $path.('.' === substr($path, -1) ? '' : '.').$extension; |
471
|
|
|
} |
472
|
|
|
|
473
|
6 |
|
return substr($path, 0, -strlen($actualExtension)).$extension; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* Returns whether a path is absolute. |
478
|
|
|
* |
479
|
|
|
* @param string $path A path string. |
480
|
|
|
* |
481
|
|
|
* @return bool Returns true if the path is absolute, false if it is |
482
|
|
|
* relative or empty. |
483
|
|
|
* |
484
|
|
|
* @since 1.0 Added method. |
485
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
486
|
|
|
*/ |
487
|
111 |
|
public static function isAbsolute($path) |
488
|
|
|
{ |
489
|
111 |
|
if ('' === $path) { |
490
|
3 |
|
return false; |
491
|
|
|
} |
492
|
|
|
|
493
|
109 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
494
|
|
|
|
495
|
|
|
// Strip scheme |
496
|
107 |
|
if (false !== ($pos = strpos($path, '://'))) { |
497
|
22 |
|
$path = substr($path, $pos + 3); |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
// UNIX root "/" or "\" (Windows style) |
501
|
107 |
|
if ('/' === $path[0] || '\\' === $path[0]) { |
502
|
60 |
|
return true; |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
// Windows root |
506
|
89 |
|
if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { |
507
|
|
|
// Special case: "C:" |
|
|
|
|
508
|
50 |
|
if (2 === strlen($path)) { |
509
|
3 |
|
return true; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
// Normal case: "C:/ or "C:\" |
513
|
47 |
|
if ('/' === $path[2] || '\\' === $path[2]) { |
514
|
45 |
|
return true; |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
|
518
|
61 |
|
return false; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* Returns whether a path is relative. |
523
|
|
|
* |
524
|
|
|
* @param string $path A path string. |
525
|
|
|
* |
526
|
|
|
* @return bool Returns true if the path is relative or empty, false if |
527
|
|
|
* it is absolute. |
528
|
|
|
* |
529
|
|
|
* @since 1.0 Added method. |
530
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
531
|
|
|
*/ |
532
|
16 |
|
public static function isRelative($path) |
533
|
|
|
{ |
534
|
16 |
|
return !static::isAbsolute($path); |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* Turns a relative path into an absolute path. |
539
|
|
|
* |
540
|
|
|
* Usually, the relative path is appended to the given base path. Dot |
541
|
|
|
* segments ("." and "..") are removed/collapsed and all slashes turned |
542
|
|
|
* into forward slashes. |
543
|
|
|
* |
544
|
|
|
* ```php |
545
|
|
|
* echo Path::makeAbsolute("../style.css", "/webmozart/puli/css"); |
546
|
|
|
* // => /webmozart/puli/style.css |
547
|
|
|
* ``` |
548
|
|
|
* |
549
|
|
|
* If an absolute path is passed, that path is returned unless its root |
550
|
|
|
* directory is different than the one of the base path. In that case, an |
551
|
|
|
* exception is thrown. |
552
|
|
|
* |
553
|
|
|
* ```php |
554
|
|
|
* Path::makeAbsolute("/style.css", "/webmozart/puli/css"); |
555
|
|
|
* // => /style.css |
556
|
|
|
* |
557
|
|
|
* Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css"); |
558
|
|
|
* // => C:/style.css |
559
|
|
|
* |
560
|
|
|
* Path::makeAbsolute("C:/style.css", "/webmozart/puli/css"); |
561
|
|
|
* // InvalidArgumentException |
562
|
|
|
* ``` |
563
|
|
|
* |
564
|
|
|
* If the base path is not an absolute path, an exception is thrown. |
565
|
|
|
* |
566
|
|
|
* The result is a canonical path. |
567
|
|
|
* |
568
|
|
|
* @param string $path A path to make absolute. |
569
|
|
|
* @param string $basePath An absolute base path. |
570
|
|
|
* |
571
|
|
|
* @return string An absolute path in canonical form. |
572
|
|
|
* |
573
|
|
|
* @throws InvalidArgumentException If the base path is not absolute or if |
574
|
|
|
* the given path is an absolute path with |
575
|
|
|
* a different root than the base path. |
576
|
|
|
* |
577
|
|
|
* @since 1.0 Added method. |
578
|
|
|
* @since 2.0 Method now fails if $path or $basePath is not a string. |
579
|
|
|
* @since 2.2.2 Method does not fail anymore of $path and $basePath are |
580
|
|
|
* absolute, but on different partitions. |
581
|
|
|
*/ |
582
|
82 |
|
public static function makeAbsolute($path, $basePath) |
583
|
|
|
{ |
584
|
82 |
|
Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s'); |
585
|
|
|
|
586
|
79 |
|
if (!static::isAbsolute($basePath)) { |
587
|
1 |
|
throw new InvalidArgumentException(sprintf( |
588
|
1 |
|
'The base path "%s" is not an absolute path.', |
589
|
|
|
$basePath |
590
|
|
|
)); |
591
|
|
|
} |
592
|
|
|
|
593
|
78 |
|
if (static::isAbsolute($path)) { |
594
|
22 |
|
return static::canonicalize($path); |
595
|
|
|
} |
596
|
|
|
|
597
|
55 |
View Code Duplication |
if (false !== ($pos = strpos($basePath, '://'))) { |
598
|
12 |
|
$scheme = substr($basePath, 0, $pos + 3); |
599
|
12 |
|
$basePath = substr($basePath, $pos + 3); |
600
|
|
|
} else { |
601
|
43 |
|
$scheme = ''; |
602
|
|
|
} |
603
|
|
|
|
604
|
55 |
|
return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path); |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
/** |
608
|
|
|
* Turns a path into a relative path. |
609
|
|
|
* |
610
|
|
|
* The relative path is created relative to the given base path: |
611
|
|
|
* |
612
|
|
|
* ```php |
613
|
|
|
* echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli"); |
614
|
|
|
* // => ../style.css |
615
|
|
|
* ``` |
616
|
|
|
* |
617
|
|
|
* If a relative path is passed and the base path is absolute, the relative |
618
|
|
|
* path is returned unchanged: |
619
|
|
|
* |
620
|
|
|
* ```php |
621
|
|
|
* Path::makeRelative("style.css", "/webmozart/puli/css"); |
622
|
|
|
* // => style.css |
623
|
|
|
* ``` |
624
|
|
|
* |
625
|
|
|
* If both paths are relative, the relative path is created with the |
626
|
|
|
* assumption that both paths are relative to the same directory: |
627
|
|
|
* |
628
|
|
|
* ```php |
629
|
|
|
* Path::makeRelative("style.css", "webmozart/puli/css"); |
630
|
|
|
* // => ../../../style.css |
631
|
|
|
* ``` |
632
|
|
|
* |
633
|
|
|
* If both paths are absolute, their root directory must be the same, |
634
|
|
|
* otherwise an exception is thrown: |
635
|
|
|
* |
636
|
|
|
* ```php |
637
|
|
|
* Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli"); |
638
|
|
|
* // InvalidArgumentException |
639
|
|
|
* ``` |
640
|
|
|
* |
641
|
|
|
* If the passed path is absolute, but the base path is not, an exception |
642
|
|
|
* is thrown as well: |
643
|
|
|
* |
644
|
|
|
* ```php |
645
|
|
|
* Path::makeRelative("/webmozart/style.css", "webmozart/puli"); |
646
|
|
|
* // InvalidArgumentException |
647
|
|
|
* ``` |
648
|
|
|
* |
649
|
|
|
* If the base path is not an absolute path, an exception is thrown. |
650
|
|
|
* |
651
|
|
|
* The result is a canonical path. |
652
|
|
|
* |
653
|
|
|
* @param string $path A path to make relative. |
654
|
|
|
* @param string $basePath A base path. |
655
|
|
|
* |
656
|
|
|
* @return string A relative path in canonical form. |
657
|
|
|
* |
658
|
|
|
* @throws InvalidArgumentException If the base path is not absolute or if |
659
|
|
|
* the given path has a different root |
660
|
|
|
* than the base path. |
661
|
|
|
* |
662
|
|
|
* @since 1.0 Added method. |
663
|
|
|
* @since 2.0 Method now fails if $path or $basePath is not a string. |
664
|
|
|
*/ |
665
|
97 |
|
public static function makeRelative($path, $basePath) |
666
|
|
|
{ |
667
|
97 |
|
Assert::string($basePath, 'The base path must be a string. Got: %s'); |
668
|
|
|
|
669
|
95 |
|
$path = static::canonicalize($path); |
670
|
94 |
|
$basePath = static::canonicalize($basePath); |
671
|
|
|
|
672
|
94 |
|
list($root, $relativePath) = self::split($path); |
673
|
94 |
|
list($baseRoot, $relativeBasePath) = self::split($basePath); |
674
|
|
|
|
675
|
|
|
// If the base path is given as absolute path and the path is already |
676
|
|
|
// relative, consider it to be relative to the given absolute path |
677
|
|
|
// already |
678
|
94 |
|
if ('' === $root && '' !== $baseRoot) { |
679
|
|
|
// If base path is already in its root |
680
|
32 |
|
if ('' === $relativeBasePath) { |
681
|
30 |
|
$relativePath = ltrim($relativePath, './\\'); |
682
|
|
|
} |
683
|
|
|
|
684
|
32 |
|
return $relativePath; |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
// If the passed path is absolute, but the base path is not, we |
688
|
|
|
// cannot generate a relative path |
689
|
62 |
|
if ('' !== $root && '' === $baseRoot) { |
690
|
2 |
|
throw new InvalidArgumentException(sprintf( |
691
|
|
|
'The absolute path "%s" cannot be made relative to the '. |
692
|
|
|
'relative path "%s". You should provide an absolute base '. |
693
|
2 |
|
'path instead.', |
694
|
|
|
$path, |
695
|
|
|
$basePath |
696
|
|
|
)); |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
// Fail if the roots of the two paths are different |
700
|
60 |
|
if ($baseRoot && $root !== $baseRoot) { |
701
|
18 |
|
throw new InvalidArgumentException(sprintf( |
702
|
|
|
'The path "%s" cannot be made relative to "%s", because they '. |
703
|
18 |
|
'have different roots ("%s" and "%s").', |
704
|
|
|
$path, |
705
|
|
|
$basePath, |
706
|
|
|
$root, |
707
|
|
|
$baseRoot |
708
|
|
|
)); |
709
|
|
|
} |
710
|
|
|
|
711
|
42 |
|
if ('' === $relativeBasePath) { |
712
|
5 |
|
return $relativePath; |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
// Build a "../../" prefix with as many "../" parts as necessary |
716
|
37 |
|
$parts = explode('/', $relativePath); |
717
|
37 |
|
$baseParts = explode('/', $relativeBasePath); |
718
|
37 |
|
$dotDotPrefix = ''; |
719
|
|
|
|
720
|
|
|
// Once we found a non-matching part in the prefix, we need to add |
721
|
|
|
// "../" parts for all remaining parts |
722
|
37 |
|
$match = true; |
723
|
|
|
|
724
|
37 |
|
foreach ($baseParts as $i => $basePart) { |
725
|
37 |
|
if ($match && isset($parts[$i]) && $basePart === $parts[$i]) { |
726
|
21 |
|
unset($parts[$i]); |
727
|
|
|
|
728
|
21 |
|
continue; |
729
|
|
|
} |
730
|
|
|
|
731
|
26 |
|
$match = false; |
732
|
26 |
|
$dotDotPrefix .= '../'; |
733
|
|
|
} |
734
|
|
|
|
735
|
37 |
|
return rtrim($dotDotPrefix.implode('/', $parts), '/'); |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* Returns whether the given path is on the local filesystem. |
740
|
|
|
* |
741
|
|
|
* @param string $path A path string. |
742
|
|
|
* |
743
|
|
|
* @return bool Returns true if the path is local, false for a URL. |
744
|
|
|
* |
745
|
|
|
* @since 1.0 Added method. |
746
|
|
|
* @since 2.0 Method now fails if $path is not a string. |
747
|
|
|
*/ |
748
|
6 |
|
public static function isLocal($path) |
749
|
|
|
{ |
750
|
6 |
|
Assert::string($path, 'The path must be a string. Got: %s'); |
751
|
|
|
|
752
|
5 |
|
return '' !== $path && false === strpos($path, '://'); |
753
|
|
|
} |
754
|
|
|
|
755
|
|
|
/** |
756
|
|
|
* Returns the longest common base path of a set of paths. |
757
|
|
|
* |
758
|
|
|
* Dot segments ("." and "..") are removed/collapsed and all slashes turned |
759
|
|
|
* into forward slashes. |
760
|
|
|
* |
761
|
|
|
* ```php |
762
|
|
|
* $basePath = Path::getLongestCommonBasePath(array( |
763
|
|
|
* '/webmozart/css/style.css', |
764
|
|
|
* '/webmozart/css/..' |
765
|
|
|
* )); |
766
|
|
|
* // => /webmozart |
767
|
|
|
* ``` |
768
|
|
|
* |
769
|
|
|
* The root is returned if no common base path can be found: |
770
|
|
|
* |
771
|
|
|
* ```php |
772
|
|
|
* $basePath = Path::getLongestCommonBasePath(array( |
773
|
|
|
* '/webmozart/css/style.css', |
774
|
|
|
* '/puli/css/..' |
775
|
|
|
* )); |
776
|
|
|
* // => / |
777
|
|
|
* ``` |
778
|
|
|
* |
779
|
|
|
* If the paths are located on different Windows partitions, `null` is |
780
|
|
|
* returned. |
781
|
|
|
* |
782
|
|
|
* ```php |
783
|
|
|
* $basePath = Path::getLongestCommonBasePath(array( |
784
|
|
|
* 'C:/webmozart/css/style.css', |
785
|
|
|
* 'D:/webmozart/css/..' |
786
|
|
|
* )); |
787
|
|
|
* // => null |
788
|
|
|
* ``` |
789
|
|
|
* |
790
|
|
|
* @param array $paths A list of paths. |
791
|
|
|
* |
792
|
|
|
* @return string|null The longest common base path in canonical form or |
793
|
|
|
* `null` if the paths are on different Windows |
794
|
|
|
* partitions. |
795
|
|
|
* |
796
|
|
|
* @since 1.0 Added method. |
797
|
|
|
* @since 2.0 Method now fails if $paths are not strings. |
798
|
|
|
*/ |
799
|
81 |
|
public static function getLongestCommonBasePath(array $paths) |
800
|
|
|
{ |
801
|
81 |
|
Assert::allString($paths, 'The paths must be strings. Got: %s'); |
802
|
|
|
|
803
|
80 |
|
list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths))); |
804
|
|
|
|
805
|
80 |
|
for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) { |
806
|
75 |
|
list($root, $path) = self::split(self::canonicalize(current($paths))); |
807
|
|
|
|
808
|
|
|
// If we deal with different roots (e.g. C:/ vs. D:/), it's time |
809
|
|
|
// to quit |
810
|
75 |
|
if ($root !== $bpRoot) { |
811
|
15 |
|
return null; |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
// Make the base path shorter until it fits into path |
815
|
60 |
|
while (true) { |
816
|
60 |
|
if ('.' === $basePath) { |
817
|
|
|
// No more base paths |
818
|
12 |
|
$basePath = ''; |
819
|
|
|
|
820
|
|
|
// Next path |
821
|
12 |
|
continue 2; |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
// Prevent false positives for common prefixes |
825
|
|
|
// see isBasePath() |
826
|
60 |
|
if (0 === strpos($path.'/', $basePath.'/')) { |
827
|
|
|
// Next path |
828
|
48 |
|
continue 2; |
829
|
|
|
} |
830
|
|
|
|
831
|
36 |
|
$basePath = dirname($basePath); |
832
|
|
|
} |
833
|
|
|
} |
834
|
|
|
|
835
|
65 |
|
return $bpRoot.$basePath; |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
/** |
839
|
|
|
* Joins two or more path strings. |
840
|
|
|
* |
841
|
|
|
* The result is a canonical path. |
842
|
|
|
* |
843
|
|
|
* @param string[]|string $paths Path parts as parameters or array. |
844
|
|
|
* |
845
|
|
|
* @return string The joint path. |
846
|
|
|
* |
847
|
|
|
* @since 2.0 Added method. |
848
|
|
|
*/ |
849
|
61 |
|
public static function join($paths) |
850
|
|
|
{ |
851
|
61 |
|
if (!is_array($paths)) { |
852
|
52 |
|
$paths = func_get_args(); |
853
|
|
|
} |
854
|
|
|
|
855
|
61 |
|
Assert::allString($paths, 'The paths must be strings. Got: %s'); |
856
|
|
|
|
857
|
60 |
|
$finalPath = null; |
858
|
60 |
|
$wasScheme = false; |
859
|
|
|
|
860
|
60 |
|
foreach ($paths as $path) { |
861
|
59 |
|
$path = (string) $path; |
862
|
|
|
|
863
|
59 |
|
if ('' === $path) { |
864
|
6 |
|
continue; |
865
|
|
|
} |
866
|
|
|
|
867
|
58 |
|
if (null === $finalPath) { |
868
|
|
|
// For first part we keep slashes, like '/top', 'C:\' or 'phar://' |
869
|
58 |
|
$finalPath = $path; |
870
|
58 |
|
$wasScheme = (strpos($path, '://') !== false); |
871
|
58 |
|
continue; |
872
|
|
|
} |
873
|
|
|
|
874
|
|
|
// Only add slash if previous part didn't end with '/' or '\' |
875
|
53 |
|
if (!in_array(substr($finalPath, -1), array('/', '\\'))) { |
876
|
28 |
|
$finalPath .= '/'; |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
// If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim |
880
|
53 |
|
$finalPath .= $wasScheme ? $path : ltrim($path, '/'); |
881
|
53 |
|
$wasScheme = false; |
882
|
|
|
} |
883
|
|
|
|
884
|
60 |
|
if (null === $finalPath) { |
885
|
2 |
|
return ''; |
886
|
|
|
} |
887
|
|
|
|
888
|
58 |
|
return self::canonicalize($finalPath); |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
/** |
892
|
|
|
* Returns whether a path is a base path of another path. |
893
|
|
|
* |
894
|
|
|
* Dot segments ("." and "..") are removed/collapsed and all slashes turned |
895
|
|
|
* into forward slashes. |
896
|
|
|
* |
897
|
|
|
* ```php |
898
|
|
|
* Path::isBasePath('/webmozart', '/webmozart/css'); |
899
|
|
|
* // => true |
900
|
|
|
* |
901
|
|
|
* Path::isBasePath('/webmozart', '/webmozart'); |
902
|
|
|
* // => true |
903
|
|
|
* |
904
|
|
|
* Path::isBasePath('/webmozart', '/webmozart/..'); |
905
|
|
|
* // => false |
906
|
|
|
* |
907
|
|
|
* Path::isBasePath('/webmozart', '/puli'); |
908
|
|
|
* // => false |
909
|
|
|
* ``` |
910
|
|
|
* |
911
|
|
|
* @param string $basePath The base path to test. |
912
|
|
|
* @param string $ofPath The other path. |
913
|
|
|
* |
914
|
|
|
* @return bool Whether the base path is a base path of the other path. |
915
|
|
|
* |
916
|
|
|
* @since 1.0 Added method. |
917
|
|
|
* @since 2.0 Method now fails if $basePath or $ofPath is not a string. |
918
|
|
|
*/ |
919
|
61 |
|
public static function isBasePath($basePath, $ofPath) |
920
|
|
|
{ |
921
|
61 |
|
Assert::string($basePath, 'The base path must be a string. Got: %s'); |
922
|
|
|
|
923
|
60 |
|
$basePath = self::canonicalize($basePath); |
924
|
60 |
|
$ofPath = self::canonicalize($ofPath); |
925
|
|
|
|
926
|
|
|
// Append slashes to prevent false positives when two paths have |
927
|
|
|
// a common prefix, for example /base/foo and /base/foobar. |
928
|
|
|
// Don't append a slash for the root "/", because then that root |
929
|
|
|
// won't be discovered as common prefix ("//" is not a prefix of |
930
|
|
|
// "/foobar/"). |
931
|
59 |
|
return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/'); |
932
|
|
|
} |
933
|
|
|
|
934
|
|
|
/** |
935
|
|
|
* Splits a part into its root directory and the remainder. |
936
|
|
|
* |
937
|
|
|
* If the path has no root directory, an empty root directory will be |
938
|
|
|
* returned. |
939
|
|
|
* |
940
|
|
|
* If the root directory is a Windows style partition, the resulting root |
941
|
|
|
* will always contain a trailing slash. |
942
|
|
|
* |
943
|
|
|
* list ($root, $path) = Path::split("C:/webmozart") |
944
|
|
|
* // => array("C:/", "webmozart") |
945
|
|
|
* |
946
|
|
|
* list ($root, $path) = Path::split("C:") |
947
|
|
|
* // => array("C:/", "") |
948
|
|
|
* |
949
|
|
|
* @param string $path The canonical path to split. |
950
|
|
|
* |
951
|
|
|
* @return string[] An array with the root directory and the remaining |
952
|
|
|
* relative path. |
953
|
|
|
*/ |
954
|
408 |
|
private static function split($path) |
955
|
|
|
{ |
956
|
408 |
|
if ('' === $path) { |
957
|
3 |
|
return array('', ''); |
958
|
|
|
} |
959
|
|
|
|
960
|
|
|
// Remember scheme as part of the root, if any |
961
|
408 |
View Code Duplication |
if (false !== ($pos = strpos($path, '://'))) { |
962
|
87 |
|
$root = substr($path, 0, $pos + 3); |
963
|
87 |
|
$path = substr($path, $pos + 3); |
964
|
|
|
} else { |
965
|
337 |
|
$root = ''; |
966
|
|
|
} |
967
|
|
|
|
968
|
408 |
|
$length = strlen($path); |
969
|
|
|
|
970
|
|
|
// Remove and remember root directory |
971
|
408 |
|
if ('/' === $path[0]) { |
972
|
188 |
|
$root .= '/'; |
973
|
188 |
|
$path = $length > 1 ? substr($path, 1) : ''; |
974
|
253 |
|
} elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { |
975
|
195 |
|
if (2 === $length) { |
976
|
|
|
// Windows special case: "C:" |
977
|
1 |
|
$root .= $path.'/'; |
978
|
1 |
|
$path = ''; |
979
|
194 |
|
} elseif ('/' === $path[2]) { |
980
|
|
|
// Windows normal case: "C:/".. |
981
|
192 |
|
$root .= substr($path, 0, 3); |
982
|
192 |
|
$path = $length > 3 ? substr($path, 3) : ''; |
983
|
|
|
} |
984
|
|
|
} |
985
|
|
|
|
986
|
408 |
|
return array($root, $path); |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
/** |
990
|
|
|
* Converts string to lower-case (multi-byte safe if mbstring is installed). |
991
|
|
|
* |
992
|
|
|
* @param string $str The string |
993
|
|
|
* |
994
|
|
|
* @return string Lower case string |
995
|
|
|
*/ |
996
|
6 |
|
private static function toLower($str) |
997
|
|
|
{ |
998
|
6 |
|
if (function_exists('mb_strtolower')) { |
999
|
6 |
|
return mb_strtolower($str, mb_detect_encoding($str)); |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
return strtolower($str); |
1003
|
|
|
} |
1004
|
|
|
|
1005
|
|
|
private function __construct() |
1006
|
|
|
{ |
1007
|
|
|
} |
1008
|
|
|
} |
1009
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.