From 3280a5ded0eb72c3d4056d0c8e32ac6c9d68c5ab Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <40327885+jrushlow@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:15:35 -0400 Subject: [PATCH] feature #1588 [make:controller] generate final controller class * [make:controller] generate final controller class * add generated file assertions * add ability to generate a twig template based off of an absolute class name * generate the controller from the class data method * cleanup makeController and generator * allow creating class data objects with absolute namespaces --- src/Generator.php | 15 +++-- src/Maker/MakeController.php | 67 +++++++++++-------- .../skeleton/controller/Controller.tpl.php | 8 +-- src/Util/ClassSource/Model/ClassData.php | 49 ++++++++++++-- tests/Maker/MakeControllerTest.php | 13 +++- tests/Util/ClassSource/ClassDataTest.php | 52 ++++++++++++++ .../expected/FinalController.php | 19 ++++++ .../expected/FinalControllerWithTemplate.php | 18 +++++ .../tests/it_generates_a_controller.php | 2 +- .../it_generates_a_controller_with_twig.php | 2 +- .../it_generates_an_invokable_controller.php | 2 +- 11 files changed, 202 insertions(+), 45 deletions(-) create mode 100644 tests/fixtures/make-controller/expected/FinalController.php create mode 100644 tests/fixtures/make-controller/expected/FinalControllerWithTemplate.php diff --git a/src/Generator.php b/src/Generator.php index 5f744594c..aa9408311 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -83,12 +83,15 @@ public function generateClass(string $className, string $templateName, array $va * * @param string $templateName Template name in Resources/skeleton to use * @param array $variables Array of variables to pass to the template + * @param bool $isController Set to true if generating a Controller that needs + * access to the TemplateComponentGenerator ("generator") in + * the twig template. e.g. to create route attributes for a route method * * @return string The path where the file will be created * * @throws \Exception */ - final public function generateClassFromClassData(ClassData $classData, string $templateName, array $variables = []): string + final public function generateClassFromClassData(ClassData $classData, string $templateName, array $variables = [], bool $isController = false): string { $classData = $this->templateComponentGenerator->configureClass($classData); $targetPath = $this->fileManager->getRelativePathForFutureClass($classData->getFullClassName()); @@ -97,11 +100,13 @@ final public function generateClassFromClassData(ClassData $classData, string $t throw new \LogicException(\sprintf('Could not determine where to locate the new class "%s", maybe try with a full namespace like "\\My\\Full\\Namespace\\%s"', $classData->getFullClassName(), $classData->getClassName())); } - $variables = array_merge($variables, [ - 'class_data' => $classData, - ]); + $globalTemplateVars = ['class_data' => $classData]; - $this->addOperation($targetPath, $templateName, $variables); + if ($isController) { + $globalTemplateVars['generator'] = $this->templateComponentGenerator; + } + + $this->addOperation($targetPath, $templateName, array_merge($variables, $globalTemplateVars)); return $targetPath; } diff --git a/src/Maker/MakeController.php b/src/Maker/MakeController.php index 4cf84e1f6..429502ae1 100644 --- a/src/Maker/MakeController.php +++ b/src/Maker/MakeController.php @@ -17,8 +17,8 @@ use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassData; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; -use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -67,45 +67,56 @@ public function configureCommand(Command $command, InputConfiguration $inputConf public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { - $controllerClassNameDetails = $generator->createClassNameDetails( - $input->getArgument('controller-class'), - 'Controller\\', - 'Controller' - ); - $withTemplate = $this->isTwigInstalled() && !$input->getOption('no-template'); $isInvokable = (bool) $input->getOption('invokable'); - $useStatements = new UseStatementGenerator([ - AbstractController::class, - $withTemplate ? Response::class : JsonResponse::class, - Route::class, - ]); - - $templateName = Str::asFilePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()) - .($isInvokable ? '.html.twig' : '/index.html.twig'); - - $controllerPath = $generator->generateController( - $controllerClassNameDetails->getFullName(), - 'controller/Controller.tpl.php', - [ - 'use_statements' => $useStatements, - 'route_path' => Str::asRoutePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()), - 'route_name' => Str::asRouteName($controllerClassNameDetails->getRelativeNameWithoutSuffix()), - 'method_name' => $isInvokable ? '__invoke' : 'index', - 'with_template' => $withTemplate, - 'template_name' => $templateName, + $controllerClass = $input->getArgument('controller-class'); + $controllerClassName = \sprintf('Controller\%s', $controllerClass); + + // If the class name provided is absolute, we do not assume it will live in src/Controller + // e.g. src/Custom/Location/For/MyController instead of src/Controller/MyController + if ($isAbsolute = '\\' === $controllerClass[0]) { + $controllerClassName = substr($controllerClass, 1); + } + + $controllerClassData = ClassData::create( + class: $controllerClassName, + suffix: 'Controller', + extendsClass: AbstractController::class, + useStatements: [ + $withTemplate ? Response::class : JsonResponse::class, + Route::class, ] ); + // Again if the class name is absolute, lets not make assumptions about where the twig template + // should live. E.g. templates/custom/location/for/my_controller.html.twig instead of + // templates/my/controller.html.twig. We do however remove the root_namespace prefix in either case + // so we don't end up with templates/app/my/controller.html.twig + $templateName = $isAbsolute ? + $controllerClassData->getFullClassName(withoutRootNamespace: true, withoutSuffix: true) : + $controllerClassData->getClassName(relative: true, withoutSuffix: true) + ; + + // Convert the twig template name into a file path where it will be generated. + $templatePath = \sprintf('%s%s', Str::asFilePath($templateName), $isInvokable ? '.html.twig' : '/index.html.twig'); + + $controllerPath = $generator->generateClassFromClassData($controllerClassData, 'controller/Controller.tpl.php', [ + 'route_path' => Str::asRoutePath($controllerClassData->getClassName(relative: true, withoutSuffix: true)), + 'route_name' => Str::AsRouteName($controllerClassData->getClassName(relative: true, withoutSuffix: true)), + 'method_name' => $isInvokable ? '__invoke' : 'index', + 'with_template' => $withTemplate, + 'template_name' => $templatePath, + ], true); + if ($withTemplate) { $generator->generateTemplate( - $templateName, + $templatePath, 'controller/twig_template.tpl.php', [ 'controller_path' => $controllerPath, 'root_directory' => $generator->getRootDirectory(), - 'class_name' => $controllerClassNameDetails->getShortName(), + 'class_name' => $controllerClassData->getClassName(), ] ); } diff --git a/src/Resources/skeleton/controller/Controller.tpl.php b/src/Resources/skeleton/controller/Controller.tpl.php index 8f509e699..9d9948457 100644 --- a/src/Resources/skeleton/controller/Controller.tpl.php +++ b/src/Resources/skeleton/controller/Controller.tpl.php @@ -1,10 +1,10 @@ -namespace ; +namespace getNamespace(); ?>; - +getUseStatements(); ?> -class extends AbstractController +getClassDeclaration(); ?> { generateRouteForControllerMethod($route_path, $route_name); ?> public function (): ResponseJsonResponse @@ -12,7 +12,7 @@ public function (): Response return $this->render('', [ - 'controller_name' => '', + 'controller_name' => 'getClassName() ?>', ]); return $this->json([ diff --git a/src/Util/ClassSource/Model/ClassData.php b/src/Util/ClassSource/Model/ClassData.php index 988404f2a..4ce0cd605 100644 --- a/src/Util/ClassSource/Model/ClassData.php +++ b/src/Util/ClassSource/Model/ClassData.php @@ -29,7 +29,11 @@ private function __construct( private UseStatementGenerator $useStatementGenerator, private bool $isFinal = true, private string $rootNamespace = 'App', + private ?string $classSuffix = null, ) { + if (str_starts_with(haystack: $this->namespace, needle: $this->rootNamespace)) { + $this->namespace = substr_replace(string: $this->namespace, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1); + } } public static function create(string $class, ?string $suffix = null, ?string $extendsClass = null, bool $isEntity = false, array $useStatements = []): self @@ -52,12 +56,30 @@ className: Str::asClassName($className), extends: null === $extendsClass ? null : Str::getShortClassName($extendsClass), isEntity: $isEntity, useStatementGenerator: $useStatements, + classSuffix: $suffix, ); } - public function getClassName(): string + public function getClassName(bool $relative = false, bool $withoutSuffix = false): string { - return $this->className; + if (!$withoutSuffix && !$relative) { + return $this->className; + } + + if ($relative) { + $class = \sprintf('%s\%s', $this->namespace, $this->className); + + $firstNsSeparatorPosition = stripos($class, '\\'); + $class = substr_replace(string: $class, replace: '', offset: 0, length: $firstNsSeparatorPosition + 1); + + if ($withoutSuffix) { + $class = Str::removeSuffix($class, $this->classSuffix); + } + + return $class; + } + + return Str::removeSuffix($this->className, $this->classSuffix); } public function getNamespace(): string @@ -66,12 +88,31 @@ public function getNamespace(): string return $this->rootNamespace; } + // Namespace is already absolute, don't add the rootNamespace. + if (str_starts_with($this->namespace, '\\')) { + return substr_replace($this->namespace, '', 0, 1); + } + return \sprintf('%s\%s', $this->rootNamespace, $this->namespace); } - public function getFullClassName(): string + /** + * Get the full class name. + * + * @param bool $withoutRootNamespace Get the full class name without global root namespace. e.g. "App" + * @param bool $withoutSuffix Get the full class name without the class suffix. e.g. "MyController" instead of "MyControllerController" + */ + public function getFullClassName($withoutRootNamespace = false, $withoutSuffix = false): string { - return \sprintf('%s\%s', $this->getNamespace(), $this->className); + $className = \sprintf('%s\%s', $this->getNamespace(), $withoutSuffix ? Str::removeSuffix($this->className, $this->classSuffix) : $this->className); + + if ($withoutRootNamespace) { + if (str_starts_with(haystack: $className, needle: $this->rootNamespace)) { + $className = substr_replace(string: $className, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1); + } + } + + return $className; } public function setRootNamespace(string $rootNamespace): self diff --git a/tests/Maker/MakeControllerTest.php b/tests/Maker/MakeControllerTest.php index bce5e787f..8ef1e6713 100644 --- a/tests/Maker/MakeControllerTest.php +++ b/tests/Maker/MakeControllerTest.php @@ -37,8 +37,13 @@ public function getTestDetails(): \Generator ]); $this->assertContainsCount('created: ', $output, 1); - $this->runControllerTest($runner, 'it_generates_a_controller.php'); + + // Ensure the generated controller matches what we expect + self::assertSame( + expected: file_get_contents(\dirname(__DIR__).'/fixtures/make-controller/expected/FinalController.php'), + actual: file_get_contents($runner->getPath('src/Controller/FooBarController.php')) + ); }), ]; @@ -66,6 +71,12 @@ public function getTestDetails(): \Generator self::assertFileExists($controllerPath); $this->runControllerTest($runner, 'it_generates_a_controller_with_twig.php'); + + // Ensure the generated controller matches what we expect + self::assertSame( + expected: file_get_contents(\dirname(__DIR__).'/fixtures/make-controller/expected/FinalControllerWithTemplate.php'), + actual: file_get_contents($runner->getPath('src/Controller/FooTwigController.php')) + ); }), ]; diff --git a/tests/Util/ClassSource/ClassDataTest.php b/tests/Util/ClassSource/ClassDataTest.php index cbad75dec..27d345028 100644 --- a/tests/Util/ClassSource/ClassDataTest.php +++ b/tests/Util/ClassSource/ClassDataTest.php @@ -91,4 +91,56 @@ public function namespaceDataProvider(): \Generator yield ['MyController', 'Maker', 'Maker', 'Maker\MyController']; yield ['Controller\MyController', 'Maker', 'Maker\Controller', 'Maker\Controller\MyController']; } + + public function testGetClassName(): void + { + $class = ClassData::create(class: 'Controller\\Foo', suffix: 'Controller'); + self::assertSame('FooController', $class->getClassName()); + self::assertSame('Foo', $class->getClassName(relative: false, withoutSuffix: true)); + self::assertSame('FooController', $class->getClassName(relative: true, withoutSuffix: false)); + self::assertSame('Foo', $class->getClassName(relative: true, withoutSuffix: true)); + self::assertSame('App\Controller\FooController', $class->getFullClassName()); + } + + public function testGetClassNameRelativeNamespace(): void + { + $class = ClassData::create(class: 'Controller\\Admin\\Foo', suffix: 'Controller'); + self::assertSame('FooController', $class->getClassName()); + self::assertSame('Foo', $class->getClassName(relative: false, withoutSuffix: true)); + self::assertSame('Admin\FooController', $class->getClassName(relative: true, withoutSuffix: false)); + self::assertSame('Admin\Foo', $class->getClassName(relative: true, withoutSuffix: true)); + self::assertSame('App\Controller\Admin\FooController', $class->getFullClassName()); + } + + public function testGetClassNameWithAbsoluteNamespace(): void + { + $class = ClassData::create(class: '\\Foo\\Bar\\Admin\\Baz', suffix: 'Controller'); + self::assertSame('BazController', $class->getClassName()); + self::assertSame('Foo\Bar\Admin', $class->getNamespace()); + self::assertSame('Foo\Bar\Admin\BazController', $class->getFullClassName()); + } + + /** @dataProvider fullClassNameProvider */ + public function testGetFullClassName(string $class, ?string $rootNamespace, bool $withoutRootNamespace, bool $withoutSuffix, string $expectedFullClassName): void + { + $class = ClassData::create($class, suffix: 'Controller'); + + if (null !== $rootNamespace) { + $class->setRootNamespace($rootNamespace); + } + + self::assertSame($expectedFullClassName, $class->getFullClassName(withoutRootNamespace: $withoutRootNamespace, withoutSuffix: $withoutSuffix)); + } + + public function fullClassNameProvider(): \Generator + { + yield ['Controller\MyController', null, false, false, 'App\Controller\MyController']; + yield ['Controller\MyController', null, true, false, 'Controller\MyController']; + yield ['Controller\MyController', null, false, true, 'App\Controller\My']; + yield ['Controller\MyController', null, true, true, 'Controller\My']; + yield ['Controller\MyController', 'Custom', false, false, 'Custom\Controller\MyController']; + yield ['Controller\MyController', 'Custom', true, false, 'Controller\MyController']; + yield ['Controller\MyController', 'Custom', false, true, 'Custom\Controller\My']; + yield ['Controller\MyController', 'Custom', true, true, 'Controller\My']; + } } diff --git a/tests/fixtures/make-controller/expected/FinalController.php b/tests/fixtures/make-controller/expected/FinalController.php new file mode 100644 index 000000000..30dcd8f1b --- /dev/null +++ b/tests/fixtures/make-controller/expected/FinalController.php @@ -0,0 +1,19 @@ +json([ + 'message' => 'Welcome to your new controller!', + 'path' => 'src/Controller/FooBarController.php', + ]); + } +} diff --git a/tests/fixtures/make-controller/expected/FinalControllerWithTemplate.php b/tests/fixtures/make-controller/expected/FinalControllerWithTemplate.php new file mode 100644 index 000000000..0a1c4e566 --- /dev/null +++ b/tests/fixtures/make-controller/expected/FinalControllerWithTemplate.php @@ -0,0 +1,18 @@ +render('foo_twig/index.html.twig', [ + 'controller_name' => 'FooTwigController', + ]); + } +} diff --git a/tests/fixtures/make-controller/tests/it_generates_a_controller.php b/tests/fixtures/make-controller/tests/it_generates_a_controller.php index 8884cd1fa..f0a6a5651 100644 --- a/tests/fixtures/make-controller/tests/it_generates_a_controller.php +++ b/tests/fixtures/make-controller/tests/it_generates_a_controller.php @@ -4,7 +4,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class GeneratedControllerTest extends WebTestCase +final class GeneratedControllerTest extends WebTestCase { public function testController() { diff --git a/tests/fixtures/make-controller/tests/it_generates_a_controller_with_twig.php b/tests/fixtures/make-controller/tests/it_generates_a_controller_with_twig.php index c21887ea1..e3980fe57 100644 --- a/tests/fixtures/make-controller/tests/it_generates_a_controller_with_twig.php +++ b/tests/fixtures/make-controller/tests/it_generates_a_controller_with_twig.php @@ -4,7 +4,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class GeneratedControllerTest extends WebTestCase +final class GeneratedControllerTest extends WebTestCase { public function testController() { diff --git a/tests/fixtures/make-controller/tests/it_generates_an_invokable_controller.php b/tests/fixtures/make-controller/tests/it_generates_an_invokable_controller.php index ed26ac147..6e6a80a22 100644 --- a/tests/fixtures/make-controller/tests/it_generates_an_invokable_controller.php +++ b/tests/fixtures/make-controller/tests/it_generates_an_invokable_controller.php @@ -4,7 +4,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class GeneratedControllerTest extends WebTestCase +final class GeneratedControllerTest extends WebTestCase { public function testControllerValidity() {