diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 4c6fed350fe2..c6ddd84f2efc 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -63,14 +63,14 @@ class Autoloader /** * Stores namespaces as key, and path as values. * - * @var array> + * @var array> */ protected $prefixes = []; /** * Stores class name as key, and path as values. * - * @var array + * @var array */ protected $classmap = []; @@ -215,7 +215,8 @@ public function addNamespace($namespace, ?string $path = null) * * If a prefix param is set, returns only paths to the given prefix. * - * @return array + * @return array>|list + * @phpstan-return ($prefix is null ? array> : list) */ public function getNamespace(?string $prefix = null) { diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 2aff0b4a018a..b2d7bcd1464b 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -175,6 +175,8 @@ public function getClassname(string $file): string * 'app/Modules/foo/Config/Routes.php', * 'app/Modules/bar/Config/Routes.php', * ] + * + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { @@ -203,7 +205,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = } // Remove any duplicates - return array_unique($foundPaths); + return array_values(array_unique($foundPaths)); } /** @@ -237,7 +239,7 @@ protected function getNamespaces() foreach ($this->autoloader->getNamespace() as $prefix => $paths) { foreach ($paths as $path) { if ($prefix === 'CodeIgniter') { - $system = [ + $system[] = [ 'prefix' => $prefix, 'path' => rtrim($path, '\\/') . DIRECTORY_SEPARATOR, ]; @@ -252,9 +254,7 @@ protected function getNamespaces() } } - $namespaces[] = $system; - - return $namespaces; + return array_merge($namespaces, $system); } /** diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index d82bd05dc7e2..3a5347df868c 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -263,6 +263,25 @@ protected function basename(string $filename): string * Parses the class name and checks if it is already qualified. */ protected function qualifyClassName(): string + { + $class = $this->normalizeInputClassName(); + + // Gets the namespace from input. Don't forget the ending backslash! + $namespace = $this->getNamespace() . '\\'; + + if (strncmp($class, $namespace, strlen($namespace)) === 0) { + return $class; // @codeCoverageIgnore + } + + $directoryString = ($this->directory !== null) ? $this->directory . '\\' : ''; + + return $namespace . $directoryString . str_replace('/', '\\', $class); + } + + /** + * Normalize input classname. + */ + private function normalizeInputClassName(): string { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); @@ -298,7 +317,7 @@ protected function qualifyClassName(): string } // Trims input, normalize separators, and ensure that all paths are in Pascalcase. - $class = ltrim( + return ltrim( implode( '\\', array_map( @@ -308,17 +327,6 @@ protected function qualifyClassName(): string ), '\\/' ); - - // Gets the namespace from input. Don't forget the ending backslash! - $namespace = $this->getNamespace() . '\\'; - - if (strncmp($class, $namespace, strlen($namespace)) === 0) { - return $class; // @codeCoverageIgnore - } - - $directoryString = ($this->directory !== null) ? $this->directory . '\\' : ''; - - return $namespace . $directoryString . str_replace('/', '\\', $class); } /** diff --git a/system/Commands/Generators/TestGenerator.php b/system/Commands/Generators/TestGenerator.php index d09ecda5ae83..c061d41f31ac 100644 --- a/system/Commands/Generators/TestGenerator.php +++ b/system/Commands/Generators/TestGenerator.php @@ -14,7 +14,9 @@ namespace CodeIgniter\Commands\Generators; use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\GeneratorTrait; +use Config\Services; /** * Generates a skeleton command file. @@ -66,7 +68,8 @@ class TestGenerator extends BaseCommand * @var array */ protected $options = [ - '--force' => 'Force overwrite existing file.', + '--namespace' => 'Set root namespace. Default: "Tests".', + '--force' => 'Force overwrite existing file.', ]; /** @@ -76,9 +79,110 @@ public function run(array $params) { $this->component = 'Test'; $this->template = 'test.tpl.php'; - $this->namespace = 'Tests'; $this->classNameLang = 'CLI.generator.className.test'; $this->generateClass($params); } + + /** + * Gets the namespace from input or the default namespace. + */ + protected function getNamespace(): string + { + if ($this->namespace !== null) { + return $this->namespace; + } + + if ($this->getOption('namespace') !== null) { + return trim( + str_replace( + '/', + '\\', + $this->getOption('namespace') + ), + '\\' + ); + } + + $class = $this->normalizeInputClassName(); + $classPaths = explode('\\', $class); + + $namespaces = Services::autoloader()->getNamespace(); + + while ($classPaths !== []) { + array_pop($classPaths); + $namespace = implode('\\', $classPaths); + + foreach (array_keys($namespaces) as $prefix) { + if ($prefix === $namespace) { + // The input classname is FQCN, and use the namespace. + return $namespace; + } + } + } + + return 'Tests'; + } + + /** + * Builds the test file path from the class name. + * + * @param string $class namespaced classname. + */ + protected function buildPath(string $class): string + { + $namespace = $this->getNamespace(); + + $base = $this->searchTestFilePath($namespace); + + if ($base === null) { + CLI::error( + lang('CLI.namespaceNotDefined', [$namespace]), + 'light_gray', + 'red' + ); + CLI::newLine(); + + return ''; + } + + $realpath = realpath($base); + $base = ($realpath !== false) ? $realpath : $base; + + $file = $base . DIRECTORY_SEPARATOR + . str_replace( + '\\', + DIRECTORY_SEPARATOR, + trim(str_replace($namespace . '\\', '', $class), '\\') + ) . '.php'; + + return implode( + DIRECTORY_SEPARATOR, + array_slice( + explode(DIRECTORY_SEPARATOR, $file), + 0, + -1 + ) + ) . DIRECTORY_SEPARATOR . $this->basename($file); + } + + /** + * Returns test file path for the namespace. + */ + private function searchTestFilePath(string $namespace): ?string + { + $bases = Services::autoloader()->getNamespace($namespace); + + $base = null; + + foreach ($bases as $candidate) { + if (str_contains($candidate, '/tests/')) { + $base = $candidate; + + break; + } + } + + return $base; + } } diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 47450812b4e0..f3f047613eb9 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -139,13 +139,13 @@ private function outputCINamespaces(array $params): array $tbody = []; foreach ($config->psr4 as $ns => $paths) { - if (array_key_exists('r', $params)) { - $pathOutput = $this->truncate($paths, $maxLength); - } else { - $pathOutput = $this->truncate(clean_path($paths), $maxLength); - } - foreach ((array) $paths as $path) { + if (array_key_exists('r', $params)) { + $pathOutput = $this->truncate($path, $maxLength); + } else { + $pathOutput = $this->truncate(clean_path($path), $maxLength); + } + $path = realpath($path) ?: $path; $tbody[] = [ diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index f70d07155613..3866ce760a56 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -91,7 +91,7 @@ class AutoloadConfig * @var array */ protected $corePsr4 = [ - 'CodeIgniter' => SYSTEMPATH, + 'CodeIgniter' => [SYSTEMPATH, TESTPATH . 'system'], 'Config' => APPPATH . 'Config', 'Tests' => ROOTPATH . 'tests', ]; @@ -106,7 +106,7 @@ class AutoloadConfig * searched for within one or more directories as they would if they * were being autoloaded through a namespace. * - * @var array + * @var array */ protected $coreClassmap = [ AbstractLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php', diff --git a/tests/system/Commands/Utilities/NamespacesTest.php b/tests/system/Commands/Utilities/NamespacesTest.php index 809a3a64d29d..0785e8d1e6c9 100644 --- a/tests/system/Commands/Utilities/NamespacesTest.php +++ b/tests/system/Commands/Utilities/NamespacesTest.php @@ -58,6 +58,7 @@ public function testNamespacesCommandCodeIgniterOnly(): void | Namespace | Path | Found? | +---------------+-------------------------+--------+ | CodeIgniter | ROOTPATH/system | Yes | + | CodeIgniter | ROOTPATH/tests/system | Yes | | Config | APPPATH/Config | Yes | | Tests | ROOTPATH/tests | Yes | | App | ROOTPATH/app | Yes | diff --git a/user_guide_src/source/cli/cli_generators.rst b/user_guide_src/source/cli/cli_generators.rst index 2f8d7c9d55bf..86f577e7d064 100644 --- a/user_guide_src/source/cli/cli_generators.rst +++ b/user_guide_src/source/cli/cli_generators.rst @@ -248,6 +248,7 @@ Argument: Options: ======== +* ``--namespace``: Set the root namespace. Defaults to value of ``Tests``. * ``--force``: Set this flag to overwrite existing files on destination. make:migration