diff --git a/.gitignore b/.gitignore index 2e460e09..595f3463 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,5 @@ /.idea/ /.vscode/ /vendor/ -/TestModule/ .phpunit.* .php_cs.cache diff --git a/composer.json b/composer.json index 6f886758..371a9be5 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ } ], "require": { + "ext-json": "*", "php": ">=7.4" }, "require-dev": { @@ -39,12 +40,14 @@ "scripts": { "test-all": [ "@test-quality", + "@test-unit", "@test-integration" ], "test-quality": [ "@csrun", "@psalm" ], + "test-unit": "./vendor/bin/phpunit --testsuite=unit", "test-integration": "./vendor/bin/phpunit --testsuite=integration", "test-coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --testsuite=integration --coverage-html=coverage", "psalm": "./vendor/bin/psalm", diff --git a/phpunit.xml b/phpunit.xml index 988a7b7a..7340e195 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,6 +18,10 @@ + + tests/Unit + + tests/Integration diff --git a/src/CodeGenerator/CodeGeneratorConfig.php b/src/CodeGenerator/CodeGeneratorConfig.php index 01f70751..23cd6858 100644 --- a/src/CodeGenerator/CodeGeneratorConfig.php +++ b/src/CodeGenerator/CodeGeneratorConfig.php @@ -5,6 +5,7 @@ namespace Gacela\CodeGenerator; use Gacela\Framework\AbstractConfig; +use Gacela\Framework\Config; final class CodeGeneratorConfig extends AbstractConfig { @@ -32,4 +33,14 @@ private function getCommandTemplateContent(string $filename): string { return file_get_contents(__DIR__ . '/Infrastructure/Template/Command/' . $filename); } + + public function getComposerJsonContentAsArray(): array + { + $filename = Config::getApplicationRootDir() . '/composer.json'; + if (!file_exists($filename)) { + return []; + } + + return json_decode(file_get_contents($filename), true); + } } diff --git a/src/CodeGenerator/CodeGeneratorFactory.php b/src/CodeGenerator/CodeGeneratorFactory.php index 791d7909..3606e56e 100644 --- a/src/CodeGenerator/CodeGeneratorFactory.php +++ b/src/CodeGenerator/CodeGeneratorFactory.php @@ -71,6 +71,8 @@ private function createGeneratorIo(): MakerIoInterface public function createCommandArgumentsParser(): CommandArgumentsParser { - return new CommandArgumentsParser(); + return new CommandArgumentsParser( + $this->getConfig()->getComposerJsonContentAsArray() + ); } } diff --git a/src/CodeGenerator/Domain/Command/AbstractMaker.php b/src/CodeGenerator/Domain/Command/AbstractMaker.php index 52d4719b..66dccada 100644 --- a/src/CodeGenerator/Domain/Command/AbstractMaker.php +++ b/src/CodeGenerator/Domain/Command/AbstractMaker.php @@ -20,13 +20,10 @@ public function __construct(MakerIoInterface $io, string $template) public function make(CommandArguments $commandArguments): void { - $pieces = explode('/', $commandArguments->targetDirectory()); - $moduleName = end($pieces); + $this->io->createDirectory($commandArguments->directory()); - $this->io->createDirectory($commandArguments->targetDirectory()); - - $path = sprintf('%s/%s.php', $commandArguments->targetDirectory(), $this->className()); - $this->io->filePutContents($path, $this->generateFileContent("{$commandArguments->rootNamespace()}\\$moduleName")); + $path = sprintf('%s/%s.php', $commandArguments->directory(), $this->className()); + $this->io->filePutContents($path, $this->generateFileContent($commandArguments->namespace())); $this->io->writeln("> Path '$path' created successfully"); } diff --git a/src/CodeGenerator/Domain/Command/ModuleMaker.php b/src/CodeGenerator/Domain/Command/ModuleMaker.php index 62387a6a..479d30ce 100644 --- a/src/CodeGenerator/Domain/Command/ModuleMaker.php +++ b/src/CodeGenerator/Domain/Command/ModuleMaker.php @@ -29,7 +29,7 @@ public function make(CommandArguments $commandArguments): void $generator->make($commandArguments); } - $pieces = explode('/', $commandArguments->targetDirectory()); + $pieces = explode('/', $commandArguments->directory()); $moduleName = end($pieces); $this->io->writeln("Module $moduleName created successfully"); } diff --git a/src/CodeGenerator/Domain/Io/CommandArgumentsParser.php b/src/CodeGenerator/Domain/Io/CommandArgumentsParser.php index a519d786..0e4653bc 100644 --- a/src/CodeGenerator/Domain/Io/CommandArgumentsParser.php +++ b/src/CodeGenerator/Domain/Io/CommandArgumentsParser.php @@ -6,24 +6,77 @@ use Gacela\CodeGenerator\Domain\ReadModel\CommandArguments; use InvalidArgumentException; +use LogicException; final class CommandArgumentsParser { + private array $composerJson; + + public function __construct(array $composerJson) + { + $this->composerJson = $composerJson; + } + /** * @throws InvalidArgumentException */ public function parse(array $arguments): CommandArguments { - [$rootNamespace, $targetDirectory] = array_pad($arguments, 2, null); + if (empty($arguments)) { + throw new InvalidArgumentException('Expected argument to be the location of the new module. For example: App/TestModule'); + } + + return $this->createCommandArguments($arguments[0]); + } + + private function createCommandArguments(string $desiredNamespace): CommandArguments + { + $psr4 = $this->composerJson['autoload']['psr-4']; + $allPsr4Combinations = $this->allPossiblePsr4Combinations($desiredNamespace); - if ($rootNamespace === null) { - throw new InvalidArgumentException('Expected 1st argument to be root-namespace of the project'); + foreach ($allPsr4Combinations as $psr4Combination) { + $psr4Key = $psr4Combination . '\\'; + if (isset($psr4[$psr4Key])) { + return $this->foundPsr4($psr4Key, $psr4[$psr4Key], $desiredNamespace); + } } - if ($targetDirectory === null) { - throw new InvalidArgumentException('Expected 2nd argument to be target-directory inside the project'); + throw new LogicException('No autoload psr-4 match found for ' . $desiredNamespace); + } + + /** + * Combine all possible psr-4 combinations and return them ordered by longer to shorter. + * This way we'll be able to find the longer match first. + * For example: App/TestModule/TestSubModule will produce an array such as: + * [ + * 'App/TestModule/TestSubModule', + * 'App/TestModule', + * 'App', + * ]. + */ + private function allPossiblePsr4Combinations(string $desiredNamespace): array + { + $result = []; + + foreach (explode('/', $desiredNamespace) as $explodedArg) { + if (empty($result)) { + $result[] = $explodedArg; + } else { + $prevValue = $result[count($result) - 1]; + $result[] = $prevValue . '\\' . $explodedArg; + } } - return new CommandArguments($rootNamespace, $targetDirectory); + return array_reverse($result); + } + + private function foundPsr4(string $psr4Key, string $psr4Value, string $desiredNamespace): CommandArguments + { + $rootDir = substr($psr4Value, 0, -1); + $rootNamespace = substr($psr4Key, 0, -1); + $targetDirectory = str_replace(['/', $rootNamespace, '\\'], ['\\', $rootDir, '/'], $desiredNamespace); + $namespace = str_replace([$rootDir, '/'], [$rootNamespace, '\\'], $targetDirectory); + + return new CommandArguments($namespace, $targetDirectory); } } diff --git a/src/CodeGenerator/Domain/ReadModel/CommandArguments.php b/src/CodeGenerator/Domain/ReadModel/CommandArguments.php index 24b47495..62f8abfb 100644 --- a/src/CodeGenerator/Domain/ReadModel/CommandArguments.php +++ b/src/CodeGenerator/Domain/ReadModel/CommandArguments.php @@ -6,22 +6,22 @@ final class CommandArguments { - private string $rootNamespace; - private string $targetDirectory; + private string $namespace; + private string $directory; - public function __construct(string $rootNamespace, string $targetDirectory) + public function __construct(string $namespace, string $directory) { - $this->rootNamespace = $rootNamespace; - $this->targetDirectory = $targetDirectory; + $this->namespace = $namespace; + $this->directory = $directory; } - public function rootNamespace(): string + public function namespace(): string { - return $this->rootNamespace; + return $this->namespace; } - public function targetDirectory(): string + public function directory(): string { - return $this->targetDirectory; + return $this->directory; } } diff --git a/tests/Integration/CodeGenerator/UsingIncorrectConfigurationTest.php b/tests/Integration/CodeGenerator/UsingIncorrectConfigurationTest.php index 158bfb08..0dcf17d9 100644 --- a/tests/Integration/CodeGenerator/UsingIncorrectConfigurationTest.php +++ b/tests/Integration/CodeGenerator/UsingIncorrectConfigurationTest.php @@ -5,32 +5,37 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use InvalidArgumentException; +use LogicException; use PHPUnit\Framework\TestCase; final class UsingIncorrectConfigurationTest extends TestCase { - public function test_make_unknown_command(): void + public function setUp(): void { - $this->expectException(InvalidArgumentException::class); - - $codeGeneratorConfig = new CodeGeneratorFacade(); - $codeGeneratorConfig->runCommand('make:unknown', [__NAMESPACE__, __DIR__ . '/Generated']); + Config::setApplicationRootDir(__DIR__); } - public function test_missing_target_directory(): void + public function test_make_unknown_command(): void { $this->expectException(InvalidArgumentException::class); - $codeGeneratorConfig = new CodeGeneratorFacade(); - $codeGeneratorConfig->runCommand('', [__NAMESPACE__]); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:unknown', ['GacelaTest/Integration/CodeGenerator/Generated']); } - public function test_missing_root_namespace_and_target_directory(): void + public function test_missing_target(): void { $this->expectException(InvalidArgumentException::class); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:module', []); + } - $codeGeneratorConfig = new CodeGeneratorFacade(); - $codeGeneratorConfig->runCommand('', []); + public function test_unknown_target(): void + { + $this->expectException(LogicException::class); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:module', ['UnknownNamespace']); } } diff --git a/tests/Integration/CodeGenerator/UsingMakeConfigTest.php b/tests/Integration/CodeGenerator/UsingMakeConfigTest.php index 94eb78ea..1d59b8b6 100644 --- a/tests/Integration/CodeGenerator/UsingMakeConfigTest.php +++ b/tests/Integration/CodeGenerator/UsingMakeConfigTest.php @@ -5,17 +5,23 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use GacelaTest\Integration\CodeGenerator\Util\DirectoryUtil; use PHPUnit\Framework\TestCase; final class UsingMakeConfigTest extends TestCase { + public function setUp(): void + { + Config::setApplicationRootDir(__DIR__); + } + public function test_make_config(): void { self::assertFileDoesNotExist(__DIR__ . '/Generated/Config.php'); - $codeGeneratorConfig = new CodeGeneratorFacade(); - $codeGeneratorConfig->runCommand('make:config', [__NAMESPACE__, __DIR__ . '/Generated']); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:config', ['GacelaTest/Integration/CodeGenerator/Generated']); $this->expectOutputRegex("~/Generated/Config.php' created successfully~"); self::assertFileExists(__DIR__ . '/Generated/Config.php'); diff --git a/tests/Integration/CodeGenerator/UsingMakeDependencyProviderTest.php b/tests/Integration/CodeGenerator/UsingMakeDependencyProviderTest.php index 44be231d..a67422c8 100644 --- a/tests/Integration/CodeGenerator/UsingMakeDependencyProviderTest.php +++ b/tests/Integration/CodeGenerator/UsingMakeDependencyProviderTest.php @@ -5,17 +5,23 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use GacelaTest\Integration\CodeGenerator\Util\DirectoryUtil; use PHPUnit\Framework\TestCase; final class UsingMakeDependencyProviderTest extends TestCase { + public function setUp(): void + { + Config::setApplicationRootDir(__DIR__); + } + public function test_make_dependency_provider(): void { self::assertFileDoesNotExist(__DIR__ . '/Generated/DependencyProvider.php'); - $codeGeneratorDependencyProvider = new CodeGeneratorFacade(); - $codeGeneratorDependencyProvider->runCommand('make:dependency-provider', [__NAMESPACE__, __DIR__ . '/Generated']); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:dependency-provider', ['GacelaTest/Integration/CodeGenerator/Generated']); $this->expectOutputRegex("~/Generated/DependencyProvider.php' created successfully~"); self::assertFileExists(__DIR__ . '/Generated/DependencyProvider.php'); diff --git a/tests/Integration/CodeGenerator/UsingMakeFacadeTest.php b/tests/Integration/CodeGenerator/UsingMakeFacadeTest.php index e524652b..7789ca6d 100644 --- a/tests/Integration/CodeGenerator/UsingMakeFacadeTest.php +++ b/tests/Integration/CodeGenerator/UsingMakeFacadeTest.php @@ -5,17 +5,23 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use GacelaTest\Integration\CodeGenerator\Util\DirectoryUtil; use PHPUnit\Framework\TestCase; final class UsingMakeFacadeTest extends TestCase { + public function setUp(): void + { + Config::setApplicationRootDir(__DIR__); + } + public function test_make_facade(): void { self::assertFileDoesNotExist(__DIR__ . '/Generated/Facade.php'); - $codeGeneratorFacade = new CodeGeneratorFacade(); - $codeGeneratorFacade->runCommand('make:facade', [__NAMESPACE__, __DIR__ . '/Generated']); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:facade', ['GacelaTest/Integration/CodeGenerator/Generated']); $this->expectOutputRegex("~/Generated/Facade.php' created successfully~"); self::assertFileExists(__DIR__ . '/Generated/Facade.php'); diff --git a/tests/Integration/CodeGenerator/UsingMakeFactoryTest.php b/tests/Integration/CodeGenerator/UsingMakeFactoryTest.php index 400c8c9d..98b1c292 100644 --- a/tests/Integration/CodeGenerator/UsingMakeFactoryTest.php +++ b/tests/Integration/CodeGenerator/UsingMakeFactoryTest.php @@ -5,21 +5,27 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use GacelaTest\Integration\CodeGenerator\Util\DirectoryUtil; use PHPUnit\Framework\TestCase; final class UsingMakeFactoryTest extends TestCase { + public function setUp(): void + { + Config::setApplicationRootDir(__DIR__); + } + public function test_make_factory(): void { self::assertFileDoesNotExist(__DIR__ . '/Generated/Factory.php'); - $codeGeneratorFactory = new CodeGeneratorFacade(); - $codeGeneratorFactory->runCommand('make:factory', [__NAMESPACE__, __DIR__ . '/Generated']); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:factory', ['GacelaTest/Integration/CodeGenerator/Generated']); $this->expectOutputRegex("~/Generated/Factory.php' created successfully~"); self::assertFileExists(__DIR__ . '/Generated/Factory.php'); - DirectoryUtil::removeDir(__DIR__ . '/Generated'); + DirectoryUtil::removeDir(__DIR__ . '/Generated/'); } } diff --git a/tests/Integration/CodeGenerator/UsingMakeModuleTest.php b/tests/Integration/CodeGenerator/UsingMakeModuleTest.php index 92437a56..48111f7b 100644 --- a/tests/Integration/CodeGenerator/UsingMakeModuleTest.php +++ b/tests/Integration/CodeGenerator/UsingMakeModuleTest.php @@ -5,17 +5,23 @@ namespace GacelaTest\Integration\CodeGenerator; use Gacela\CodeGenerator\CodeGeneratorFacade; +use Gacela\Framework\Config; use GacelaTest\Integration\CodeGenerator\Util\DirectoryUtil; use PHPUnit\Framework\TestCase; final class UsingMakeModuleTest extends TestCase { + public function setUp(): void + { + Config::setApplicationRootDir(__DIR__); + } + public function test_make_module(): void { self::assertFileDoesNotExist(__DIR__ . '/Generated/Module.php'); - $codeGeneratorModule = new CodeGeneratorFacade(); - $codeGeneratorModule->runCommand('make:module', [__NAMESPACE__, __DIR__ . '/Generated']); + $facade = new CodeGeneratorFacade(); + $facade->runCommand('make:module', ['GacelaTest/Integration/CodeGenerator/Generated']); $this->expectOutputRegex("~/Generated/Facade.php' created successfully~"); $this->expectOutputRegex("~/Generated/Factory.php' created successfully~"); diff --git a/tests/Integration/CodeGenerator/composer.json b/tests/Integration/CodeGenerator/composer.json new file mode 100644 index 00000000..f5a6858e --- /dev/null +++ b/tests/Integration/CodeGenerator/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "GacelaTest\\": "tests/" + } + } +} diff --git a/tests/Integration/Framework/UsingConfig/LocalConfig/Config.php b/tests/Integration/Framework/UsingConfig/LocalConfig/Config.php index a6c12871..5533ac41 100644 --- a/tests/Integration/Framework/UsingConfig/LocalConfig/Config.php +++ b/tests/Integration/Framework/UsingConfig/LocalConfig/Config.php @@ -11,9 +11,9 @@ final class Config extends AbstractConfig public function getArrayConfig(): array { return [ - 'config' => (int) $this->get('config'), - 'config_local' => (int) $this->get('config_local'), - 'override' => (int) $this->get('override'), + 'config' => (int)$this->get('config'), + 'config_local' => (int)$this->get('config_local'), + 'override' => (int)$this->get('override'), ]; } } diff --git a/tests/Unit/CodeGenerator/Io/CommandArgumentsParserTest.php b/tests/Unit/CodeGenerator/Io/CommandArgumentsParserTest.php new file mode 100644 index 00000000..aa331bec --- /dev/null +++ b/tests/Unit/CodeGenerator/Io/CommandArgumentsParserTest.php @@ -0,0 +1,105 @@ +exampleOneLevelComposerJson()); + $args = $parser->parse($arguments); + + self::assertSame($expected, $args->namespace()); + } + + public function providerOneLevelRootNamespace(): Generator + { + yield 'One level composer psr-4 and one level namespace' => [ + 'arguments' => ['App/TestModule'], + 'expected' => 'App\TestModule', + ]; + + yield 'One level composer psr-4 but multiple level namespace' => [ + 'arguments' => ['App/TestModule/TestSubModule'], + 'expected' => 'App\TestModule\TestSubModule', + ]; + } + + /** + * @dataProvider providerOneLevelTargetDirectory + */ + public function test_parse_one_level_target_directory(array $arguments, string $expected): void + { + $parser = new CommandArgumentsParser($this->exampleOneLevelComposerJson()); + $args = $parser->parse($arguments); + + self::assertSame($expected, $args->directory()); + } + + public function providerOneLevelTargetDirectory(): Generator + { + yield 'One level composer psr-4 and one level namespace' => [ + 'arguments' => ['App/TestModule'], + 'expected' => 'src/TestModule', + ]; + + yield 'One level composer psr-4 but multiple level namespace' => [ + 'arguments' => ['App/TestModule/TestSubModule'], + 'expected' => 'src/TestModule/TestSubModule', + ]; + } + + private function exampleOneLevelComposerJson(): array + { + $composerJson = <<<'JSON' +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} +JSON; + return json_decode($composerJson, true); + } + + public function test_parse_multilevel_root_namespace(): void + { + $parser = new CommandArgumentsParser($this->exampleMultiLevelComposerJson()); + $args = $parser->parse(['App/TestModule/TestSubModule']); + + self::assertSame('App\TestModule\TestSubModule', $args->namespace()); + } + + public function test_parse_multilevel_target_directory(): void + { + $parser = new CommandArgumentsParser($this->exampleMultiLevelComposerJson()); + $args = $parser->parse(['App/TestModule/TestSubModule']); + + self::assertSame('src/TestSubModule', $args->directory()); + } + + private function exampleMultiLevelComposerJson(): array + { + $composerJson = <<<'JSON' +{ + "autoload": { + "psr-4": { + "App\\TestModule\\": "src/" + } + } +} +JSON; + return json_decode($composerJson, true); + } +}