Skip to content

Commit

Permalink
feature symfony#1588 [make:controller] generate final controller class
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
jrushlow authored Sep 26, 2024
1 parent d0cfae6 commit 3280a5d
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 45 deletions.
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

0 comments on commit 3280a5d

Please sign in to comment.