Skip to content

Commit f6c8719

Browse files
authored
feature #1488 allow the option to use ulid's for entity id's
1 parent 970f4c0 commit f6c8719

File tree

12 files changed

+271
-23
lines changed

12 files changed

+271
-23
lines changed

src/Doctrine/EntityClassGenerator.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
use ApiPlatform\Metadata\ApiResource;
1515
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
1616
use Doctrine\Persistence\ManagerRegistry;
17+
use Symfony\Bridge\Doctrine\Types\UlidType;
1718
use Symfony\Bridge\Doctrine\Types\UuidType;
1819
use Symfony\Bundle\MakerBundle\Generator;
20+
use Symfony\Bundle\MakerBundle\Maker\Common\EntityIdTypeEnum;
1921
use Symfony\Bundle\MakerBundle\Str;
2022
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
2123
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
2224
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
2325
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
2426
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
2527
use Symfony\Component\Security\Core\User\UserInterface;
28+
use Symfony\Component\Uid\Ulid;
2629
use Symfony\Component\Uid\Uuid;
2730
use Symfony\UX\Turbo\Attribute\Broadcast;
2831

@@ -37,7 +40,7 @@ public function __construct(
3740
) {
3841
}
3942

40-
public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $apiResource, bool $withPasswordUpgrade = false, bool $generateRepositoryClass = true, bool $broadcast = false, bool $useUuidIdentifier = false): string
43+
public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $apiResource, bool $withPasswordUpgrade = false, bool $generateRepositoryClass = true, bool $broadcast = false, EntityIdTypeEnum $useUuidIdentifier = EntityIdTypeEnum::INT): string
4144
{
4245
$repoClassDetails = $this->generator->createClassNameDetails(
4346
$entityClassDetails->getRelativeName(),
@@ -60,13 +63,20 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
6063
$useStatements->addUseStatement(ApiResource::class);
6164
}
6265

63-
if ($useUuidIdentifier) {
66+
if (EntityIdTypeEnum::UUID === $useUuidIdentifier) {
6467
$useStatements->addUseStatement([
6568
Uuid::class,
6669
UuidType::class,
6770
]);
6871
}
6972

73+
if (EntityIdTypeEnum::ULID === $useUuidIdentifier) {
74+
$useStatements->addUseStatement([
75+
Ulid::class,
76+
UlidType::class,
77+
]);
78+
}
79+
7080
$entityPath = $this->generator->generateClass(
7181
$entityClassDetails->getFullName(),
7282
'doctrine/Entity.tpl.php',
@@ -77,7 +87,7 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
7787
'broadcast' => $broadcast,
7888
'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName),
7989
'table_name' => $tableName,
80-
'uses_uuid' => $useUuidIdentifier,
90+
'id_type' => $useUuidIdentifier,
8191
]
8292
);
8393

src/Maker/Common/EntityIdTypeEnum.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
/**
15+
* @author Jesse Rushlow <jr@rushlow.dev>
16+
*
17+
* @internal
18+
*/
19+
enum EntityIdTypeEnum
20+
{
21+
case INT;
22+
case UUID;
23+
case ULID;
24+
}

src/Maker/Common/UidTrait.php

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
namespace Symfony\Bundle\MakerBundle\Maker\Common;
1313

14+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Input\InputInterface;
1617
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Uid\Ulid;
1719
use Symfony\Component\Uid\Uuid;
1820

1921
/**
@@ -23,18 +25,23 @@
2325
*/
2426
trait UidTrait
2527
{
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;
28+
private bool $usesUuid = false;
29+
private bool $usesUlid = false;
3130

3231
/**
3332
* Call this in a maker's configure() to consistently allow entity's with UUID's.
33+
* This should be called after you calling "setHelp()" in the maker.
3434
*/
3535
protected function addWithUuidOption(Command $command): Command
3636
{
37-
$command->addOption('with-uuid', 'u', InputOption::VALUE_NONE, 'Use UUID for entity "id"');
37+
$uidHelp = file_get_contents(\dirname(__DIR__, 2).'/Resources/help/_WithUid.txt');
38+
$help = $command->getHelp()."\n".$uidHelp;
39+
40+
$command
41+
->addOption(name: 'with-uuid', mode: InputOption::VALUE_NONE, description: 'Use UUID for entity "id"')
42+
->addOption('with-ulid', mode: InputOption::VALUE_NONE, description: 'Use ULID for entity "id"')
43+
->setHelp($help)
44+
;
3845

3946
return $command;
4047
}
@@ -44,8 +51,29 @@ protected function addWithUuidOption(Command $command): Command
4451
*/
4552
protected function checkIsUsingUid(InputInterface $input): void
4653
{
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)');
54+
if (($this->usesUuid = $input->getOption('with-uuid')) && !class_exists(Uuid::class)) {
55+
throw new RuntimeCommandException('You must install symfony/uid to use Uuid\'s as "id" (composer require symfony/uid)');
56+
}
57+
58+
if (($this->usesUlid = $input->getOption('with-ulid')) && !class_exists(Ulid::class)) {
59+
throw new RuntimeCommandException('You must install symfony/uid to use Ulid\'s as "id" (composer require symfony/uid)');
60+
}
61+
62+
if ($this->usesUuid && $this->usesUlid) {
63+
throw new RuntimeCommandException('Setting --with-uuid & --with-ulid at the same time is not allowed. Please choose only one.');
64+
}
65+
}
66+
67+
protected function getIdType(): EntityIdTypeEnum
68+
{
69+
if ($this->usesUuid) {
70+
return EntityIdTypeEnum::UUID;
4971
}
72+
73+
if ($this->usesUlid) {
74+
return EntityIdTypeEnum::ULID;
75+
}
76+
77+
return EntityIdTypeEnum::INT;
5078
}
5179
}

src/Maker/MakeEntity.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
191191
if (!$classExists) {
192192
$broadcast = $input->getOption('broadcast');
193193
$entityPath = $this->entityClassGenerator->generateEntityClass(
194-
$entityClassDetails,
195-
$input->getOption('api-resource'),
196-
false,
197-
true,
198-
$broadcast,
199-
$this->usesUid
194+
entityClassDetails: $entityClassDetails,
195+
apiResource: $input->getOption('api-resource'),
196+
broadcast: $broadcast,
197+
useUuidIdentifier: $this->getIdType(),
200198
);
201199

202200
if ($broadcast) {

src/Maker/MakeResetPassword.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,12 @@ private function successMessage(ConsoleStyle $io, string $requestClassName): voi
407407

408408
private function generateRequestEntity(Generator $generator, ClassNameDetails $requestClassNameDetails, ClassNameDetails $repositoryClassNameDetails): void
409409
{
410-
$requestEntityPath = $this->entityClassGenerator->generateEntityClass($requestClassNameDetails, false, generateRepositoryClass: false, useUuidIdentifier: $this->usesUid);
410+
$requestEntityPath = $this->entityClassGenerator->generateEntityClass(
411+
entityClassDetails: $requestClassNameDetails,
412+
apiResource: false,
413+
generateRepositoryClass: false,
414+
useUuidIdentifier: $this->getIdType()
415+
);
411416

412417
$generator->writeChanges();
413418

src/Maker/MakeUser.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
135135
// A) Generate the User class
136136
if ($userClassConfiguration->isEntity()) {
137137
$classPath = $this->entityClassGenerator->generateEntityClass(
138-
$userClassNameDetails,
139-
false, // api resource
140-
$userClassConfiguration->hasPassword(), // security user
141-
useUuidIdentifier: $this->usesUid
138+
entityClassDetails: $userClassNameDetails,
139+
apiResource: false, // api resource
140+
withPasswordUpgrade: $userClassConfiguration->hasPassword(), // security user
141+
useUuidIdentifier: $this->getIdType()
142142
);
143143
} else {
144144
$classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');

src/Resources/help/_WithUid.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Instead of using the default "int" type for the entity's "id", you can use the
2+
UUID type from Symfony's Uid component.
3+
<href=https://symfony.com/doc/current/components/uid.html#storing-uuids-in-databases>https://symfony.com/doc/current/components/uid.html#storing-uuids-in-databases</>
4+
5+
<info>php %command.full_name% --with-uuid</info>
6+
7+
Or you can use the ULID type from Symfony's Uid component.
8+
<href=https://symfony.com/doc/current/components/uid.html#storing-ulids-in-databases>https://symfony.com/doc/current/components/uid.html#storing-ulids-in-databases</>
9+
10+
<info>php %command.full_name% --with-ulid</info>

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
<?php
2+
3+
use Symfony\Bundle\MakerBundle\Maker\Common\EntityIdTypeEnum;
4+
5+
?>
16
<?= "<?php\n" ?>
27

38
namespace <?= $namespace ?>;
@@ -15,7 +20,7 @@
1520
<?php endif ?>
1621
class <?= $class_name."\n" ?>
1722
{
18-
<?php if ($uses_uuid): ?>
23+
<?php if (EntityIdTypeEnum::UUID === $id_type): ?>
1924
#[ORM\Id]
2025
#[ORM\Column(type: UuidType::NAME, unique: true)]
2126
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
@@ -26,6 +31,17 @@ public function getId(): ?Uuid
2631
{
2732
return $this->id;
2833
}
34+
<?php elseif (EntityIdTypeEnum::ULID === $id_type): ?>
35+
#[ORM\Id]
36+
#[ORM\Column(type: UlidType::NAME, unique: true)]
37+
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
38+
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
39+
private ?Ulid $id = null;
40+
41+
public function getId(): ?Ulid
42+
{
43+
return $this->id;
44+
}
2945
<?php else: ?>
3046
#[ORM\Id]
3147
#[ORM\GeneratedValue]

tests/Maker/MakeEntityTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,26 @@ public function getTestDetails(): \Generator
159159
}),
160160
];
161161

162+
yield 'it_creates_a_new_class_with_ulid' => [$this->createMakeEntityTest()
163+
->addExtraDependencies('symfony/uid')
164+
->run(function (MakerTestRunner $runner) {
165+
$runner->runMaker([
166+
// entity class name
167+
'User',
168+
// add not additional fields
169+
'',
170+
], '--with-ulid');
171+
172+
$this->assertFileExists($runner->getPath('src/Entity/User.php'));
173+
174+
$content = file_get_contents($runner->getPath('src/Entity/User.php'));
175+
$this->assertStringContainsString('use Symfony\Component\Uid\Ulid;', $content);
176+
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.ulid_generator\')]', $content);
177+
178+
$this->runEntityTest($runner);
179+
}),
180+
];
181+
162182
yield 'it_creates_a_new_class_with_fields' => [$this->createMakeEntityTest()
163183
->run(function (MakerTestRunner $runner) {
164184
$runner->runMaker([

tests/Maker/MakeResetPasswordTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,66 @@ public function getTestDetails(): \Generator
142142
}),
143143
];
144144

145+
yield 'it_generates_with_ulid' => [$this->createMakerTest()
146+
->setSkippedPhpVersions(80100, 80109)
147+
->addExtraDependencies('symfony/uid')
148+
->run(function (MakerTestRunner $runner) {
149+
$this->makeUser($runner);
150+
151+
$output = $runner->runMaker([
152+
'App\Entity\User',
153+
'app_home',
154+
'jr@rushlow.dev',
155+
'SymfonyCasts',
156+
], '--with-ulid');
157+
158+
$this->assertStringContainsString('Success', $output);
159+
160+
$generatedFiles = [
161+
'src/Controller/ResetPasswordController.php',
162+
'src/Entity/ResetPasswordRequest.php',
163+
'src/Form/ChangePasswordFormType.php',
164+
'src/Form/ResetPasswordRequestFormType.php',
165+
'src/Repository/ResetPasswordRequestRepository.php',
166+
'templates/reset_password/check_email.html.twig',
167+
'templates/reset_password/email.html.twig',
168+
'templates/reset_password/request.html.twig',
169+
'templates/reset_password/reset.html.twig',
170+
];
171+
172+
foreach ($generatedFiles as $file) {
173+
$this->assertFileExists($runner->getPath($file));
174+
}
175+
176+
$resetPasswordRequestEntityContents = file_get_contents($runner->getPath('src/Entity/ResetPasswordRequest.php'));
177+
$this->assertStringContainsString('use Symfony\Component\Uid\Ulid;', $resetPasswordRequestEntityContents);
178+
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.ulid_generator\')]', $resetPasswordRequestEntityContents);
179+
180+
$configFileContents = file_get_contents($runner->getPath('config/packages/reset_password.yaml'));
181+
182+
// Flex recipe adds comments in reset_password.yaml, check file was replaced by maker
183+
$this->assertStringNotContainsString('#', $configFileContents);
184+
185+
$resetPasswordConfig = $runner->readYaml('config/packages/reset_password.yaml');
186+
187+
$this->assertSame('App\Repository\ResetPasswordRequestRepository', $resetPasswordConfig['symfonycasts_reset_password']['request_password_repository']);
188+
189+
$runner->writeFile(
190+
'config/packages/mailer.yaml',
191+
Yaml::dump(['framework' => [
192+
'mailer' => ['dsn' => 'null://null'],
193+
]])
194+
);
195+
196+
$runner->copy(
197+
'make-reset-password/tests/it_generates_with_normal_setup.php',
198+
'tests/ResetPasswordFunctionalTest.php'
199+
);
200+
201+
$runner->runTests();
202+
}),
203+
];
204+
145205
yield 'it_generates_with_translator_installed' => [$this->createMakerTest()
146206
// @legacy - drop skipped versions when PHP 8.1 is no longer supported.
147207
->setSkippedPhpVersions(80100, 80109)

0 commit comments

Comments
 (0)