Skip to content

Commit f0d1574

Browse files
authored
feature #1539 [make:crud|voter] generate classes with final keyword
1 parent ba37c4f commit f0d1574

File tree

18 files changed

+472
-57
lines changed

18 files changed

+472
-57
lines changed

src/DependencyInjection/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public function getConfigTreeBuilder(): TreeBuilder
2424
$rootNode
2525
->children()
2626
->scalarNode('root_namespace')->defaultValue('App')->end()
27+
->booleanNode('generate_final_classes')->defaultTrue()->end()
28+
->booleanNode('generate_final_entities')->defaultFalse()->end()
2729
->end()
2830
;
2931

src/DependencyInjection/MakerExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ public function load(array $configs, ContainerBuilder $container): void
4545
$doctrineHelperDefinition = $container->getDefinition('maker.doctrine_helper');
4646
$doctrineHelperDefinition->replaceArgument(0, $rootNamespace.'\\Entity');
4747

48+
$componentGeneratorDefinition = $container->getDefinition('maker.template_component_generator');
49+
$componentGeneratorDefinition
50+
->replaceArgument(0, $config['generate_final_classes'])
51+
->replaceArgument(1, $config['generate_final_entities'])
52+
->replaceArgument(2, $rootNamespace)
53+
;
54+
4855
$container->registerForAutoconfiguration(MakerInterface::class)
4956
->addTag(MakeCommandRegistrationPass::MAKER_TAG);
5057
}

src/Generator.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1515
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
1616
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
17+
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassData;
1718
use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;
1819
use Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator;
1920

@@ -54,6 +55,11 @@ public function __construct(
5455
*/
5556
public function generateClass(string $className, string $templateName, array $variables = []): string
5657
{
58+
if (\array_key_exists('class_data', $variables) && $variables['class_data'] instanceof ClassData) {
59+
$classData = $this->templateComponentGenerator->configureClass($variables['class_data']);
60+
$className = $classData->getFullClassName();
61+
}
62+
5763
$targetPath = $this->fileManager->getRelativePathForFutureClass($className);
5864

5965
if (null === $targetPath) {

src/Maker/MakeCrud.php

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
use Symfony\Bundle\MakerBundle\Maker\Common\CanGenerateTestsTrait;
2828
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
2929
use Symfony\Bundle\MakerBundle\Str;
30-
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
30+
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassData;
3131
use Symfony\Bundle\MakerBundle\Validator;
3232
use Symfony\Bundle\TwigBundle\TwigBundle;
3333
use Symfony\Component\Console\Command\Command;
@@ -147,6 +147,21 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
147147
++$iter;
148148
} while (class_exists($formClassDetails->getFullName()));
149149

150+
$controllerClassData = ClassData::create(
151+
class: \sprintf('Controller\%s', $this->controllerClassName),
152+
suffix: 'Controller',
153+
extendsClass: AbstractController::class,
154+
useStatements: [
155+
$entityClassDetails->getFullName(),
156+
$formClassDetails->getFullName(),
157+
$repositoryClassName,
158+
AbstractController::class,
159+
Request::class,
160+
Response::class,
161+
Route::class,
162+
],
163+
);
164+
150165
$entityVarPlural = lcfirst($this->inflector->pluralize($entityClassDetails->getShortName()));
151166
$entityVarSingular = lcfirst($this->inflector->singularize($entityClassDetails->getShortName()));
152167

@@ -156,25 +171,15 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
156171
$routeName = Str::asRouteName($controllerClassDetails->getRelativeNameWithoutSuffix());
157172
$templatesPath = Str::asFilePath($controllerClassDetails->getRelativeNameWithoutSuffix());
158173

159-
$useStatements = new UseStatementGenerator([
160-
$entityClassDetails->getFullName(),
161-
$formClassDetails->getFullName(),
162-
$repositoryClassName,
163-
AbstractController::class,
164-
Request::class,
165-
Response::class,
166-
Route::class,
167-
]);
168-
169174
if (EntityManagerInterface::class !== $repositoryClassName) {
170-
$useStatements->addUseStatement(EntityManagerInterface::class);
175+
$controllerClassData->addUseStatement(EntityManagerInterface::class);
171176
}
172177

173178
$generator->generateController(
174-
$controllerClassDetails->getFullName(),
179+
$controllerClassData->getFullClassName(),
175180
'crud/controller/Controller.tpl.php',
176181
array_merge([
177-
'use_statements' => $useStatements,
182+
'class_data' => $controllerClassData,
178183
'entity_class_name' => $entityClassDetails->getShortName(),
179184
'form_class_name' => $formClassDetails->getShortName(),
180185
'route_path' => Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()),
@@ -242,37 +247,33 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
242247
}
243248

244249
if ($this->shouldGenerateTests()) {
245-
$testClassDetails = $generator->createClassNameDetails(
246-
$entityClassDetails->getRelativeNameWithoutSuffix(),
247-
'Test\\Controller\\',
248-
'ControllerTest'
250+
$testClassData = ClassData::create(
251+
class: \sprintf('Tests\Controller\%s', $entityClassDetails->getRelativeNameWithoutSuffix()),
252+
suffix: 'ControllerTest',
253+
extendsClass: WebTestCase::class,
254+
useStatements: [
255+
$entityClassDetails->getFullName(),
256+
WebTestCase::class,
257+
KernelBrowser::class,
258+
$repositoryClassName,
259+
EntityRepository::class,
260+
],
249261
);
250262

251-
$useStatements = new UseStatementGenerator([
252-
$entityClassDetails->getFullName(),
253-
WebTestCase::class,
254-
KernelBrowser::class,
255-
$repositoryClassName,
256-
]);
257-
258-
$useStatements->addUseStatement(EntityRepository::class);
259-
260263
if (EntityManagerInterface::class !== $repositoryClassName) {
261-
$useStatements->addUseStatement(EntityManagerInterface::class);
264+
$testClassData->addUseStatement(EntityManagerInterface::class);
262265
}
263266

264-
$generator->generateFile(
265-
'tests/Controller/'.$testClassDetails->getShortName().'.php',
267+
$generator->generateClass(
268+
$testClassData->getFullClassName(),
266269
'crud/test/Test.EntityManager.tpl.php',
267270
[
268-
'use_statements' => $useStatements,
271+
'class_data' => $testClassData,
269272
'entity_full_class_name' => $entityClassDetails->getFullName(),
270273
'entity_class_name' => $entityClassDetails->getShortName(),
271274
'entity_var_singular' => $entityVarSingular,
272275
'route_path' => Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()),
273276
'route_name' => $routeName,
274-
'class_name' => Str::getShortClassName($testClassDetails->getFullName()),
275-
'namespace' => Str::getNamespace($testClassDetails->getFullName()),
276277
'form_fields' => $entityDoctrineDetails->getFormFields(),
277278
'repository_class_name' => EntityManagerInterface::class,
278279
'form_field_prefix' => strtolower(Str::asSnakeCase($entityTwigVarSingular)),

src/Maker/MakeVoter.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
use Symfony\Bundle\MakerBundle\DependencyBuilder;
1616
use Symfony\Bundle\MakerBundle\Generator;
1717
use Symfony\Bundle\MakerBundle\InputConfiguration;
18+
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassData;
1819
use Symfony\Component\Console\Command\Command;
1920
use Symfony\Component\Console\Input\InputArgument;
2021
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2123
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
24+
use Symfony\Component\Security\Core\User\UserInterface;
2225

2326
/**
2427
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
@@ -46,16 +49,21 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4649

4750
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
4851
{
49-
$voterClassNameDetails = $generator->createClassNameDetails(
50-
$input->getArgument('name'),
51-
'Security\\Voter\\',
52-
'Voter'
52+
$voterClassData = ClassData::create(
53+
class: \sprintf('Security\Voter\%s', $input->getArgument('name')),
54+
suffix: 'Voter',
55+
extendsClass: Voter::class,
56+
useStatements: [
57+
TokenInterface::class,
58+
Voter::class,
59+
UserInterface::class,
60+
]
5361
);
5462

5563
$generator->generateClass(
56-
$voterClassNameDetails->getFullName(),
64+
$voterClassData->getFullClassName(),
5765
'security/Voter.tpl.php',
58-
[]
66+
['class_data' => $voterClassData]
5967
);
6068

6169
$generator->writeChanges();

src/Resources/config/services.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
</service>
8181

8282
<service id="maker.template_component_generator" class="Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator">
83+
<argument /> <!-- generate_final_classes -->
84+
<argument /> <!-- generate_final_entities -->
85+
<argument /> <!-- root_namespace -->
8386
</service>
8487
</services>
8588
</container>

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

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

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

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

77
#[Route('<?= $route_path ?>')]
8-
class <?= $class_name ?> extends AbstractController
8+
<?= $class_data->getClassDeclaration() ?>
99
{
1010
<?= $generator->generateRouteForControllerMethod('/', sprintf('%s_index', $route_name), ['GET']) ?>
1111
<?php if (isset($repository_full_class_name)): ?>

src/Resources/skeleton/crud/test/Test.EntityManager.tpl.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
namespace <?= $namespace ?>;
55

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

8-
class <?= $class_name ?> extends WebTestCase<?= "\n" ?>
8+
<?= $class_data->getClassDeclaration() ?>
99
{
1010
private KernelBrowser $client;
1111
private EntityManagerInterface $manager;

src/Resources/skeleton/security/Voter.tpl.php

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

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

5-
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
6-
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7-
use Symfony\Component\Security\Core\User\UserInterface;
5+
<?= $class_data->getUseStatements(); ?>
86

9-
class <?= $class_name ?> extends Voter
7+
<?= $class_data->getClassDeclaration() ?>
108
{
119
public const EDIT = 'POST_EDIT';
1210
public const VIEW = 'POST_VIEW';
@@ -16,7 +14,7 @@ protected function supports(string $attribute, mixed $subject): bool
1614
// replace with your own logic
1715
// https://symfony.com/doc/current/security/voters.html
1816
return in_array($attribute, [self::EDIT, self::VIEW])
19-
&& $subject instanceof \App\Entity\<?= str_replace('Voter', null, $class_name) ?>;
17+
&& $subject instanceof \App\Entity\<?= str_replace('Voter', null, $class_data->getClassName()) ?>;
2018
}
2119

2220
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Util\ClassSource\Model;
13+
14+
use Symfony\Bundle\MakerBundle\Str;
15+
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
16+
17+
/**
18+
* @author Jesse Rushlow <jr@rushlow.dev>
19+
*
20+
* @internal
21+
*/
22+
final class ClassData
23+
{
24+
private function __construct(
25+
private string $className,
26+
private string $namespace,
27+
public readonly ?string $extends,
28+
public readonly bool $isEntity,
29+
private UseStatementGenerator $useStatementGenerator,
30+
private bool $isFinal = true,
31+
private string $rootNamespace = 'App',
32+
) {
33+
}
34+
35+
public static function create(string $class, ?string $suffix = null, ?string $extendsClass = null, bool $isEntity = false, array $useStatements = []): self
36+
{
37+
$className = Str::getShortClassName($class);
38+
39+
if (null !== $suffix && !str_ends_with($className, $suffix)) {
40+
$className = Str::asClassName(\sprintf('%s%s', $className, $suffix));
41+
}
42+
43+
$useStatements = new UseStatementGenerator($useStatements);
44+
45+
if ($extendsClass) {
46+
$useStatements->addUseStatement($extendsClass);
47+
}
48+
49+
return new self(
50+
className: Str::asClassName($className),
51+
namespace: Str::getNamespace($class),
52+
extends: null === $extendsClass ? null : Str::getShortClassName($extendsClass),
53+
isEntity: $isEntity,
54+
useStatementGenerator: $useStatements,
55+
);
56+
}
57+
58+
public function getClassName(): string
59+
{
60+
return $this->className;
61+
}
62+
63+
public function getNamespace(): string
64+
{
65+
if (empty($this->namespace)) {
66+
return $this->rootNamespace;
67+
}
68+
69+
return \sprintf('%s\%s', $this->rootNamespace, $this->namespace);
70+
}
71+
72+
public function getFullClassName(): string
73+
{
74+
return \sprintf('%s\%s', $this->getNamespace(), $this->className);
75+
}
76+
77+
public function setRootNamespace(string $rootNamespace): self
78+
{
79+
$this->rootNamespace = $rootNamespace;
80+
81+
return $this;
82+
}
83+
84+
public function getClassDeclaration(): string
85+
{
86+
$extendsDeclaration = '';
87+
88+
if (null !== $this->extends) {
89+
$extendsDeclaration = \sprintf(' extends %s', $this->extends);
90+
}
91+
92+
return \sprintf('%sclass %s%s',
93+
$this->isFinal ? 'final ' : '',
94+
$this->className,
95+
$extendsDeclaration,
96+
);
97+
}
98+
99+
public function setIsFinal(bool $isFinal): self
100+
{
101+
$this->isFinal = $isFinal;
102+
103+
return $this;
104+
}
105+
106+
public function addUseStatement(array|string $useStatement): self
107+
{
108+
$this->useStatementGenerator->addUseStatement($useStatement);
109+
110+
return $this;
111+
}
112+
113+
public function getUseStatements(): string
114+
{
115+
return (string) $this->useStatementGenerator;
116+
}
117+
}

0 commit comments

Comments
 (0)