Skip to content

Commit 3280a5d

Browse files
authored
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
1 parent d0cfae6 commit 3280a5d

File tree

11 files changed

+202
-45
lines changed

11 files changed

+202
-45
lines changed

src/Generator.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,15 @@ public function generateClass(string $className, string $templateName, array $va
8383
*
8484
* @param string $templateName Template name in Resources/skeleton to use
8585
* @param array $variables Array of variables to pass to the template
86+
* @param bool $isController Set to true if generating a Controller that needs
87+
* access to the TemplateComponentGenerator ("generator") in
88+
* the twig template. e.g. to create route attributes for a route method
8689
*
8790
* @return string The path where the file will be created
8891
*
8992
* @throws \Exception
9093
*/
91-
final public function generateClassFromClassData(ClassData $classData, string $templateName, array $variables = []): string
94+
final public function generateClassFromClassData(ClassData $classData, string $templateName, array $variables = [], bool $isController = false): string
9295
{
9396
$classData = $this->templateComponentGenerator->configureClass($classData);
9497
$targetPath = $this->fileManager->getRelativePathForFutureClass($classData->getFullClassName());
@@ -97,11 +100,13 @@ final public function generateClassFromClassData(ClassData $classData, string $t
97100
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()));
98101
}
99102

100-
$variables = array_merge($variables, [
101-
'class_data' => $classData,
102-
]);
103+
$globalTemplateVars = ['class_data' => $classData];
103104

104-
$this->addOperation($targetPath, $templateName, $variables);
105+
if ($isController) {
106+
$globalTemplateVars['generator'] = $this->templateComponentGenerator;
107+
}
108+
109+
$this->addOperation($targetPath, $templateName, array_merge($variables, $globalTemplateVars));
105110

106111
return $targetPath;
107112
}

src/Maker/MakeController.php

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
use Symfony\Bundle\MakerBundle\Generator;
1818
use Symfony\Bundle\MakerBundle\InputConfiguration;
1919
use Symfony\Bundle\MakerBundle\Str;
20+
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassData;
2021
use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;
21-
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
2222
use Symfony\Bundle\TwigBundle\TwigBundle;
2323
use Symfony\Component\Console\Command\Command;
2424
use Symfony\Component\Console\Input\InputArgument;
@@ -67,45 +67,56 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
6767

6868
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
6969
{
70-
$controllerClassNameDetails = $generator->createClassNameDetails(
71-
$input->getArgument('controller-class'),
72-
'Controller\\',
73-
'Controller'
74-
);
75-
7670
$withTemplate = $this->isTwigInstalled() && !$input->getOption('no-template');
7771
$isInvokable = (bool) $input->getOption('invokable');
7872

79-
$useStatements = new UseStatementGenerator([
80-
AbstractController::class,
81-
$withTemplate ? Response::class : JsonResponse::class,
82-
Route::class,
83-
]);
84-
85-
$templateName = Str::asFilePath($controllerClassNameDetails->getRelativeNameWithoutSuffix())
86-
.($isInvokable ? '.html.twig' : '/index.html.twig');
87-
88-
$controllerPath = $generator->generateController(
89-
$controllerClassNameDetails->getFullName(),
90-
'controller/Controller.tpl.php',
91-
[
92-
'use_statements' => $useStatements,
93-
'route_path' => Str::asRoutePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()),
94-
'route_name' => Str::asRouteName($controllerClassNameDetails->getRelativeNameWithoutSuffix()),
95-
'method_name' => $isInvokable ? '__invoke' : 'index',
96-
'with_template' => $withTemplate,
97-
'template_name' => $templateName,
73+
$controllerClass = $input->getArgument('controller-class');
74+
$controllerClassName = \sprintf('Controller\%s', $controllerClass);
75+
76+
// If the class name provided is absolute, we do not assume it will live in src/Controller
77+
// e.g. src/Custom/Location/For/MyController instead of src/Controller/MyController
78+
if ($isAbsolute = '\\' === $controllerClass[0]) {
79+
$controllerClassName = substr($controllerClass, 1);
80+
}
81+
82+
$controllerClassData = ClassData::create(
83+
class: $controllerClassName,
84+
suffix: 'Controller',
85+
extendsClass: AbstractController::class,
86+
useStatements: [
87+
$withTemplate ? Response::class : JsonResponse::class,
88+
Route::class,
9889
]
9990
);
10091

92+
// Again if the class name is absolute, lets not make assumptions about where the twig template
93+
// should live. E.g. templates/custom/location/for/my_controller.html.twig instead of
94+
// templates/my/controller.html.twig. We do however remove the root_namespace prefix in either case
95+
// so we don't end up with templates/app/my/controller.html.twig
96+
$templateName = $isAbsolute ?
97+
$controllerClassData->getFullClassName(withoutRootNamespace: true, withoutSuffix: true) :
98+
$controllerClassData->getClassName(relative: true, withoutSuffix: true)
99+
;
100+
101+
// Convert the twig template name into a file path where it will be generated.
102+
$templatePath = \sprintf('%s%s', Str::asFilePath($templateName), $isInvokable ? '.html.twig' : '/index.html.twig');
103+
104+
$controllerPath = $generator->generateClassFromClassData($controllerClassData, 'controller/Controller.tpl.php', [
105+
'route_path' => Str::asRoutePath($controllerClassData->getClassName(relative: true, withoutSuffix: true)),
106+
'route_name' => Str::AsRouteName($controllerClassData->getClassName(relative: true, withoutSuffix: true)),
107+
'method_name' => $isInvokable ? '__invoke' : 'index',
108+
'with_template' => $withTemplate,
109+
'template_name' => $templatePath,
110+
], true);
111+
101112
if ($withTemplate) {
102113
$generator->generateTemplate(
103-
$templateName,
114+
$templatePath,
104115
'controller/twig_template.tpl.php',
105116
[
106117
'controller_path' => $controllerPath,
107118
'root_directory' => $generator->getRootDirectory(),
108-
'class_name' => $controllerClassNameDetails->getShortName(),
119+
'class_name' => $controllerClassData->getClassName(),
109120
]
110121
);
111122
}

src/Resources/skeleton/controller/Controller.tpl.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<?= "<?php\n" ?>
22

3-
namespace <?= $namespace; ?>;
3+
namespace <?= $class_data->getNamespace(); ?>;
44

5-
<?= $use_statements; ?>
5+
<?= $class_data->getUseStatements(); ?>
66

7-
class <?= $class_name; ?> extends AbstractController
7+
<?= $class_data->getClassDeclaration(); ?>
88
{
99
<?= $generator->generateRouteForControllerMethod($route_path, $route_name); ?>
1010
public function <?= $method_name ?>(): <?php if ($with_template) { ?>Response<?php } else { ?>JsonResponse<?php } ?>
1111

1212
{
1313
<?php if ($with_template) { ?>
1414
return $this->render('<?= $template_name ?>', [
15-
'controller_name' => '<?= $class_name ?>',
15+
'controller_name' => '<?= $class_data->getClassName() ?>',
1616
]);
1717
<?php } else { ?>
1818
return $this->json([

src/Util/ClassSource/Model/ClassData.php

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ private function __construct(
2929
private UseStatementGenerator $useStatementGenerator,
3030
private bool $isFinal = true,
3131
private string $rootNamespace = 'App',
32+
private ?string $classSuffix = null,
3233
) {
34+
if (str_starts_with(haystack: $this->namespace, needle: $this->rootNamespace)) {
35+
$this->namespace = substr_replace(string: $this->namespace, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1);
36+
}
3337
}
3438

3539
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),
5256
extends: null === $extendsClass ? null : Str::getShortClassName($extendsClass),
5357
isEntity: $isEntity,
5458
useStatementGenerator: $useStatements,
59+
classSuffix: $suffix,
5560
);
5661
}
5762

58-
public function getClassName(): string
63+
public function getClassName(bool $relative = false, bool $withoutSuffix = false): string
5964
{
60-
return $this->className;
65+
if (!$withoutSuffix && !$relative) {
66+
return $this->className;
67+
}
68+
69+
if ($relative) {
70+
$class = \sprintf('%s\%s', $this->namespace, $this->className);
71+
72+
$firstNsSeparatorPosition = stripos($class, '\\');
73+
$class = substr_replace(string: $class, replace: '', offset: 0, length: $firstNsSeparatorPosition + 1);
74+
75+
if ($withoutSuffix) {
76+
$class = Str::removeSuffix($class, $this->classSuffix);
77+
}
78+
79+
return $class;
80+
}
81+
82+
return Str::removeSuffix($this->className, $this->classSuffix);
6183
}
6284

6385
public function getNamespace(): string
@@ -66,12 +88,31 @@ public function getNamespace(): string
6688
return $this->rootNamespace;
6789
}
6890

91+
// Namespace is already absolute, don't add the rootNamespace.
92+
if (str_starts_with($this->namespace, '\\')) {
93+
return substr_replace($this->namespace, '', 0, 1);
94+
}
95+
6996
return \sprintf('%s\%s', $this->rootNamespace, $this->namespace);
7097
}
7198

72-
public function getFullClassName(): string
99+
/**
100+
* Get the full class name.
101+
*
102+
* @param bool $withoutRootNamespace Get the full class name without global root namespace. e.g. "App"
103+
* @param bool $withoutSuffix Get the full class name without the class suffix. e.g. "MyController" instead of "MyControllerController"
104+
*/
105+
public function getFullClassName($withoutRootNamespace = false, $withoutSuffix = false): string
73106
{
74-
return \sprintf('%s\%s', $this->getNamespace(), $this->className);
107+
$className = \sprintf('%s\%s', $this->getNamespace(), $withoutSuffix ? Str::removeSuffix($this->className, $this->classSuffix) : $this->className);
108+
109+
if ($withoutRootNamespace) {
110+
if (str_starts_with(haystack: $className, needle: $this->rootNamespace)) {
111+
$className = substr_replace(string: $className, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1);
112+
}
113+
}
114+
115+
return $className;
75116
}
76117

77118
public function setRootNamespace(string $rootNamespace): self

tests/Maker/MakeControllerTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ public function getTestDetails(): \Generator
3737
]);
3838

3939
$this->assertContainsCount('created: ', $output, 1);
40-
4140
$this->runControllerTest($runner, 'it_generates_a_controller.php');
41+
42+
// Ensure the generated controller matches what we expect
43+
self::assertSame(
44+
expected: file_get_contents(\dirname(__DIR__).'/fixtures/make-controller/expected/FinalController.php'),
45+
actual: file_get_contents($runner->getPath('src/Controller/FooBarController.php'))
46+
);
4247
}),
4348
];
4449

@@ -66,6 +71,12 @@ public function getTestDetails(): \Generator
6671
self::assertFileExists($controllerPath);
6772

6873
$this->runControllerTest($runner, 'it_generates_a_controller_with_twig.php');
74+
75+
// Ensure the generated controller matches what we expect
76+
self::assertSame(
77+
expected: file_get_contents(\dirname(__DIR__).'/fixtures/make-controller/expected/FinalControllerWithTemplate.php'),
78+
actual: file_get_contents($runner->getPath('src/Controller/FooTwigController.php'))
79+
);
6980
}),
7081
];
7182

tests/Util/ClassSource/ClassDataTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,56 @@ public function namespaceDataProvider(): \Generator
9191
yield ['MyController', 'Maker', 'Maker', 'Maker\MyController'];
9292
yield ['Controller\MyController', 'Maker', 'Maker\Controller', 'Maker\Controller\MyController'];
9393
}
94+
95+
public function testGetClassName(): void
96+
{
97+
$class = ClassData::create(class: 'Controller\\Foo', suffix: 'Controller');
98+
self::assertSame('FooController', $class->getClassName());
99+
self::assertSame('Foo', $class->getClassName(relative: false, withoutSuffix: true));
100+
self::assertSame('FooController', $class->getClassName(relative: true, withoutSuffix: false));
101+
self::assertSame('Foo', $class->getClassName(relative: true, withoutSuffix: true));
102+
self::assertSame('App\Controller\FooController', $class->getFullClassName());
103+
}
104+
105+
public function testGetClassNameRelativeNamespace(): void
106+
{
107+
$class = ClassData::create(class: 'Controller\\Admin\\Foo', suffix: 'Controller');
108+
self::assertSame('FooController', $class->getClassName());
109+
self::assertSame('Foo', $class->getClassName(relative: false, withoutSuffix: true));
110+
self::assertSame('Admin\FooController', $class->getClassName(relative: true, withoutSuffix: false));
111+
self::assertSame('Admin\Foo', $class->getClassName(relative: true, withoutSuffix: true));
112+
self::assertSame('App\Controller\Admin\FooController', $class->getFullClassName());
113+
}
114+
115+
public function testGetClassNameWithAbsoluteNamespace(): void
116+
{
117+
$class = ClassData::create(class: '\\Foo\\Bar\\Admin\\Baz', suffix: 'Controller');
118+
self::assertSame('BazController', $class->getClassName());
119+
self::assertSame('Foo\Bar\Admin', $class->getNamespace());
120+
self::assertSame('Foo\Bar\Admin\BazController', $class->getFullClassName());
121+
}
122+
123+
/** @dataProvider fullClassNameProvider */
124+
public function testGetFullClassName(string $class, ?string $rootNamespace, bool $withoutRootNamespace, bool $withoutSuffix, string $expectedFullClassName): void
125+
{
126+
$class = ClassData::create($class, suffix: 'Controller');
127+
128+
if (null !== $rootNamespace) {
129+
$class->setRootNamespace($rootNamespace);
130+
}
131+
132+
self::assertSame($expectedFullClassName, $class->getFullClassName(withoutRootNamespace: $withoutRootNamespace, withoutSuffix: $withoutSuffix));
133+
}
134+
135+
public function fullClassNameProvider(): \Generator
136+
{
137+
yield ['Controller\MyController', null, false, false, 'App\Controller\MyController'];
138+
yield ['Controller\MyController', null, true, false, 'Controller\MyController'];
139+
yield ['Controller\MyController', null, false, true, 'App\Controller\My'];
140+
yield ['Controller\MyController', null, true, true, 'Controller\My'];
141+
yield ['Controller\MyController', 'Custom', false, false, 'Custom\Controller\MyController'];
142+
yield ['Controller\MyController', 'Custom', true, false, 'Controller\MyController'];
143+
yield ['Controller\MyController', 'Custom', false, true, 'Custom\Controller\My'];
144+
yield ['Controller\MyController', 'Custom', true, true, 'Controller\My'];
145+
}
94146
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\JsonResponse;
7+
use Symfony\Component\Routing\Attribute\Route;
8+
9+
final class FooBarController extends AbstractController
10+
{
11+
#[Route('/foo/bar', name: 'app_foo_bar')]
12+
public function index(): JsonResponse
13+
{
14+
return $this->json([
15+
'message' => 'Welcome to your new controller!',
16+
'path' => 'src/Controller/FooBarController.php',
17+
]);
18+
}
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\Routing\Attribute\Route;
8+
9+
final class FooTwigController extends AbstractController
10+
{
11+
#[Route('/foo/twig', name: 'app_foo_twig')]
12+
public function index(): Response
13+
{
14+
return $this->render('foo_twig/index.html.twig', [
15+
'controller_name' => 'FooTwigController',
16+
]);
17+
}
18+
}

tests/fixtures/make-controller/tests/it_generates_a_controller.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
66

7-
class GeneratedControllerTest extends WebTestCase
7+
final class GeneratedControllerTest extends WebTestCase
88
{
99
public function testController()
1010
{

tests/fixtures/make-controller/tests/it_generates_a_controller_with_twig.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
66

7-
class GeneratedControllerTest extends WebTestCase
7+
final class GeneratedControllerTest extends WebTestCase
88
{
99
public function testController()
1010
{

tests/fixtures/make-controller/tests/it_generates_an_invokable_controller.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
66

7-
class GeneratedControllerTest extends WebTestCase
7+
final class GeneratedControllerTest extends WebTestCase
88
{
99
public function testControllerValidity()
1010
{

0 commit comments

Comments
 (0)