From 0dacd1038932d6c5df50ad9b08f5134ffa9dfbc4 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Mon, 18 Nov 2024 12:49:25 +0100 Subject: [PATCH] refactor(support): convert `PathHelper` to immutable class (#743) --- src/Tempest/Cache/src/ProjectCache.php | 2 +- .../Console/src/ConsoleApplication.php | 6 +- .../LogOutputBufferInitializer.php | 4 +- src/Tempest/Core/composer.json | 1 + src/Tempest/Core/src/Composer.php | 4 +- src/Tempest/Core/src/DiscoveryCache.php | 2 +- .../src/Kernel/LoadDiscoveryLocations.php | 26 ++-- src/Tempest/Core/src/PublishesFiles.php | 11 +- src/Tempest/Core/src/functions.php | 19 +-- .../Framework/Testing/InstallerTester.php | 2 +- .../Generation/src/StubFileGenerator.php | 11 +- src/Tempest/Http/src/HttpApplication.php | 6 +- .../Session/Managers/FileSessionManager.php | 4 +- .../Http/src/Static/StaticGenerateCommand.php | 4 +- src/Tempest/Support/src/NamespaceHelper.php | 90 ++++++++++++ src/Tempest/Support/src/PathHelper.php | 131 +++++++++--------- src/Tempest/Support/src/StringHelper.php | 4 +- src/Tempest/Support/src/functions.php | 13 +- src/Tempest/Support/tests/PathHelperTest.php | 7 +- .../src/Renderers/TempestViewCompiler.php | 2 +- src/Tempest/View/src/ViewCache.php | 4 +- src/Tempest/View/src/ViewCachePool.php | 8 +- src/Tempest/View/tests/ViewCachePoolTest.php | 25 ++-- src/Tempest/View/tests/ViewCacheTest.php | 7 +- tests/Integration/Core/PublishesFilesTest.php | 4 +- tests/Integration/Core/RootPathHelperTest.php | 4 +- tests/Integration/Http/FileSessionTest.php | 2 +- .../Http/Static/StaticCleanCommandTest.php | 4 +- .../Http/Static/StaticGenerateCommandTest.php | 8 +- .../Support/NamespaceHelperTest.php | 46 ++++++ tests/Integration/Support/PathHelperTest.php | 48 ------- 31 files changed, 298 insertions(+), 211 deletions(-) create mode 100644 src/Tempest/Support/src/NamespaceHelper.php create mode 100644 tests/Integration/Support/NamespaceHelperTest.php delete mode 100644 tests/Integration/Support/PathHelperTest.php diff --git a/src/Tempest/Cache/src/ProjectCache.php b/src/Tempest/Cache/src/ProjectCache.php index 9900183d8..7ea6842d6 100644 --- a/src/Tempest/Cache/src/ProjectCache.php +++ b/src/Tempest/Cache/src/ProjectCache.php @@ -18,7 +18,7 @@ public function __construct( private readonly CacheConfig $cacheConfig, ) { $this->pool = $this->cacheConfig->projectCachePool ?? new FilesystemAdapter( - directory: path($this->cacheConfig->directory, 'project'), + directory: path($this->cacheConfig->directory, 'project')->toString(), ); } diff --git a/src/Tempest/Console/src/ConsoleApplication.php b/src/Tempest/Console/src/ConsoleApplication.php index 315c4ce4a..3848134ff 100644 --- a/src/Tempest/Console/src/ConsoleApplication.php +++ b/src/Tempest/Console/src/ConsoleApplication.php @@ -13,7 +13,7 @@ use Tempest\Core\Tempest; use Tempest\Log\Channels\AppendLogChannel; use Tempest\Log\LogConfig; -use Tempest\Support\PathHelper; +use function Tempest\path; use Throwable; final readonly class ConsoleApplication implements Application @@ -45,8 +45,8 @@ public static function boot( $logConfig->debugLogPath === null && $logConfig->channels === [] ) { - $logConfig->debugLogPath = PathHelper::make($container->get(Kernel::class)->root, '/log/debug.log'); - $logConfig->channels[] = new AppendLogChannel(PathHelper::make($container->get(Kernel::class)->root, '/log/tempest.log')); + $logConfig->debugLogPath = path($container->get(Kernel::class)->root, '/log/debug.log')->toString(); + $logConfig->channels[] = new AppendLogChannel(path($container->get(Kernel::class)->root, '/log/tempest.log')->toString()); } return $application; diff --git a/src/Tempest/Console/src/Initializers/LogOutputBufferInitializer.php b/src/Tempest/Console/src/Initializers/LogOutputBufferInitializer.php index 52a4d5f0a..a41128820 100644 --- a/src/Tempest/Console/src/Initializers/LogOutputBufferInitializer.php +++ b/src/Tempest/Console/src/Initializers/LogOutputBufferInitializer.php @@ -10,7 +10,7 @@ use Tempest\Container\Initializer; use Tempest\Container\Singleton; use Tempest\Core\Kernel; -use Tempest\Support\PathHelper; +use function Tempest\path; final readonly class LogOutputBufferInitializer implements Initializer { @@ -20,7 +20,7 @@ public function initialize(Container $container): LogOutputBuffer $consoleConfig = $container->get(ConsoleConfig::class); $kernel = $container->get(Kernel::class); - $path = $consoleConfig->logPath ?? PathHelper::make($kernel->root, 'console.log'); + $path = $consoleConfig->logPath ?? path($kernel->root, 'console.log')->toString(); return new LogOutputBuffer($path); } diff --git a/src/Tempest/Core/composer.json b/src/Tempest/Core/composer.json index d0283ac38..837d8347c 100644 --- a/src/Tempest/Core/composer.json +++ b/src/Tempest/Core/composer.json @@ -6,6 +6,7 @@ "require": { "php": "^8.3", "tempest/container": "dev-main", + "tempest/support": "dev-main", "vlucas/phpdotenv": "^5.6", "filp/whoops": "^2.15" }, diff --git a/src/Tempest/Core/src/Composer.php b/src/Tempest/Core/src/Composer.php index a9910ce2c..e504e6a2a 100644 --- a/src/Tempest/Core/src/Composer.php +++ b/src/Tempest/Core/src/Composer.php @@ -4,8 +4,8 @@ namespace Tempest\Core; +use function Tempest\path; use function Tempest\Support\arr; -use Tempest\Support\PathHelper; final class Composer { @@ -19,7 +19,7 @@ final class Composer public function __construct( string $root, ) { - $composerFilePath = PathHelper::make($root, 'composer.json'); + $composerFilePath = path($root, 'composer.json')->toString(); $this->composer = $this->loadComposerFile($composerFilePath); $this->namespaces = arr($this->composer) diff --git a/src/Tempest/Core/src/DiscoveryCache.php b/src/Tempest/Core/src/DiscoveryCache.php index 2c5ee6df5..10e3cfc2c 100644 --- a/src/Tempest/Core/src/DiscoveryCache.php +++ b/src/Tempest/Core/src/DiscoveryCache.php @@ -27,7 +27,7 @@ public function __construct( ?CacheItemPoolInterface $pool = null, ) { $this->pool = $pool ?? new FilesystemAdapter( - directory: path($this->cacheConfig->directory, 'discovery'), + directory: path($this->cacheConfig->directory, 'discovery')->toString(), ); } diff --git a/src/Tempest/Core/src/Kernel/LoadDiscoveryLocations.php b/src/Tempest/Core/src/Kernel/LoadDiscoveryLocations.php index 9626d2833..067fe8af5 100644 --- a/src/Tempest/Core/src/Kernel/LoadDiscoveryLocations.php +++ b/src/Tempest/Core/src/Kernel/LoadDiscoveryLocations.php @@ -8,7 +8,7 @@ use Tempest\Core\DiscoveryException; use Tempest\Core\DiscoveryLocation; use Tempest\Core\Kernel; -use Tempest\Support\PathHelper; +use function Tempest\path; /** @internal */ final readonly class LoadDiscoveryLocations @@ -35,8 +35,8 @@ public function __invoke(): void */ private function discoverCorePackages(): array { - $composerPath = PathHelper::make($this->kernel->root, 'vendor/composer'); - $installed = $this->loadJsonFile(PathHelper::make($composerPath, 'installed.json')); + $composerPath = path($this->kernel->root, 'vendor/composer'); + $installed = $this->loadJsonFile(path($composerPath, 'installed.json')->toString()); $packages = $installed['packages'] ?? []; $discoveredLocations = []; @@ -49,12 +49,12 @@ private function discoverCorePackages(): array continue; } - $packagePath = PathHelper::make($composerPath, $package['install-path'] ?? ''); + $packagePath = path($composerPath, $package['install-path'] ?? ''); foreach ($package['autoload']['psr-4'] as $namespace => $namespacePath) { - $namespacePath = PathHelper::make($packagePath, $namespacePath); + $namespacePath = path($packagePath, $namespacePath); - $discoveredLocations[] = new DiscoveryLocation($namespace, $namespacePath); + $discoveredLocations[] = new DiscoveryLocation($namespace, $namespacePath->toString()); } } @@ -69,9 +69,9 @@ private function discoverAppNamespaces(): array $discoveredLocations = []; foreach ($this->composer->namespaces as $namespace) { - $path = PathHelper::make($this->kernel->root, $namespace->path); + $path = path($this->kernel->root, $namespace->path); - $discoveredLocations[] = new DiscoveryLocation($namespace->namespace, $path); + $discoveredLocations[] = new DiscoveryLocation($namespace->namespace, $path->toString()); } return $discoveredLocations; @@ -82,8 +82,8 @@ private function discoverAppNamespaces(): array */ private function discoverVendorPackages(): array { - $composerPath = PathHelper::make($this->kernel->root, 'vendor/composer'); - $installed = $this->loadJsonFile(PathHelper::make($composerPath, 'installed.json')); + $composerPath = path($this->kernel->root, 'vendor/composer'); + $installed = $this->loadJsonFile(path($composerPath, 'installed.json')->toString()); $packages = $installed['packages'] ?? []; $discoveredLocations = []; @@ -96,7 +96,7 @@ private function discoverVendorPackages(): array continue; } - $packagePath = PathHelper::make($composerPath, $package['install-path'] ?? ''); + $packagePath = path($composerPath, $package['install-path'] ?? ''); $requiresTempest = isset($package['require']['tempest/framework']) || isset($package['require']['tempest/core']); $hasPsr4Namespaces = isset($package['autoload']['psr-4']); @@ -105,9 +105,9 @@ private function discoverVendorPackages(): array } foreach ($package['autoload']['psr-4'] as $namespace => $namespacePath) { - $path = PathHelper::make($packagePath, $namespacePath); + $path = path($packagePath, $namespacePath); - $discoveredLocations[] = new DiscoveryLocation($namespace, $path); + $discoveredLocations[] = new DiscoveryLocation($namespace, $path->toString()); } } diff --git a/src/Tempest/Core/src/PublishesFiles.php b/src/Tempest/Core/src/PublishesFiles.php index d5b6db3de..1a809f007 100644 --- a/src/Tempest/Core/src/PublishesFiles.php +++ b/src/Tempest/Core/src/PublishesFiles.php @@ -13,7 +13,8 @@ use Tempest\Generation\Exceptions\FileGenerationAbortedException; use Tempest\Generation\Exceptions\FileGenerationFailedException; use Tempest\Generation\StubFileGenerator; -use Tempest\Support\PathHelper; +use function Tempest\path; +use Tempest\Support\NamespaceHelper; use function Tempest\Support\str; use Tempest\Validation\Rules\EndsWith; use Tempest\Validation\Rules\NotEmpty; @@ -133,15 +134,15 @@ public function publishImports(): void public function getSuggestedPath(string $className, ?string $pathPrefix = null, ?string $classSuffix = null): string { // Separate input path and classname - $inputClassName = PathHelper::toClassName($className); - $inputPath = str(PathHelper::make($className))->replaceLast($inputClassName, '')->toString(); + $inputClassName = NamespaceHelper::toClassName($className); + $inputPath = str(path($className))->replaceLast($inputClassName, '')->toString(); $className = str($inputClassName) ->pascal() ->finish($classSuffix ?? '') ->toString(); // Prepare the suggested path from the project namespace - return str(PathHelper::make( + return str(path( $this->composer->mainNamespace->path, $pathPrefix ?? '', $inputPath, @@ -158,7 +159,7 @@ public function getSuggestedPath(string $className, ?string $pathPrefix = null, */ public function promptTargetPath(string $suggestedPath): string { - $className = PathHelper::toClassName($suggestedPath); + $className = NamespaceHelper::toClassName($suggestedPath); return $this->console->ask( question: sprintf('Where do you want to save the file "%s"?', $className), diff --git a/src/Tempest/Core/src/functions.php b/src/Tempest/Core/src/functions.php index 35402a5f8..9279d98ce 100644 --- a/src/Tempest/Core/src/functions.php +++ b/src/Tempest/Core/src/functions.php @@ -8,28 +8,15 @@ use Tempest\Core\Composer; use Tempest\Core\DeferredTasks; use Tempest\Core\Kernel; + use function Tempest\Support\path; use function Tempest\Support\str; - /** - * Creates and sanitizes a file system path from the given `$parts`. The resulting path is not checked against the file system. - */ - function path(string ...$parts): string - { - $path = implode('/', $parts); - - return str_replace( - ['///', '//', '\\', '\\\\'], - '/', - $path, - ); - } - /** * Creates a path scoped within the root of the project */ function root_path(string ...$parts): string { - return path(realpath(get(Kernel::class)->root), ...$parts); + return path(realpath(get(Kernel::class)->root), ...$parts)->toString(); } /** @@ -39,7 +26,7 @@ function src_path(string ...$parts): string { $composer = get(Composer::class); - return path($composer->mainNamespace->path, ...$parts); + return path($composer->mainNamespace->path, ...$parts)->toString(); } /** diff --git a/src/Tempest/Framework/Testing/InstallerTester.php b/src/Tempest/Framework/Testing/InstallerTester.php index a736e9498..e67831d3d 100644 --- a/src/Tempest/Framework/Testing/InstallerTester.php +++ b/src/Tempest/Framework/Testing/InstallerTester.php @@ -48,7 +48,7 @@ public function setRoot(string $root): self public function path(string $path): string { - return path($this->root, $path); + return path($this->root, $path)->toString(); } public function put(string $path, string $contents): self diff --git a/src/Tempest/Generation/src/StubFileGenerator.php b/src/Tempest/Generation/src/StubFileGenerator.php index 7f067d6d3..73fdd37a7 100644 --- a/src/Tempest/Generation/src/StubFileGenerator.php +++ b/src/Tempest/Generation/src/StubFileGenerator.php @@ -9,7 +9,8 @@ use Tempest\Generation\Enums\StubFileType; use Tempest\Generation\Exceptions\FileGenerationAbortedException; use Tempest\Generation\Exceptions\FileGenerationFailedException; -use Tempest\Support\PathHelper; +use function Tempest\path; +use Tempest\Support\NamespaceHelper; use function Tempest\Support\str; use Tempest\Support\StringHelper; use Throwable; @@ -51,8 +52,8 @@ public function generateClassFile( $this->prepareFilesystem($targetPath); // Transform stub to class - $namespace = PathHelper::toMainNamespace($targetPath); - $classname = PathHelper::toClassName($targetPath); + $namespace = NamespaceHelper::toMainNamespace($targetPath); + $classname = NamespaceHelper::toClassName($targetPath); $classManipulator = (new ClassManipulator($stubFile->filePath)) ->setNamespace($namespace) ->setClassName($classname); @@ -68,8 +69,8 @@ public function generateClassFile( // Run all manipulations $classManipulator = array_reduce( array: $manipulations, - initial: $classManipulator, - callback: fn (ClassManipulator $manipulator, Closure $manipulation) => $manipulation($manipulator) + callback: fn (ClassManipulator $manipulator, Closure $manipulation) => $manipulation($manipulator), + initial: $classManipulator ); if (file_exists($targetPath) && $shouldOverride) { diff --git a/src/Tempest/Http/src/HttpApplication.php b/src/Tempest/Http/src/HttpApplication.php index 4b6d1c581..99fe65e25 100644 --- a/src/Tempest/Http/src/HttpApplication.php +++ b/src/Tempest/Http/src/HttpApplication.php @@ -14,7 +14,7 @@ use Tempest\Http\Session\Session; use Tempest\Log\Channels\AppendLogChannel; use Tempest\Log\LogConfig; -use Tempest\Support\PathHelper; +use function Tempest\path; use Throwable; #[Singleton] @@ -41,9 +41,9 @@ public static function boot( && $logConfig->serverLogPath === null && $logConfig->channels === [] ) { - $logConfig->debugLogPath = PathHelper::make($container->get(Kernel::class)->root, '/log/debug.log'); + $logConfig->debugLogPath = path($container->get(Kernel::class)->root, '/log/debug.log')->toString(); $logConfig->serverLogPath = env('SERVER_LOG'); - $logConfig->channels[] = new AppendLogChannel(PathHelper::make($root, '/log/tempest.log')); + $logConfig->channels[] = new AppendLogChannel(path($root, '/log/tempest.log')->toString()); } return $application; diff --git a/src/Tempest/Http/src/Session/Managers/FileSessionManager.php b/src/Tempest/Http/src/Session/Managers/FileSessionManager.php index 0d46c5309..da9cc5df7 100644 --- a/src/Tempest/Http/src/Session/Managers/FileSessionManager.php +++ b/src/Tempest/Http/src/Session/Managers/FileSessionManager.php @@ -68,7 +68,7 @@ public function isValid(SessionId $id): bool private function getPath(SessionId $id): string { - return path($this->sessionConfig->path, (string)$id); + return path($this->sessionConfig->path, (string)$id)->toString(); } private function resolve(SessionId $id): ?Session @@ -129,7 +129,7 @@ private function persist(SessionId $id, ?array $data = null): Session public function cleanup(): void { - $sessionFiles = glob(path($this->sessionConfig->path, '/*')); + $sessionFiles = glob(path($this->sessionConfig->path, '/*')->toString()); foreach ($sessionFiles as $sessionFile) { $id = new SessionId(pathinfo($sessionFile, PATHINFO_FILENAME)); diff --git a/src/Tempest/Http/src/Static/StaticGenerateCommand.php b/src/Tempest/Http/src/Static/StaticGenerateCommand.php index f81d4e927..ae0e4d7ae 100644 --- a/src/Tempest/Http/src/Static/StaticGenerateCommand.php +++ b/src/Tempest/Http/src/Static/StaticGenerateCommand.php @@ -84,13 +84,13 @@ public function __invoke(): void continue; } - $directory = pathinfo($file, PATHINFO_DIRNAME); + $directory = $file->dirname(); if (! is_dir($directory)) { mkdir($directory, recursive: true); } - file_put_contents($file, $content); + file_put_contents($file->path(), $content); $this->writeln("- {$uri} > {$file}"); } catch (Throwable $e) { diff --git a/src/Tempest/Support/src/NamespaceHelper.php b/src/Tempest/Support/src/NamespaceHelper.php new file mode 100644 index 000000000..572a8c536 --- /dev/null +++ b/src/Tempest/Support/src/NamespaceHelper.php @@ -0,0 +1,90 @@ +replaceEnd('\\', ''); + + return arr(explode('\\', (string) $path)) + ->map(fn (string $segment) => (string) str($segment)->pascal()) + ->implode('\\') + ->toString(); + } + + public static function toMainNamespace(string $path): string + { + return self::toNamespace( + src_namespace() . '/' . str($path) + ->replaceStart(src_path(), '') + ->trim('/') + ->toString() + ); + } + + public static function toRegisteredNamespace(string $path): string + { + $composer = get(Composer::class); + $kernel = get(Kernel::class); + + $relativePath = self::prepareStringForNamespace($path, $kernel->root) + ->replaceEnd('\\', '') + ->replace('\\', '/') + ->finish('/'); + + foreach ($composer->namespaces as $namespace) { + if ($relativePath->startsWith($namespace->path)) { + return (string) $relativePath + ->replace($namespace->path, $namespace->namespace) + ->replace(['/', '//'], '\\') + ->replaceEnd('.php', '') + ->replaceEnd('\\', ''); + } + } + + throw new Exception(sprintf('No registered namespace matches the specified path [%s].', $path)); + } + + /** + * Convert a path to a class name. + * + * @param string $path The path to convert. + */ + public static function toClassName(string $path): string + { + return str($path) + ->replace(['/', '\\'], '/') + ->replaceEnd('/', '') + ->replaceEnd('.php', '') + ->afterLast('/') + ->classBasename() + ->toString(); + } + + private static function prepareStringForNamespace(string $path, string $root = ''): StringHelper + { + $normalized = str($path) + ->replaceStart($root, '') + ->replaceStart('/', '') + ->replace(['/', '//'], '\\'); + + // If the path is a to a PHP file, we exclude the file name. Otherwise, + // it's a path to a directory, which should be included in the namespace. + if ($normalized->endsWith('.php')) { + return $normalized->beforeLast(['/', '\\']); + } + + return $normalized; + } +} diff --git a/src/Tempest/Support/src/PathHelper.php b/src/Tempest/Support/src/PathHelper.php index 428710184..dae12491b 100644 --- a/src/Tempest/Support/src/PathHelper.php +++ b/src/Tempest/Support/src/PathHelper.php @@ -4,20 +4,19 @@ namespace Tempest\Support; -use Exception; -use Tempest\Core\Composer; -use Tempest\Core\Kernel; -use function Tempest\get; -use function Tempest\src_namespace; -use function Tempest\src_path; - -final readonly class PathHelper +use Stringable; + +final readonly class PathHelper implements Stringable { - /** - * Returns a valid path from the specified portions. - */ - public static function make(string ...$paths): string + private string $path; + + public function __construct(Stringable|string ...$paths) { + $paths = array_map( + fn (self|string $path) => (string)$path, + $paths, + ); + // Split paths items on forward and backward slashes $parts = array_reduce($paths, fn (array $carry, string $part) => [...$carry, ...explode('/', $part)], []); $parts = array_reduce($parts, fn (array $carry, string $part) => [...$carry, ...explode('\\', $part)], []); @@ -31,91 +30,85 @@ public static function make(string ...$paths): string // Add / if first entry starts with forward- or backward slash $firstEntry = $paths[0]; + if (str_starts_with($firstEntry, '/') || str_starts_with($firstEntry, '\\')) { $path = '/' . $path; } // Add / if last entry ends with forward- or backward slash $lastEntry = $paths[count($paths) - 1]; + if ((count($paths) > 1 || strlen($lastEntry) > 1) && (str_ends_with($lastEntry, '/') || str_ends_with($lastEntry, '\\'))) { $path .= '/'; } - return $path; + $this->path = $path; } - private static function prepareStringForNamespace(string $path, string $root = ''): StringHelper + public function toString(): string { - $normalized = str($path) - ->replaceStart($root, '') - ->replaceStart('/', '') - ->replace(['/', '//'], '\\'); - - // If the path is a to a PHP file, we exclude the file name. Otherwise, - // it's a path to a directory, which should be included in the namespace. - if ($normalized->endsWith('.php')) { - return $normalized->beforeLast(['/', '\\']); - } + return $this->path; + } - return $normalized; + public function info(int $flags = PATHINFO_ALL): string|array + { + return pathinfo($this->path, $flags); } - public static function toNamespace(string $path, string $root = ''): string + public function path(): string { - $path = static::prepareStringForNamespace($path, $root)->replaceEnd('\\', ''); + return $this->path; + } - return arr(explode('\\', (string) $path)) - ->map(fn (string $segment) => (string) str($segment)->pascal()) - ->implode('\\') - ->toString(); + public function dirname(): string + { + return $this->info(PATHINFO_DIRNAME); } - public static function toMainNamespace(string $path): string + public function filename(): string { - return self::toNamespace( - src_namespace() . '/' . str($path) - ->replaceStart(src_path(), '') - ->trim('/') - ->toString() + return $this->info(PATHINFO_FILENAME); + } + + public function basename(): string + { + return $this->info(PATHINFO_BASENAME); + } + + public function extension(): string + { + return $this->info(PATHINFO_EXTENSION); + } + + public function glob(string $pattern): ArrayHelper + { + return arr( + glob((new self($this->path, $pattern))->toString()), ); } - public static function toRegisteredNamespace(string $path): string + public function isDirectory(): bool { - $composer = get(Composer::class); - $kernel = get(Kernel::class); - - $relativePath = static::prepareStringForNamespace($path, $kernel->root) - ->replaceEnd('\\', '') - ->replace('\\', '/') - ->finish('/'); - - foreach ($composer->namespaces as $namespace) { - if ($relativePath->startsWith($namespace->path)) { - return (string) $relativePath - ->replace($namespace->path, $namespace->namespace) - ->replace(['/', '//'], '\\') - ->replaceEnd('.php', '') - ->replaceEnd('\\', ''); - } - } + return is_dir($this->path); + } - throw new Exception(sprintf('No registered namespace matches the specified path [%s].', $path)); + public function isFile(): bool + { + return is_file($this->path); + } + + public function exists(): bool + { + return file_exists($this->path); + } + + public function equals(Stringable $other): bool + { + return $this->path === (string)$other; } - /** - * Convert a path to a class name. - * - * @param string $path The path to convert. - */ - public static function toClassName(string $path): string + public function __toString(): string { - return str($path) - ->replace(['/', '\\'], '/') - ->replaceEnd('/', '') - ->replaceEnd('.php', '') - ->afterLast('/') - ->classBasename() - ->toString(); + return $this->path; } } diff --git a/src/Tempest/Support/src/StringHelper.php b/src/Tempest/Support/src/StringHelper.php index 0193b6522..d671b4cba 100644 --- a/src/Tempest/Support/src/StringHelper.php +++ b/src/Tempest/Support/src/StringHelper.php @@ -16,9 +16,9 @@ { private string $string; - public function __construct(?string $string = '') + public function __construct(Stringable|string|null $string = '') { - $this->string = $string ?? ''; + $this->string = (string) ($string ?? ''); } /** diff --git a/src/Tempest/Support/src/functions.php b/src/Tempest/Support/src/functions.php index 43cdd15b0..49c65a370 100644 --- a/src/Tempest/Support/src/functions.php +++ b/src/Tempest/Support/src/functions.php @@ -3,10 +3,13 @@ declare(strict_types=1); namespace Tempest\Support { + + use Stringable; + /** * Creates an instance of {@see StringHelper} using the given `$string`. */ - function str(?string $string = ''): StringHelper + function str(Stringable|string|null $string = ''): StringHelper { return new StringHelper($string); } @@ -18,4 +21,12 @@ function arr(mixed $input = []): ArrayHelper { return new ArrayHelper($input); } + + /** + * Creates and sanitizes a file system path from the given `$parts`. The resulting path is not checked against the file system. + */ + function path(Stringable|string ...$parts): PathHelper + { + return new PathHelper(...$parts); + } } diff --git a/src/Tempest/Support/tests/PathHelperTest.php b/src/Tempest/Support/tests/PathHelperTest.php index ba2046804..cb9580209 100644 --- a/src/Tempest/Support/tests/PathHelperTest.php +++ b/src/Tempest/Support/tests/PathHelperTest.php @@ -8,7 +8,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Tempest\Support\PathHelper; +use function Tempest\path; +use Tempest\Support\NamespaceHelper; /** * @internal @@ -19,7 +20,7 @@ final class PathHelperTest extends TestCase public function test_make(array $paths, string $expected): void { // Act - $output = PathHelper::make(...$paths); + $output = path(...$paths)->toString(); // Assert $this->assertSame($expected, $output); @@ -118,8 +119,8 @@ public static function paths(): Generator public function toClassName(string $path, string $expected): void { $this->assertSame( - actual: PathHelper::toClassName($path), expected: $expected, + actual: NamespaceHelper::toClassName($path), ); } diff --git a/src/Tempest/View/src/Renderers/TempestViewCompiler.php b/src/Tempest/View/src/Renderers/TempestViewCompiler.php index b5c0804ac..ea1b4cb28 100644 --- a/src/Tempest/View/src/Renderers/TempestViewCompiler.php +++ b/src/Tempest/View/src/Renderers/TempestViewCompiler.php @@ -67,7 +67,7 @@ private function retrieveTemplate(string $path): string $searchPath = $path; while (! file_exists($searchPath) && $location = current($discoveryLocations)) { - $searchPath = path($location->path, $path); + $searchPath = path($location->path, $path)->toString(); next($discoveryLocations); } diff --git a/src/Tempest/View/src/ViewCache.php b/src/Tempest/View/src/ViewCache.php index 78efb5099..00d921cc5 100644 --- a/src/Tempest/View/src/ViewCache.php +++ b/src/Tempest/View/src/ViewCache.php @@ -22,7 +22,7 @@ public function __construct( ?ViewCachePool $pool = null, ) { $this->cachePool = $pool ?? new ViewCachePool( - directory: path($this->cacheConfig->directory, 'views'), + directory: path($this->cacheConfig->directory, 'views')->toString(), ); } @@ -38,7 +38,7 @@ public function getCachedViewPath(string $path, Closure $compiledView): string $this->cachePool->save($cacheItem); } - return path($this->cachePool->directory, $cacheItem->getKey() . '.php'); + return path($this->cachePool->directory, $cacheItem->getKey() . '.php')->toString(); } protected function getCachePool(): CacheItemPoolInterface diff --git a/src/Tempest/View/src/ViewCachePool.php b/src/Tempest/View/src/ViewCachePool.php index ed011ebc2..2522078f6 100644 --- a/src/Tempest/View/src/ViewCachePool.php +++ b/src/Tempest/View/src/ViewCachePool.php @@ -55,9 +55,11 @@ public function hasItem(string $key): bool public function clear(): bool { - if (is_dir($this->directory)) { + $path = path($this->directory); + + if ($path->isDirectory()) { /** @phpstan-ignore-next-line */ - arr(glob(path($this->directory, '/*.php')))->each(fn (string $file) => unlink($file)); + $path->glob('/*.php')->each(fn (string $file) => unlink($file)); rmdir($this->directory); } @@ -108,6 +110,6 @@ private function makePath(CacheItemInterface|string $key): string { $key = is_string($key) ? $key : $key->getKey(); - return path($this->directory, "/{$key}.php"); + return path($this->directory, "/{$key}.php")->toString(); } } diff --git a/src/Tempest/View/tests/ViewCachePoolTest.php b/src/Tempest/View/tests/ViewCachePoolTest.php index a88562fc1..989cfa5b3 100644 --- a/src/Tempest/View/tests/ViewCachePoolTest.php +++ b/src/Tempest/View/tests/ViewCachePoolTest.php @@ -7,7 +7,6 @@ use Exception; use PHPUnit\Framework\TestCase; use function Tempest\path; -use function Tempest\Support\arr; use Tempest\View\ViewCachePool; /** @@ -30,9 +29,11 @@ protected function setUp(): void protected function tearDown(): void { - if (is_dir(self::DIRECTORY)) { + $directory = path(self::DIRECTORY); + + if ($directory->isDirectory()) { /** @phpstan-ignore-next-line */ - arr(glob(path(self::DIRECTORY, '/*.php')))->each(fn (string $file) => unlink($file)); + $directory->glob('/*.php')->each(fn (string $file) => unlink($file)); rmdir(self::DIRECTORY); } @@ -47,8 +48,8 @@ public function test_get_item(): void $this->pool->save($item); - $this->assertFileExists(path(self::DIRECTORY, 'test.php')); - $this->assertEquals('hi', file_get_contents(path(self::DIRECTORY, 'test.php'))); + $this->assertFileExists(path(self::DIRECTORY, 'test.php')->toString()); + $this->assertEquals('hi', file_get_contents(path(self::DIRECTORY, 'test.php')->toString())); } public function test_has_item(): void @@ -88,7 +89,7 @@ public function test_delete_item(): void $this->pool->save($item); $this->pool->deleteItem('test'); - $this->assertFileDoesNotExist(path(self::DIRECTORY, 'test.php')); + $this->assertFileDoesNotExist(path(self::DIRECTORY, 'test.php')->toString()); } public function test_delete_items(): void @@ -101,13 +102,13 @@ public function test_delete_items(): void $items[1]->set('hi'); $this->pool->save($items[1]); - $this->assertFileExists(path(self::DIRECTORY, 'a.php')); - $this->assertFileExists(path(self::DIRECTORY, 'b.php')); + $this->assertFileExists(path(self::DIRECTORY, 'a.php')->toString()); + $this->assertFileExists(path(self::DIRECTORY, 'b.php')->toString()); $this->pool->deleteItems(['a', 'b']); - $this->assertFileDoesNotExist(path(self::DIRECTORY, 'a.php')); - $this->assertFileDoesNotExist(path(self::DIRECTORY, 'b.php')); + $this->assertFileDoesNotExist(path(self::DIRECTORY, 'a.php')->toString()); + $this->assertFileDoesNotExist(path(self::DIRECTORY, 'b.php')->toString()); } public function test_clear_pool(): void @@ -118,8 +119,8 @@ public function test_clear_pool(): void $this->pool->save($item); $this->pool->clear(); - $this->assertFileDoesNotExist(path(self::DIRECTORY, 'test.php')); - $this->assertDirectoryDoesNotExist(path(self::DIRECTORY)); + $this->assertFileDoesNotExist(path(self::DIRECTORY, 'test.php')->toString()); + $this->assertDirectoryDoesNotExist(path(self::DIRECTORY)->toString()); } public function test_save_deferred(): void diff --git a/src/Tempest/View/tests/ViewCacheTest.php b/src/Tempest/View/tests/ViewCacheTest.php index ab51200ed..aa9b405e6 100644 --- a/src/Tempest/View/tests/ViewCacheTest.php +++ b/src/Tempest/View/tests/ViewCacheTest.php @@ -7,7 +7,6 @@ use PHPUnit\Framework\TestCase; use Tempest\Cache\CacheConfig; use function Tempest\path; -use function Tempest\Support\arr; use Tempest\View\ViewCache; use Tempest\View\ViewCachePool; @@ -38,9 +37,11 @@ protected function setUp(): void protected function tearDown(): void { - if (is_dir(self::DIRECTORY)) { + $directory = path(self::DIRECTORY); + + if ($directory->isDirectory()) { /** @phpstan-ignore-next-line */ - arr(glob(path(self::DIRECTORY, '/*.php')))->each(fn (string $file) => unlink($file)); + $directory->glob('/*.php')->each(fn (string $file) => unlink($file)); rmdir(self::DIRECTORY); } diff --git a/tests/Integration/Core/PublishesFilesTest.php b/tests/Integration/Core/PublishesFilesTest.php index 844a9a51e..7c7c73518 100644 --- a/tests/Integration/Core/PublishesFilesTest.php +++ b/tests/Integration/Core/PublishesFilesTest.php @@ -47,12 +47,12 @@ public function get_suggested_path( $appPath = str_replace('\\', '/', $composer->mainNamespace->path); // Normalize windows path $this->assertSame( + expected: path($appPath, $expected)->toString(), actual: $concreteClass->getSuggestedPath( className: $className, pathPrefix: $pathPrefix, classSuffix: $classSuffix - ), - expected: path($appPath, $expected) + ) ); } diff --git a/tests/Integration/Core/RootPathHelperTest.php b/tests/Integration/Core/RootPathHelperTest.php index ab597691f..b0b5abed8 100644 --- a/tests/Integration/Core/RootPathHelperTest.php +++ b/tests/Integration/Core/RootPathHelperTest.php @@ -15,7 +15,7 @@ final class RootPathHelperTest extends FrameworkIntegrationTestCase { public function test_can_get_base_path(): void { - $this->assertSame(path(realpath($this->root)), root_path()); - $this->assertSame(path(realpath($this->root . '/tests/Fixtures')), root_path('/tests/Fixtures')); + $this->assertSame(path(realpath($this->root))->toString(), root_path()); + $this->assertSame(path(realpath($this->root . '/tests/Fixtures'))->toString(), root_path('/tests/Fixtures')); } } diff --git a/tests/Integration/Http/FileSessionTest.php b/tests/Integration/Http/FileSessionTest.php index 9d7afac35..40e20ebdf 100644 --- a/tests/Integration/Http/FileSessionTest.php +++ b/tests/Integration/Http/FileSessionTest.php @@ -74,7 +74,7 @@ public function test_destroy(): void { $session = $this->container->get(Session::class); - $path = path($this->path, (string) $session->id); + $path = path($this->path, (string) $session->id)->toString(); $this->assertFileExists($path); $session->destroy(); $this->assertFileDoesNotExist($path); diff --git a/tests/Integration/Http/Static/StaticCleanCommandTest.php b/tests/Integration/Http/Static/StaticCleanCommandTest.php index 9e6db8e5b..ca483ddb1 100644 --- a/tests/Integration/Http/Static/StaticCleanCommandTest.php +++ b/tests/Integration/Http/Static/StaticCleanCommandTest.php @@ -27,7 +27,7 @@ public function test_generate(): void $root = $this->kernel->root; - $this->assertFileDoesNotExist(path($root, '/public/static/a/b/index.html')); - $this->assertFileDoesNotExist(path($root, '/public/static/c/d/index.html')); + $this->assertFileDoesNotExist(path($root, '/public/static/a/b/index.html')->toString()); + $this->assertFileDoesNotExist(path($root, '/public/static/c/d/index.html')->toString()); } } diff --git a/tests/Integration/Http/Static/StaticGenerateCommandTest.php b/tests/Integration/Http/Static/StaticGenerateCommandTest.php index b774f28fd..244cad07e 100644 --- a/tests/Integration/Http/Static/StaticGenerateCommandTest.php +++ b/tests/Integration/Http/Static/StaticGenerateCommandTest.php @@ -26,11 +26,11 @@ public function test_static_site_generate_command(): void $root = $this->kernel->root; - $this->assertFileExists(path($root, '/public/static/a/b/index.html')); - $this->assertFileExists(path($root, '/public/static/c/d/index.html')); + $this->assertFileExists(path($root, '/public/static/a/b/index.html')->toString()); + $this->assertFileExists(path($root, '/public/static/c/d/index.html')->toString()); - $b = file_get_contents(path($root, '/public/static/a/b/index.html')); - $d = file_get_contents(path($root, '/public/static/c/d/index.html')); + $b = file_get_contents(path($root, '/public/static/a/b/index.html')->toString()); + $d = file_get_contents(path($root, '/public/static/c/d/index.html')->toString()); $this->assertStringContainsString('a', $b); $this->assertStringContainsString('b', $b); diff --git a/tests/Integration/Support/NamespaceHelperTest.php b/tests/Integration/Support/NamespaceHelperTest.php new file mode 100644 index 000000000..b6f0d0ef2 --- /dev/null +++ b/tests/Integration/Support/NamespaceHelperTest.php @@ -0,0 +1,46 @@ +assertSame('Tempest\\Auth', NamespaceHelper::toMainNamespace('src/Tempest/Auth/src/SomeNewClass.php')); + $this->assertSame('Tempest\\Auth\\SomeDirectory', NamespaceHelper::toMainNamespace('src/Tempest/Auth/src/SomeDirectory')); + } + + #[Test] + public function paths_to_non_registered_namespace_throw(): void + { + $this->expectException(Exception::class); + NamespaceHelper::toRegisteredNamespace('app/SomeNewClass.php'); + } + + #[Test] + public function path_to_namespace(): void + { + $this->assertSame('App', NamespaceHelper::toNamespace('app/SomeNewClass.php')); + $this->assertSame('App\\Foo\\Bar', NamespaceHelper::toNamespace('app/Foo/Bar/SomeNewClass.php')); + $this->assertSame('App\\Foo\\Bar\\Baz', NamespaceHelper::toNamespace('app/Foo/Bar/Baz')); + $this->assertSame('App\\FooBar', NamespaceHelper::toNamespace('app\\FooBar\\')); + $this->assertSame('App\\FooBar', NamespaceHelper::toNamespace('app\\FooBar\\File.php')); + + $this->assertSame('App\\Foo', NamespaceHelper::toNamespace('/home/project-name/app/Foo/Bar.php', root: '/home/project-name')); + $this->assertSame('App\\Foo', NamespaceHelper::toNamespace('/home/project-name/app/Foo/Bar.php', root: '/home/project-name/')); + + // we don't support skill issues + $this->assertSame('Home\ProjectName\App\Foo', NamespaceHelper::toNamespace('/home/project-name/app/Foo/Bar.php')); + } +} diff --git a/tests/Integration/Support/PathHelperTest.php b/tests/Integration/Support/PathHelperTest.php deleted file mode 100644 index f380441ea..000000000 --- a/tests/Integration/Support/PathHelperTest.php +++ /dev/null @@ -1,48 +0,0 @@ -assertSame('Tempest\\Auth', PathHelper::toRegisteredNamespace('src/Tempest/Auth/src/SomeNewClass.php')); - $this->assertSame('Tempest\\Auth\\SomeDirectory', PathHelper::toRegisteredNamespace('src/Tempest/Auth/src/SomeDirectory')); - $this->assertSame('Tempest\\Auth', PathHelper::toRegisteredNamespace($this->root.'/src/Tempest/Auth/src/SomeNewClass.php')); - $this->assertSame('Tempest\\Auth\\SomeDirectory', PathHelper::toRegisteredNamespace($this->root.'/src/Tempest/Auth/src/SomeDirectory')); - } - - #[Test] - public function paths_to_non_registered_namespace_throw(): void - { - $this->expectException(Exception::class); - PathHelper::toRegisteredNamespace('app/SomeNewClass.php'); - } - - #[Test] - public function path_to_namespace(): void - { - $this->assertSame('App', PathHelper::toNamespace('app/SomeNewClass.php')); - $this->assertSame('App\\Foo\\Bar', PathHelper::toNamespace('app/Foo/Bar/SomeNewClass.php')); - $this->assertSame('App\\Foo\\Bar\\Baz', PathHelper::toNamespace('app/Foo/Bar/Baz')); - $this->assertSame('App\\FooBar', PathHelper::toNamespace('app\\FooBar\\')); - $this->assertSame('App\\FooBar', PathHelper::toNamespace('app\\FooBar\\File.php')); - - $this->assertSame('App\\Foo', PathHelper::toNamespace('/home/project-name/app/Foo/Bar.php', root: '/home/project-name')); - $this->assertSame('App\\Foo', PathHelper::toNamespace('/home/project-name/app/Foo/Bar.php', root: '/home/project-name/')); - - // we don't support skill issues - $this->assertSame('Home\ProjectName\App\Foo', PathHelper::toNamespace('/home/project-name/app/Foo/Bar.php')); - } -}