Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[make:controller] generate final controller class #1588

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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;
}
Expand Down
67 changes: 39 additions & 28 deletions src/Maker/MakeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
]
);
}
Expand Down
8 changes: 4 additions & 4 deletions src/Resources/skeleton/controller/Controller.tpl.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?= "<?php\n" ?>

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

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

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

{
<?php if ($with_template) { ?>
return $this->render('<?= $template_name ?>', [
'controller_name' => '<?= $class_name ?>',
'controller_name' => '<?= $class_data->getClassName() ?>',
]);
<?php } else { ?>
return $this->json([
Expand Down
49 changes: 45 additions & 4 deletions src/Util/ClassSource/Model/ClassData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 12 additions & 1 deletion tests/Maker/MakeControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
);
}),
];

Expand Down Expand Up @@ -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'))
);
}),
];

Expand Down
52 changes: 52 additions & 0 deletions tests/Util/ClassSource/ClassDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}
}
19 changes: 19 additions & 0 deletions tests/fixtures/make-controller/expected/FinalController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class FooBarController extends AbstractController
{
#[Route('/foo/bar', name: 'app_foo_bar')]
public function index(): JsonResponse
{
return $this->json([
'message' => 'Welcome to your new controller!',
'path' => 'src/Controller/FooBarController.php',
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class FooTwigController extends AbstractController
{
#[Route('/foo/twig', name: 'app_foo_twig')]
public function index(): Response
{
return $this->render('foo_twig/index.html.twig', [
'controller_name' => 'FooTwigController',
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class GeneratedControllerTest extends WebTestCase
final class GeneratedControllerTest extends WebTestCase
{
public function testController()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class GeneratedControllerTest extends WebTestCase
final class GeneratedControllerTest extends WebTestCase
{
public function testController()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class GeneratedControllerTest extends WebTestCase
final class GeneratedControllerTest extends WebTestCase
{
public function testControllerValidity()
{
Expand Down
Loading