annotate vendor/webmozart/path-util/src/Path.php @ 19:fa3358dc1485 tip

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