Skip to content

Commit 970f4c0

Browse files
feature #1329 Add support for generating UUID id fields in entities
Co-authored-by: Jesse Rushlow <jr@rushlow.dev>
1 parent 7217480 commit 970f4c0

File tree

11 files changed

+260
-4
lines changed

11 files changed

+260
-4
lines changed

src/Doctrine/EntityClassGenerator.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use ApiPlatform\Metadata\ApiResource;
1515
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
1616
use Doctrine\Persistence\ManagerRegistry;
17+
use Symfony\Bridge\Doctrine\Types\UuidType;
1718
use Symfony\Bundle\MakerBundle\Generator;
1819
use Symfony\Bundle\MakerBundle\Str;
1920
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
@@ -22,6 +23,7 @@
2223
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
2324
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
2425
use Symfony\Component\Security\Core\User\UserInterface;
26+
use Symfony\Component\Uid\Uuid;
2527
use Symfony\UX\Turbo\Attribute\Broadcast;
2628

2729
/**
@@ -35,7 +37,7 @@ public function __construct(
3537
) {
3638
}
3739

38-
public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $apiResource, bool $withPasswordUpgrade = false, bool $generateRepositoryClass = true, bool $broadcast = false): string
40+
public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $apiResource, bool $withPasswordUpgrade = false, bool $generateRepositoryClass = true, bool $broadcast = false, bool $useUuidIdentifier = false): string
3941
{
4042
$repoClassDetails = $this->generator->createClassNameDetails(
4143
$entityClassDetails->getRelativeName(),
@@ -58,6 +60,13 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
5860
$useStatements->addUseStatement(ApiResource::class);
5961
}
6062

63+
if ($useUuidIdentifier) {
64+
$useStatements->addUseStatement([
65+
Uuid::class,
66+
UuidType::class,
67+
]);
68+
}
69+
6170
$entityPath = $this->generator->generateClass(
6271
$entityClassDetails->getFullName(),
6372
'doctrine/Entity.tpl.php',
@@ -68,6 +77,7 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
6877
'broadcast' => $broadcast,
6978
'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName),
7079
'table_name' => $tableName,
80+
'uses_uuid' => $useUuidIdentifier,
7181
]
7282
);
7383

src/Maker/Common/UidTrait.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Maker\Common;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Uid\Uuid;
18+
19+
/**
20+
* @author Jesse Rushlow<jr@rushlow.dev>
21+
*
22+
* @internal
23+
*/
24+
trait UidTrait
25+
{
26+
/**
27+
* Set by calling checkIsUsingUuid().
28+
* Use in a maker's generate() to determine if entity wants to use uuid's.
29+
*/
30+
protected bool $usesUid = false;
31+
32+
/**
33+
* Call this in a maker's configure() to consistently allow entity's with UUID's.
34+
*/
35+
protected function addWithUuidOption(Command $command): Command
36+
{
37+
$command->addOption('with-uuid', 'u', InputOption::VALUE_NONE, 'Use UUID for entity "id"');
38+
39+
return $command;
40+
}
41+
42+
/**
43+
* Call this as early as possible in a maker's interact().
44+
*/
45+
protected function checkIsUsingUid(InputInterface $input): void
46+
{
47+
if (($this->usesUid = $input->getOption('with-uuid')) && !class_exists(Uuid::class)) {
48+
throw new \RuntimeException('You must install symfony/uid to use Uuid\'s as "id" (composer require symfony/uid)');
49+
}
50+
}
51+
}

src/Maker/MakeEntity.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Bundle\MakerBundle\Generator;
2626
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
2727
use Symfony\Bundle\MakerBundle\InputConfiguration;
28+
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
2829
use Symfony\Bundle\MakerBundle\Str;
2930
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
3031
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
@@ -47,6 +48,8 @@
4748
*/
4849
final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface
4950
{
51+
use UidTrait;
52+
5053
private Generator $generator;
5154
private EntityClassGenerator $entityClassGenerator;
5255

@@ -97,6 +100,8 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
97100
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt'))
98101
;
99102

103+
$this->addWithUuidOption($command);
104+
100105
$inputConfig->setArgumentAsNonInteractive('name');
101106
}
102107

@@ -122,6 +127,8 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
122127
return;
123128
}
124129

130+
$this->checkIsUsingUid($input);
131+
125132
$argument = $command->getDefinition()->getArgument('name');
126133
$question = $this->createEntityClassQuestion($argument->getDescription());
127134
$entityClassName = $io->askQuestion($question);
@@ -188,7 +195,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
188195
$input->getOption('api-resource'),
189196
false,
190197
true,
191-
$broadcast
198+
$broadcast,
199+
$this->usesUid
192200
);
193201

194202
if ($broadcast) {

src/Maker/MakeResetPassword.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Bundle\MakerBundle\FileManager;
2727
use Symfony\Bundle\MakerBundle\Generator;
2828
use Symfony\Bundle\MakerBundle\InputConfiguration;
29+
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
2930
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
3031
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
3132
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
@@ -80,6 +81,8 @@
8081
*/
8182
class MakeResetPassword extends AbstractMaker
8283
{
84+
use UidTrait;
85+
8386
private string $fromEmailAddress;
8487
private string $fromEmailName;
8588
private string $controllerResetSuccessRedirect;
@@ -110,6 +113,8 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
110113
$command
111114
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeResetPassword.txt'))
112115
;
116+
117+
$this->addWithUuidOption($command);
113118
}
114119

115120
public function configureDependencies(DependencyBuilder $dependencies): void
@@ -134,6 +139,8 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
134139
{
135140
$io->title('Let\'s make a password reset feature!');
136141

142+
$this->checkIsUsingUid($input);
143+
137144
$interactiveSecurityHelper = new InteractiveSecurityHelper();
138145

139146
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
@@ -400,7 +407,7 @@ private function successMessage(ConsoleStyle $io, string $requestClassName): voi
400407

401408
private function generateRequestEntity(Generator $generator, ClassNameDetails $requestClassNameDetails, ClassNameDetails $repositoryClassNameDetails): void
402409
{
403-
$requestEntityPath = $this->entityClassGenerator->generateEntityClass($requestClassNameDetails, false, false, false);
410+
$requestEntityPath = $this->entityClassGenerator->generateEntityClass($requestClassNameDetails, false, generateRepositoryClass: false, useUuidIdentifier: $this->usesUid);
404411

405412
$generator->writeChanges();
406413

src/Maker/MakeUser.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Bundle\MakerBundle\FileManager;
2222
use Symfony\Bundle\MakerBundle\Generator;
2323
use Symfony\Bundle\MakerBundle\InputConfiguration;
24+
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
2425
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
2526
use Symfony\Bundle\MakerBundle\Security\UserClassBuilder;
2627
use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
@@ -48,6 +49,8 @@
4849
*/
4950
final class MakeUser extends AbstractMaker
5051
{
52+
use UidTrait;
53+
5154
public function __construct(
5255
private FileManager $fileManager,
5356
private UserClassBuilder $userClassBuilder,
@@ -76,11 +79,15 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
7679
->addOption('with-password', null, InputOption::VALUE_NONE, 'Will this app be responsible for checking the password? Choose <comment>No</comment> if the password is actually checked by some other system (e.g. a single sign-on server)')
7780
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt'));
7881

82+
$this->addWithUuidOption($command);
83+
7984
$inputConfig->setArgumentAsNonInteractive('name');
8085
}
8186

8287
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
8388
{
89+
$this->checkIsUsingUid($input);
90+
8491
if (null === $input->getArgument('name')) {
8592
$name = $io->ask(
8693
$command->getDefinition()->getArgument('name')->getDescription(),
@@ -130,7 +137,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
130137
$classPath = $this->entityClassGenerator->generateEntityClass(
131138
$userClassNameDetails,
132139
false, // api resource
133-
$userClassConfiguration->hasPassword() // security user
140+
$userClassConfiguration->hasPassword(), // security user
141+
useUuidIdentifier: $this->usesUid
134142
);
135143
} else {
136144
$classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');

src/Resources/skeleton/doctrine/Entity.tpl.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
<?php endif ?>
1616
class <?= $class_name."\n" ?>
1717
{
18+
<?php if ($uses_uuid): ?>
19+
#[ORM\Id]
20+
#[ORM\Column(type: UuidType::NAME, unique: true)]
21+
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
22+
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
23+
private ?Uuid $id = null;
24+
25+
public function getId(): ?Uuid
26+
{
27+
return $this->id;
28+
}
29+
<?php else: ?>
1830
#[ORM\Id]
1931
#[ORM\GeneratedValue]
2032
#[ORM\Column]
@@ -24,4 +36,5 @@ public function getId(): ?int
2436
{
2537
return $this->id;
2638
}
39+
<?php endif ?>
2740
}

tests/Maker/FunctionalTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public function testWiring()
3333
->in(__DIR__.'/../../src/Maker')
3434
// exclude deprecated classes
3535
->notContains('/@deprecated/')
36+
// exclude Maker/Common/ as no maker's should live in this dir
37+
->notPath('Common')
3638
;
3739

3840
$application = new Application($kernel);

tests/Maker/MakeEntityTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,26 @@ public function getTestDetails(): \Generator
139139
}),
140140
];
141141

142+
yield 'it_creates_a_new_class_with_uuid' => [$this->createMakeEntityTest()
143+
->addExtraDependencies('symfony/uid')
144+
->run(function (MakerTestRunner $runner) {
145+
$runner->runMaker([
146+
// entity class name
147+
'User',
148+
// add not additional fields
149+
'',
150+
], '--with-uuid');
151+
152+
$this->assertFileExists($runner->getPath('src/Entity/User.php'));
153+
154+
$content = file_get_contents($runner->getPath('src/Entity/User.php'));
155+
$this->assertStringContainsString('use Symfony\Component\Uid\Uuid;', $content);
156+
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.uuid_generator\')]', $content);
157+
158+
$this->runEntityTest($runner);
159+
}),
160+
];
161+
142162
yield 'it_creates_a_new_class_with_fields' => [$this->createMakeEntityTest()
143163
->run(function (MakerTestRunner $runner) {
144164
$runner->runMaker([

tests/Maker/MakeResetPasswordTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,66 @@ public function getTestDetails(): \Generator
8282
}),
8383
];
8484

85+
yield 'it_generates_with_uuid' => [$this->createMakerTest()
86+
->setSkippedPhpVersions(80100, 80109)
87+
->addExtraDependencies('symfony/uid')
88+
->run(function (MakerTestRunner $runner) {
89+
$this->makeUser($runner);
90+
91+
$output = $runner->runMaker([
92+
'App\Entity\User',
93+
'app_home',
94+
'jr@rushlow.dev',
95+
'SymfonyCasts',
96+
], '--with-uuid');
97+
98+
$this->assertStringContainsString('Success', $output);
99+
100+
$generatedFiles = [
101+
'src/Controller/ResetPasswordController.php',
102+
'src/Entity/ResetPasswordRequest.php',
103+
'src/Form/ChangePasswordFormType.php',
104+
'src/Form/ResetPasswordRequestFormType.php',
105+
'src/Repository/ResetPasswordRequestRepository.php',
106+
'templates/reset_password/check_email.html.twig',
107+
'templates/reset_password/email.html.twig',
108+
'templates/reset_password/request.html.twig',
109+
'templates/reset_password/reset.html.twig',
110+
];
111+
112+
foreach ($generatedFiles as $file) {
113+
$this->assertFileExists($runner->getPath($file));
114+
}
115+
116+
$resetPasswordRequestEntityContents = file_get_contents($runner->getPath('src/Entity/ResetPasswordRequest.php'));
117+
$this->assertStringContainsString('use Symfony\Component\Uid\Uuid;', $resetPasswordRequestEntityContents);
118+
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.uuid_generator\')]', $resetPasswordRequestEntityContents);
119+
120+
$configFileContents = file_get_contents($runner->getPath('config/packages/reset_password.yaml'));
121+
122+
// Flex recipe adds comments in reset_password.yaml, check file was replaced by maker
123+
$this->assertStringNotContainsString('#', $configFileContents);
124+
125+
$resetPasswordConfig = $runner->readYaml('config/packages/reset_password.yaml');
126+
127+
$this->assertSame('App\Repository\ResetPasswordRequestRepository', $resetPasswordConfig['symfonycasts_reset_password']['request_password_repository']);
128+
129+
$runner->writeFile(
130+
'config/packages/mailer.yaml',
131+
Yaml::dump(['framework' => [
132+
'mailer' => ['dsn' => 'null://null'],
133+
]])
134+
);
135+
136+
$runner->copy(
137+
'make-reset-password/tests/it_generates_with_normal_setup.php',
138+
'tests/ResetPasswordFunctionalTest.php'
139+
);
140+
141+
$runner->runTests();
142+
}),
143+
];
144+
85145
yield 'it_generates_with_translator_installed' => [$this->createMakerTest()
86146
// @legacy - drop skipped versions when PHP 8.1 is no longer supported.
87147
->setSkippedPhpVersions(80100, 80109)

tests/Maker/MakeUserTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ public function getTestDetails(): \Generator
4545
}),
4646
];
4747

48+
yield 'it_generates_entity_with_password_and_uuid' => [$this->createMakerTest()
49+
->addExtraDependencies('doctrine')
50+
->addExtraDependencies('symfony/uid')
51+
->run(function (MakerTestRunner $runner) {
52+
$runner->copy(
53+
'make-user/standard_setup',
54+
''
55+
);
56+
57+
$runner->runMaker([
58+
// user class name
59+
'User',
60+
'y', // entity
61+
'email', // identity property
62+
'y', // with password
63+
], '--with-uuid');
64+
65+
$this->runUserTest($runner, 'it_generates_entity_with_password_and_uuid.php');
66+
}),
67+
];
68+
4869
yield 'it_generates_non_entity_no_password' => [$this->createMakerTest()
4970
->addExtraDependencies('doctrine')
5071
->run(function (MakerTestRunner $runner) {

0 commit comments

Comments
 (0)