Browse Source

Merge pull request #17630 from cakephp/path-combine

Add pathCombine() helper to Core functions
Mark Story 2 years ago
parent
commit
bab1a6e501
3 changed files with 120 additions and 0 deletions
  1. 58 0
      src/Core/functions.php
  2. 20 0
      src/Core/functions_global.php
  3. 42 0
      tests/TestCase/Core/FunctionsTest.php

+ 58 - 0
src/Core/functions.php

@@ -26,6 +26,64 @@ if (!defined('DS')) {
     define('DS', DIRECTORY_SEPARATOR);
 }
 
+if (!function_exists('Cake\Core\pathCombine')) {
+    /**
+     * Combines parts with a forward-slash `/`.
+     *
+     * Skips adding a forward-slash if either `/` or `\` already exists.
+     *
+     * @param list<string> $parts
+     * @param bool|null $trailing Determines how trailing slashes are handled
+     *  - If true, ensures a trailing forward-slash is added if one doesn't exist
+     *  - If false, ensures any trailing slash is removed
+     *  - if null, ignores trailing slashes
+     * @return string
+     */
+    function pathCombine(array $parts, ?bool $trailing = null): string
+    {
+        $numParts = count($parts);
+        if ($numParts === 0) {
+            if ($trailing === true) {
+                return '/';
+            } else {
+                return '';
+            }
+        }
+
+        $path = $parts[0];
+        for ($i = 1; $i < $numParts; ++$i) {
+            $part = $parts[$i];
+            if (strlen($part) === 0) {
+                continue;
+            }
+
+            if ($path[-1] === '/' || $path[-1] === '\\') {
+                if ($part[0] === '/' || $part[0] === '\\') {
+                    $path .= substr($part, 1);
+                } else {
+                    $path .= $part;
+                }
+            } elseif ($part[0] === '/' || $part[0] === '\\') {
+                $path .= $part;
+            } else {
+                $path .= '/' . $part;
+            }
+        }
+
+        if ($trailing === true) {
+            if ($path === '' || ($path[-1] !== '/' && $path[-1] !== '\\')) {
+                $path .= '/';
+            }
+        } elseif ($trailing === false) {
+            if ($path !== '' && ($path[-1] === '/' || $path[-1] === '\\')) {
+                $path = substr($path, 0, -1);
+            }
+        }
+
+        return $path;
+    }
+}
+
 if (!function_exists('Cake\Core\h')) {
     /**
      * Convenience method for htmlspecialchars.

+ 20 - 0
src/Core/functions_global.php

@@ -20,11 +20,31 @@ use function Cake\Core\deprecationWarning as cakeDeprecationWarning;
 use function Cake\Core\env as cakeEnv;
 use function Cake\Core\h as cakeH;
 use function Cake\Core\namespaceSplit as cakeNamespaceSplit;
+use function Cake\Core\pathCombine as cakePathCombine;
 use function Cake\Core\pj as cakePj;
 use function Cake\Core\pluginSplit as cakePluginSplit;
 use function Cake\Core\pr as cakePr;
 use function Cake\Core\triggerWarning as cakeTriggerWarning;
 
+if (!function_exists('pathCombine')) {
+    /**
+     * Combines parts with a forward-slash `/`.
+     *
+     * Skips adding a forward-slash if either `/` or `\` already exists.
+     *
+     * @param list<string> $parts
+     * @param bool|null $trailing Determines how trailing slashes are handled
+     *  - If true, ensures a trailing forward-slash is added if one doesn't exist
+     *  - If false, ensures any trailing slash is removed
+     *  - if null, ignores trailing slashes
+     * @return string
+     */
+    function pathCombine(array $parts, ?bool $trailing = null): string
+    {
+        return cakePathCombine($parts, $trailing);
+    }
+}
+
 if (!function_exists('h')) {
     /**
      * Convenience method for htmlspecialchars.

+ 42 - 0
tests/TestCase/Core/FunctionsTest.php

@@ -25,6 +25,7 @@ use function Cake\Core\deprecationWarning;
 use function Cake\Core\env;
 use function Cake\Core\h;
 use function Cake\Core\namespaceSplit;
+use function Cake\Core\pathCombine;
 use function Cake\Core\pluginSplit;
 use function Cake\Core\toBool;
 use function Cake\Core\toInt;
@@ -36,6 +37,47 @@ use function Cake\Core\triggerWarning;
  */
 class FunctionsTest extends TestCase
 {
+    public function testPathCombine(): void
+    {
+        $this->assertSame('', pathCombine([]));
+        $this->assertSame('', pathCombine(['']));
+        $this->assertSame('', pathCombine(['', '']));
+        $this->assertSame('/', pathCombine(['/', '/']));
+
+        $this->assertSame('path/to/file', pathCombine(['path', 'to', 'file']));
+        $this->assertSame('path/to/file', pathCombine(['path/', 'to', 'file']));
+        $this->assertSame('path/to/file', pathCombine(['path', 'to/', 'file']));
+        $this->assertSame('path/to/file', pathCombine(['path/', 'to/', 'file']));
+        $this->assertSame('path/to/file', pathCombine(['path/', '/to/', 'file']));
+
+        $this->assertSame('/path/to/file', pathCombine(['/', 'path', 'to', 'file']));
+        $this->assertSame('/path/to/file', pathCombine(['/', '/path', 'to', 'file']));
+
+        $this->assertSame('/path/to/file/', pathCombine(['/path', 'to', 'file/']));
+        $this->assertSame('/path/to/file/', pathCombine(['/path', 'to', 'file', '/']));
+        $this->assertSame('/path/to/file/', pathCombine(['/path', 'to', 'file/', '/']));
+
+        // Test adding trailing slash
+        $this->assertSame('/', pathCombine([], trailing: true));
+        $this->assertSame('/', pathCombine([''], trailing: true));
+        $this->assertSame('/', pathCombine(['/'], trailing: true));
+        $this->assertSame('/path/to/file/', pathCombine(['/path', 'to', 'file/'], trailing: true));
+        $this->assertSame('/path/to/file/', pathCombine(['/path', 'to', 'file/', '/'], trailing: true));
+
+        // Test removing trailing slash
+        $this->assertSame('', pathCombine([''], trailing: false));
+        $this->assertSame('', pathCombine(['/'], trailing: false));
+        $this->assertSame('/path/to/file', pathCombine(['/path', 'to', 'file/'], trailing: false));
+        $this->assertSame('/path/to/file', pathCombine(['/path', 'to', 'file/', '/'], trailing: false));
+
+        // Test Windows-style backslashes
+        $this->assertSame('/path/to\\file', pathCombine(['/', '\\path', 'to', '\\file']));
+        $this->assertSame('/path\\to\\file/', pathCombine(['/', 'path', '\\to\\', 'file'], trailing: true));
+        $this->assertSame('/path\\to\\file\\', pathCombine(['/', 'path', '\\to\\', 'file', '\\'], trailing: true));
+        $this->assertSame('/path\\to\\file', pathCombine(['/', 'path', '\\to\\', 'file'], trailing: false));
+        $this->assertSame('/path\\to\\file', pathCombine(['/', 'path', '\\to\\', 'file', '\\'], trailing: false));
+    }
+
     /**
      * Test cases for env()
      */