Skip to content

Commit

Permalink
New FileHelper methods
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonkelly committed Dec 9, 2022
1 parent 5ad080d commit bf59883
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## Unreleased

- Fixed a bug where entry tab contents could remain visible when switching to other tabs, after changing the entry type.
- Added `craft\helpers\FileHelper::absolutePath()`.
- Added `craft\helpers\FileHelper::findClosestFile()`.
- Added `craft\helpers\FileHelper::isWithin()`.
- Added `craft\helpers\FileHelper::relativePath()`.

## 4.3.4 - 2022-11-29

Expand Down
112 changes: 112 additions & 0 deletions src/helpers/FileHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,83 @@ public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR): string
return $path;
}

/**
* Returns a relative path based on a source location or the current working directory.
*
* @param string $to The target path.
* @param string|null $from The source location. Defaults to the current working directory.
* @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
* @return string The relative path if possible, or an absolute path if the directory is not contained within `$from`.
* @since 4.3.5
*/
public static function relativePath(
string $to,
?string $from = null,
string $ds = DIRECTORY_SEPARATOR,
): string {
$to = static::absolutePath($to, ds: $ds);

if ($from === null) {
$from = FileHelper::normalizePath(getcwd(), $ds);
} else {
$from = static::absolutePath($from, ds: $ds);
}

if ($from === $to) {
return '.';
}

if (!str_starts_with($to . $ds, $from . $ds)) {
return $to;
}

return substr($to, strlen($from) + 1);
}

/**
* Returns an absolute path based on a source location or the current working directory.
*
* @param string $to The target path.
* @param string|null $from The source location. Defaults to the current working directory.
* @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
* @return string
* @since 4.3.5
*/
public static function absolutePath(
string $to,
?string $from = null,
string $ds = DIRECTORY_SEPARATOR,
): string {
$to = static::normalizePath($to, $ds);

// Already absolute?
if (str_starts_with($to, $ds)) {
return $to;
}

if ($from === null) {
$from = FileHelper::normalizePath(getcwd(), $ds);
} else {
$from = static::absolutePath($from, ds: $ds);
}

return $from . $ds . $to;
}

/**
* Returns whether the given path is within another path.
*
* @param string $path the path to check
* @param string $parentPath the parent path that `$path` should be within
* @return bool
*/
public static function isWithin(string $path, string $parentPath): bool
{
$path = static::absolutePath($path, ds: '/');
$parentPath = static::absolutePath($parentPath, ds: '/');
return $path !== $parentPath && str_starts_with("$path/", "$parentPath/");
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -505,6 +582,41 @@ public static function clearDirectory(string $dir, array $options = []): void
closedir($handle);
}

/**
* Traverses up the filesystem looking for the closest file to the given directory.
*
* @param string $dir the directory at or above which the file will be looked for
* @param array $options options for file searching. See [[findFiles()]].
* @return string|null the closest matching file
* @throws InvalidArgumentException if the directory is invalid
* @since 4.3.5
*/
public static function findClosestFile(string $dir, array $options = []): ?string
{
$options['recursive'] = false;
$dir = static::absolutePath($dir, ds: '/');
while (true) {
$exists = file_exists($dir);
try {
$files = static::findFiles($dir, $options);
} catch (InvalidArgumentException $e) {
if ($exists) {
return null;
}
throw $e;
}

if (!empty($files)) {
return reset($files);
}
$parent = dirname($dir);
if ($parent === $dir) {
return null;
}
$dir = $parent;
}
}

/**
* Returns the last modification time for the given path.
*
Expand Down
6 changes: 4 additions & 2 deletions src/test/TestSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,16 @@ public static function configureCraft(): bool
defined('CURLOPT_TIMEOUT_MS') || define('CURLOPT_TIMEOUT_MS', 155);
defined('CURLOPT_CONNECTTIMEOUT_MS') || define('CURLOPT_CONNECTTIMEOUT_MS', 156);

$libPath = dirname(__DIR__, 2) . '/lib';
$srcPath = dirname(__DIR__);
$repoRoot = dirname(__DIR__, 2);
$libPath = $repoRoot . '/lib';
$srcPath = $repoRoot . '/src';

require $libPath . '/yii2/Yii.php';
require $srcPath . '/Craft.php';

// Set aliases
Craft::setAlias('@vendor', $vendorPath);
Craft::setAlias('@craftcms', $repoRoot);
Craft::setAlias('@lib', $libPath);
Craft::setAlias('@craft', $srcPath);
Craft::setAlias('@appicons', $srcPath . DIRECTORY_SEPARATOR . 'icons');
Expand Down
119 changes: 119 additions & 0 deletions tests/unit/helpers/FileHelper/FileHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,41 @@ public function testNormalizePath(string $expected, string $path, string $ds): v
self::assertSame($expected, FileHelper::normalizePath($path, $ds));
}

/**
* @dataProvider absolutePathDataProvider
* @param string $expected
* @param string $to
* @param string|null $from
* @param string $ds
*/
public function testAbsolutePath(string $expected, string $to, ?string $from, string $ds): void
{
self::assertSame($expected, FileHelper::absolutePath($to, $from, $ds));
}

/**
* @dataProvider relativePathDataProvider
* @param string $expected
* @param string $to
* @param string|null $from
* @param string $ds
*/
public function testRelativePath(string $expected, string $to, ?string $from, string $ds): void
{
self::assertSame($expected, FileHelper::relativePath($to, $from, $ds));
}

/**
* @dataProvider isWithinDataProvider
* @param bool $expected
* @param string $path
* @param string $parentPath
*/
public function testIsWithin(bool $expected, string $path, string $parentPath): void
{
self::assertSame($expected, FileHelper::isWithin($path, $parentPath));
}

/**
* @dataProvider isDirectoryEmptyDataProvider
* @param bool $expected
Expand Down Expand Up @@ -235,6 +270,19 @@ public function testWriteToFileExceptions(): void
});
}

/**
* @dataProvider findClosestFileDataProvider
*/
public function testFindClosestFile(string|null|false $expected, string $dir, array $options = [])
{
if ($expected === false) {
$this->expectException(InvalidArgumentException::class);
FileHelper::findClosestFile($dir, $options);
} else {
self::assertSame($expected, FileHelper::findClosestFile($dir, $options));
}
}

/**
* @return array
*/
Expand All @@ -255,6 +303,46 @@ public function normalizePathDataProvider(): array
];
}

/**
* @return array
*/
public function absolutePathDataProvider(): array
{
return [
['/foo/bar', 'bar', '/foo', '/'],
['/foo/bar', '/foo/bar', null, '/'],
['\\foo\\bar', 'bar', '/foo', '\\'],
[FileHelper::normalizePath(getcwd(), '/') . '/foo/bar', 'foo/bar', null, '/'],
[FileHelper::normalizePath(getcwd(), '/') . '/baz/foo/bar', 'foo/bar', 'baz', '/'],
];
}

/**
* @return array
*/
public function relativePathDataProvider(): array
{
return [
['bar/baz', '/foo/bar/baz', '/foo', '/'],
['bar\\baz', '/foo/bar/baz', '/foo', '\\'],
['/foo/bar/baz', '/foo/bar/baz', '/test', '/'],
];
}

/**
* @return array
*/
public function isWithinDataProvider(): array
{
return [
[true, '/foo/bar', '/foo'],
[true, 'foo/bar', 'foo'],
[true, 'foo/bar', getcwd() . '/foo'],
[false, '/foo/bar', '\\foo\\bar'],
[false, '/baz', '/foo'],
];
}

/**
* @return array
*/
Expand Down Expand Up @@ -348,6 +436,37 @@ public function writeToFileDataProvider(): array
];
}

/**
* @return array
*/
public function findClosestFileDataProvider(): array
{
return [
[
FileHelper::normalizePath(__DIR__ . '/sandbox/singlefile/foo.txt', '/'),
__DIR__ . '/sandbox/singlefile',
],
[
FileHelper::normalizePath(__DIR__ . '/sandbox/singlefile/foo.txt', '/'),
__DIR__ . '/sandbox/singlefile/nested',
[
'except' => ['ignore*'],
],
],
[
null,
'/',
[
'only' => ['nonexistent.txt'],
],
],
[
false,
__DIR__ . '/sandbox/singlefile/nonexistent',
],
];
}

/**
* @inheritdoc
*/
Expand Down
Empty file.
Empty file.

0 comments on commit bf59883

Please sign in to comment.