Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace GLOB_BRACE with a more robust solution #664

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Facades/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ public static function smartGlob(string $pattern, int $flags = 0): Collection
return self::kernel()->filesystem()->smartGlob($pattern, $flags);
}

/**
* Find files in the project's directory, with optional filtering by extension and recursion.
*
* The returned collection will be a list of paths relative to the project root.
*
* @param string $directory
* @param string|array<string>|false $matchExtensions The file extension(s) to match, or false to match all files.
* @param bool $recursive Whether to search recursively or not.
* @return \Illuminate\Support\Collection<int, string>
*/
public static function findFiles(string $directory, string|array|false $matchExtensions = false, bool $recursive = false): Collection
{
return self::kernel()->filesystem()->findFiles($directory, $matchExtensions, $recursive);
}

/**
* Touch one or more files in the project's directory.
*
Expand Down
4 changes: 2 additions & 2 deletions src/Foundation/Kernel/FileCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace Hyde\Foundation\Kernel;

use Hyde\Facades\Filesystem;
use Hyde\Foundation\Concerns\BaseFoundationCollection;
use Hyde\Framework\Exceptions\FileNotFoundException;
use Hyde\Pages\Concerns\HydePage;
use Hyde\Support\Filesystem\SourceFile;

use function basename;
use function glob;
use function str_starts_with;

/**
Expand Down Expand Up @@ -59,7 +59,7 @@ protected function runExtensionHandlers(): void
protected function discoverFilesFor(string $pageClass): void
{
// Scan the source directory, and directories therein, for files that match the model's file extension.
foreach (glob($this->kernel->path($pageClass::sourcePath('{*,**/*}')), GLOB_BRACE) as $path) {
foreach (Filesystem::findFiles($pageClass::sourceDirectory(), $pageClass::fileExtension(), true) as $path) {
if (! str_starts_with(basename((string) $path), '_')) {
$this->addFile(SourceFile::make($path, $pageClass));
}
Expand Down
13 changes: 13 additions & 0 deletions src/Foundation/Kernel/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Hyde\Foundation\HydeKernel;
use Hyde\Foundation\PharSupport;
use Illuminate\Support\Collection;
use Hyde\Framework\Actions\Internal\FileFinder;

use function collect;
use function Hyde\normalize_slashes;
Expand Down Expand Up @@ -183,4 +184,16 @@ public function smartGlob(string $pattern, int $flags = 0): Collection

return $files->map(fn (string $path): string => $this->pathToRelative($path));
}

/**
* @param string|array<string>|false $matchExtensions
* @return \Illuminate\Support\Collection<int, string>
*/
public function findFiles(string $directory, string|array|false $matchExtensions = false, bool $recursive = false): Collection
{
/** @var \Hyde\Framework\Actions\Internal\FileFinder $finder */
$finder = app(FileFinder::class);

return $finder->handle($directory, $matchExtensions, $recursive);
}
}
66 changes: 66 additions & 0 deletions src/Framework/Actions/Internal/FileFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Actions\Internal;

use Hyde\Facades\Filesystem;
use Hyde\Hyde;
use Illuminate\Support\Collection;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

/**
* @interal This class is used internally by the framework and is not part of the public API, unless that is requested on GitHub with a valid use case.
*/
class FileFinder
{
/**
* @param array<string>|string|false $matchExtensions
* @return \Illuminate\Support\Collection<int, string>
*/
public static function handle(string $directory, array|string|false $matchExtensions = false, bool $recursive = false): Collection
{
if (! Filesystem::isDirectory($directory)) {
return collect();
}

$finder = Finder::create()->files()->in(Hyde::path($directory));

if ($recursive === false) {
$finder->depth('== 0');
}

if ($matchExtensions !== false) {
$finder->name(static::buildFileExtensionPattern((array) $matchExtensions));
}

return collect($finder)->map(function (SplFileInfo $file): string {
return Hyde::pathToRelative($file->getPathname());
})->sort()->values();
}

/** @param array<string> $extensions */
protected static function buildFileExtensionPattern(array $extensions): string
{
$extensions = self::expandCommaSeparatedValues($extensions);

return '/\.('.self::normalizeExtensionForRegexPattern($extensions).')$/i';
}

/** @param array<string> $extensions */
private static function expandCommaSeparatedValues(array $extensions): array
{
return array_merge(...array_map(function (string $item): array {
return array_map(fn (string $item): string => trim($item), explode(',', $item));
}, $extensions));
}

/** @param array<string> $extensions */
private static function normalizeExtensionForRegexPattern(array $extensions): string
{
return implode('|', array_map(function (string $extension): string {
return preg_quote(ltrim($extension, '.'), '/');
}, $extensions));
}
}
3 changes: 1 addition & 2 deletions src/Framework/Actions/PreBuildTasks/CleanSiteDirectory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Hyde\Framework\Features\BuildTasks\PreBuildTask;

use function basename;
use function glob;
use function in_array;
use function sprintf;

Expand All @@ -21,7 +20,7 @@ class CleanSiteDirectory extends PreBuildTask
public function handle(): void
{
if ($this->isItSafeToCleanOutputDirectory()) {
Filesystem::unlink(glob(Hyde::sitePath('*.{html,json}'), GLOB_BRACE));
Filesystem::unlink(Filesystem::findFiles(Hyde::sitePath(), ['html', 'json'])->all());
Filesystem::cleanDirectory(Hyde::siteMediaPath());
}
}
Expand Down
7 changes: 2 additions & 5 deletions src/Support/DataCollections.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

use function implode;
use function Hyde\path_join;
use function json_decode;
use function sprintf;
use function Hyde\unslash;
use function str_starts_with;

Expand Down Expand Up @@ -102,9 +101,7 @@ public static function json(string $name, bool $asArray = false): static

protected static function findFiles(string $name, array|string $extensions): Collection
{
return Filesystem::smartGlob(sprintf('%s/%s/*.{%s}',
static::$sourceDirectory, $name, implode(',', (array) $extensions)
), GLOB_BRACE);
return Filesystem::findFiles(path_join(static::$sourceDirectory, $name), $extensions);
}

protected static function makeIdentifier(string $path): string
Expand Down
17 changes: 9 additions & 8 deletions src/Support/Filesystem/MediaFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Hyde\Support\Filesystem;

use Hyde\Facades\Filesystem;
use Hyde\Hyde;
use Hyde\Facades\Config;
use Hyde\Framework\Exceptions\FileNotFoundException;
Expand All @@ -14,12 +15,9 @@
use function array_merge;
use function array_keys;
use function filesize;
use function implode;
use function pathinfo;
use function collect;
use function is_file;
use function sprintf;
use function glob;

/**
* File abstraction for a project media file.
Expand Down Expand Up @@ -104,15 +102,18 @@ protected static function discoverMediaAssetFiles(): array
})->all();
}

/** @return array<string> */
protected static function getMediaAssetFiles(): array
{
return glob(Hyde::path(static::getMediaGlobPattern()), GLOB_BRACE) ?: [];
return Filesystem::findFiles(Hyde::getMediaDirectory(), static::getMediaFileExtensions(), true)->all();
}

protected static function getMediaGlobPattern(): string
/** @return array<string>|string */
protected static function getMediaFileExtensions(): array|string
{
return sprintf(Hyde::getMediaDirectory().'/{*,**/*,**/*/*}.{%s}', implode(',',
Config::getArray('hyde.media_extensions', self::EXTENSIONS)
));
/** @var array<string>|string $config */
$config = Config::get('hyde.media_extensions', self::EXTENSIONS);

return $config;
}
}
2 changes: 1 addition & 1 deletion tests/Feature/DiscoveryServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function testMediaAssetExtensionsCanBeAddedByCommaSeparatedValues()

$this->assertSame([], MediaFile::files());

self::mockConfig(['hyde.media_extensions' => ['1,2,3']]);
self::mockConfig(['hyde.media_extensions' => '1,2,3']);
$this->assertSame(['test.1', 'test.2', 'test.3'], MediaFile::files());
}

Expand Down
16 changes: 16 additions & 0 deletions tests/Feature/FileCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,20 @@ public function testDocumentationPagesAreDiscovered()
$this->assertArrayHasKey('_docs/foo.md', $collection->toArray());
$this->assertEquals(new SourceFile('_docs/foo.md', DocumentationPage::class), $collection->get('_docs/foo.md'));
}

public function testDiscoverFilesForRecursivelyDiscoversFilesInSubdirectories()
{
$this->file('_pages/foo.md');
$this->file('_pages/foo/bar.md');
$this->file('_pages/foo/bar/baz.md');
$collection = FileCollection::init(Hyde::getInstance())->boot();

$this->assertArrayHasKey('_pages/foo.md', $collection->toArray());
$this->assertArrayHasKey('_pages/foo/bar.md', $collection->toArray());
$this->assertArrayHasKey('_pages/foo/bar/baz.md', $collection->toArray());

$this->assertEquals(new SourceFile('_pages/foo.md', MarkdownPage::class), $collection->get('_pages/foo.md'));
$this->assertEquals(new SourceFile('_pages/foo/bar.md', MarkdownPage::class), $collection->get('_pages/foo/bar.md'));
$this->assertEquals(new SourceFile('_pages/foo/bar/baz.md', MarkdownPage::class), $collection->get('_pages/foo/bar/baz.md'));
}
}
82 changes: 82 additions & 0 deletions tests/Feature/Foundation/FilesystemTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,28 @@
use Hyde\Foundation\HydeKernel;
use Hyde\Foundation\Kernel\Filesystem;
use Hyde\Foundation\PharSupport;
use Hyde\Framework\Actions\Internal\FileFinder;
use Hyde\Hyde;
use Hyde\Pages\BladePage;
use Hyde\Pages\DocumentationPage;
use Hyde\Pages\HtmlPage;
use Hyde\Pages\MarkdownPage;
use Hyde\Pages\MarkdownPost;
use Hyde\Testing\CreatesTemporaryFiles;
use Hyde\Testing\UnitTestCase;
use Illuminate\Support\Collection;

use function Hyde\normalize_slashes;

/**
* @covers \Hyde\Foundation\HydeKernel
* @covers \Hyde\Foundation\Kernel\Filesystem
* @covers \Hyde\Facades\Filesystem
*/
class FilesystemTest extends UnitTestCase
{
use CreatesTemporaryFiles;

protected string $originalBasePath;

protected Filesystem $filesystem;
Expand Down Expand Up @@ -365,4 +371,80 @@ public function testPathToRelativeHelperDoesNotModifyNonProjectPaths()
$this->assertSame(normalize_slashes($testString), Hyde::pathToRelative($testString));
}
}

public function testFindFileMethodFindsFilesInDirectory()
{
$this->files(['directory/apple.md', 'directory/banana.md', 'directory/cherry.md']);
$files = $this->filesystem->findFiles('directory');

$this->assertCount(3, $files);
$this->assertContains('directory/apple.md', $files);
$this->assertContains('directory/banana.md', $files);
$this->assertContains('directory/cherry.md', $files);

$this->cleanUpFilesystem();
}

public function testFindFileMethodTypes()
{
$this->file('directory/apple.md');
$files = $this->filesystem->findFiles('directory');

$this->assertInstanceOf(Collection::class, $files);
$this->assertContainsOnly('int', $files->keys());
$this->assertContainsOnly('string', $files->all());
$this->assertSame('directory/apple.md', $files->first());

$this->cleanUpFilesystem();
}

public function testFindFileMethodTypesWithArguments()
{
$this->file('directory/apple.md');

$this->assertInstanceOf(Collection::class, $this->filesystem->findFiles('directory', false, false));
$this->assertInstanceOf(Collection::class, $this->filesystem->findFiles('directory', 'md', false));
$this->assertInstanceOf(Collection::class, $this->filesystem->findFiles('directory', false, true));
$this->assertInstanceOf(Collection::class, $this->filesystem->findFiles('directory', 'md', true));

$this->cleanUpFilesystem();
}

public function testFindFilesFromFilesystemFacade()
{
$this->files(['directory/apple.md', 'directory/banana.md', 'directory/cherry.md']);
$files = \Hyde\Facades\Filesystem::findFiles('directory');

$this->assertSame(['directory/apple.md', 'directory/banana.md', 'directory/cherry.md'], $files->sort()->values()->all());

$this->cleanUpFilesystem();
}

public function testFindFilesFromFilesystemFacadeWithArguments()
{
$this->files(['directory/apple.md', 'directory/banana.txt', 'directory/cherry.blade.php', 'directory/nested/dates.md']);

$files = \Hyde\Facades\Filesystem::findFiles('directory', 'md');
$this->assertSame(['directory/apple.md'], $files->all());

$files = \Hyde\Facades\Filesystem::findFiles('directory', false, true);
$this->assertSame(['directory/apple.md', 'directory/banana.txt', 'directory/cherry.blade.php', 'directory/nested/dates.md'], $files->sort()->values()->all());

$this->cleanUpFilesystem();
}

public function testCanSwapOutFileFinder()
{
app()->bind(FileFinder::class, function () {
return new class
{
public static function handle(): Collection
{
return collect(['mocked']);
}
};
});

$this->assertSame(['mocked'], \Hyde\Facades\Filesystem::findFiles('directory')->toArray());
}
}
Loading
Loading