Skip to content

Commit

Permalink
feature #1329 Add support for generating UUID id fields in entities
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Rushlow <[email protected]>
  • Loading branch information
Coffee2CodeNL and jrushlow authored Mar 22, 2024
1 parent 7217480 commit 970f4c0
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 4 deletions.
12 changes: 11 additions & 1 deletion src/Doctrine/EntityClassGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ApiPlatform\Metadata\ApiResource;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
Expand All @@ -22,6 +23,7 @@
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\UX\Turbo\Attribute\Broadcast;

/**
Expand All @@ -35,7 +37,7 @@ public function __construct(
) {
}

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

if ($useUuidIdentifier) {
$useStatements->addUseStatement([
Uuid::class,
UuidType::class,
]);
}

$entityPath = $this->generator->generateClass(
$entityClassDetails->getFullName(),
'doctrine/Entity.tpl.php',
Expand All @@ -68,6 +77,7 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
'broadcast' => $broadcast,
'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName),
'table_name' => $tableName,
'uses_uuid' => $useUuidIdentifier,
]
);

Expand Down
51 changes: 51 additions & 0 deletions src/Maker/Common/UidTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Maker\Common;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Uid\Uuid;

/**
* @author Jesse Rushlow<[email protected]>
*
* @internal
*/
trait UidTrait
{
/**
* Set by calling checkIsUsingUuid().
* Use in a maker's generate() to determine if entity wants to use uuid's.
*/
protected bool $usesUid = false;

/**
* Call this in a maker's configure() to consistently allow entity's with UUID's.
*/
protected function addWithUuidOption(Command $command): Command
{
$command->addOption('with-uuid', 'u', InputOption::VALUE_NONE, 'Use UUID for entity "id"');

return $command;
}

/**
* Call this as early as possible in a maker's interact().
*/
protected function checkIsUsingUid(InputInterface $input): void
{
if (($this->usesUid = $input->getOption('with-uuid')) && !class_exists(Uuid::class)) {
throw new \RuntimeException('You must install symfony/uid to use Uuid\'s as "id" (composer require symfony/uid)');
}
}
}
10 changes: 9 additions & 1 deletion src/Maker/MakeEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
Expand All @@ -47,6 +48,8 @@
*/
final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface
{
use UidTrait;

private Generator $generator;
private EntityClassGenerator $entityClassGenerator;

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

$this->addWithUuidOption($command);

$inputConfig->setArgumentAsNonInteractive('name');
}

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

$this->checkIsUsingUid($input);

$argument = $command->getDefinition()->getArgument('name');
$question = $this->createEntityClassQuestion($argument->getDescription());
$entityClassName = $io->askQuestion($question);
Expand Down Expand Up @@ -188,7 +195,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$input->getOption('api-resource'),
false,
true,
$broadcast
$broadcast,
$this->usesUid
);

if ($broadcast) {
Expand Down
9 changes: 8 additions & 1 deletion src/Maker/MakeResetPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
Expand Down Expand Up @@ -80,6 +81,8 @@
*/
class MakeResetPassword extends AbstractMaker
{
use UidTrait;

private string $fromEmailAddress;
private string $fromEmailName;
private string $controllerResetSuccessRedirect;
Expand Down Expand Up @@ -110,6 +113,8 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
$command
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeResetPassword.txt'))
;

$this->addWithUuidOption($command);
}

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

$this->checkIsUsingUid($input);

$interactiveSecurityHelper = new InteractiveSecurityHelper();

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

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

$generator->writeChanges();

Expand Down
10 changes: 9 additions & 1 deletion src/Maker/MakeUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
use Symfony\Bundle\MakerBundle\Security\UserClassBuilder;
use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
Expand Down Expand Up @@ -48,6 +49,8 @@
*/
final class MakeUser extends AbstractMaker
{
use UidTrait;

public function __construct(
private FileManager $fileManager,
private UserClassBuilder $userClassBuilder,
Expand Down Expand Up @@ -76,11 +79,15 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
->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)')
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt'));

$this->addWithUuidOption($command);

$inputConfig->setArgumentAsNonInteractive('name');
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$this->checkIsUsingUid($input);

if (null === $input->getArgument('name')) {
$name = $io->ask(
$command->getDefinition()->getArgument('name')->getDescription(),
Expand Down Expand Up @@ -130,7 +137,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$classPath = $this->entityClassGenerator->generateEntityClass(
$userClassNameDetails,
false, // api resource
$userClassConfiguration->hasPassword() // security user
$userClassConfiguration->hasPassword(), // security user
useUuidIdentifier: $this->usesUid
);
} else {
$classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');
Expand Down
13 changes: 13 additions & 0 deletions src/Resources/skeleton/doctrine/Entity.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
<?php endif ?>
class <?= $class_name."\n" ?>
{
<?php if ($uses_uuid): ?>
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?Uuid $id = null;

public function getId(): ?Uuid
{
return $this->id;
}
<?php else: ?>
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
Expand All @@ -24,4 +36,5 @@ public function getId(): ?int
{
return $this->id;
}
<?php endif ?>
}
2 changes: 2 additions & 0 deletions tests/Maker/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public function testWiring()
->in(__DIR__.'/../../src/Maker')
// exclude deprecated classes
->notContains('/@deprecated/')
// exclude Maker/Common/ as no maker's should live in this dir
->notPath('Common')
;

$application = new Application($kernel);
Expand Down
20 changes: 20 additions & 0 deletions tests/Maker/MakeEntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,26 @@ public function getTestDetails(): \Generator
}),
];

yield 'it_creates_a_new_class_with_uuid' => [$this->createMakeEntityTest()
->addExtraDependencies('symfony/uid')
->run(function (MakerTestRunner $runner) {
$runner->runMaker([
// entity class name
'User',
// add not additional fields
'',
], '--with-uuid');

$this->assertFileExists($runner->getPath('src/Entity/User.php'));

$content = file_get_contents($runner->getPath('src/Entity/User.php'));
$this->assertStringContainsString('use Symfony\Component\Uid\Uuid;', $content);
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.uuid_generator\')]', $content);

$this->runEntityTest($runner);
}),
];

yield 'it_creates_a_new_class_with_fields' => [$this->createMakeEntityTest()
->run(function (MakerTestRunner $runner) {
$runner->runMaker([
Expand Down
60 changes: 60 additions & 0 deletions tests/Maker/MakeResetPasswordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,66 @@ public function getTestDetails(): \Generator
}),
];

yield 'it_generates_with_uuid' => [$this->createMakerTest()
->setSkippedPhpVersions(80100, 80109)
->addExtraDependencies('symfony/uid')
->run(function (MakerTestRunner $runner) {
$this->makeUser($runner);

$output = $runner->runMaker([
'App\Entity\User',
'app_home',
'[email protected]',
'SymfonyCasts',
], '--with-uuid');

$this->assertStringContainsString('Success', $output);

$generatedFiles = [
'src/Controller/ResetPasswordController.php',
'src/Entity/ResetPasswordRequest.php',
'src/Form/ChangePasswordFormType.php',
'src/Form/ResetPasswordRequestFormType.php',
'src/Repository/ResetPasswordRequestRepository.php',
'templates/reset_password/check_email.html.twig',
'templates/reset_password/email.html.twig',
'templates/reset_password/request.html.twig',
'templates/reset_password/reset.html.twig',
];

foreach ($generatedFiles as $file) {
$this->assertFileExists($runner->getPath($file));
}

$resetPasswordRequestEntityContents = file_get_contents($runner->getPath('src/Entity/ResetPasswordRequest.php'));
$this->assertStringContainsString('use Symfony\Component\Uid\Uuid;', $resetPasswordRequestEntityContents);
$this->assertStringContainsString('[ORM\CustomIdGenerator(class: \'doctrine.uuid_generator\')]', $resetPasswordRequestEntityContents);

$configFileContents = file_get_contents($runner->getPath('config/packages/reset_password.yaml'));

// Flex recipe adds comments in reset_password.yaml, check file was replaced by maker
$this->assertStringNotContainsString('#', $configFileContents);

$resetPasswordConfig = $runner->readYaml('config/packages/reset_password.yaml');

$this->assertSame('App\Repository\ResetPasswordRequestRepository', $resetPasswordConfig['symfonycasts_reset_password']['request_password_repository']);

$runner->writeFile(
'config/packages/mailer.yaml',
Yaml::dump(['framework' => [
'mailer' => ['dsn' => 'null://null'],
]])
);

$runner->copy(
'make-reset-password/tests/it_generates_with_normal_setup.php',
'tests/ResetPasswordFunctionalTest.php'
);

$runner->runTests();
}),
];

yield 'it_generates_with_translator_installed' => [$this->createMakerTest()
// @legacy - drop skipped versions when PHP 8.1 is no longer supported.
->setSkippedPhpVersions(80100, 80109)
Expand Down
21 changes: 21 additions & 0 deletions tests/Maker/MakeUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ public function getTestDetails(): \Generator
}),
];

yield 'it_generates_entity_with_password_and_uuid' => [$this->createMakerTest()
->addExtraDependencies('doctrine')
->addExtraDependencies('symfony/uid')
->run(function (MakerTestRunner $runner) {
$runner->copy(
'make-user/standard_setup',
''
);

$runner->runMaker([
// user class name
'User',
'y', // entity
'email', // identity property
'y', // with password
], '--with-uuid');

$this->runUserTest($runner, 'it_generates_entity_with_password_and_uuid.php');
}),
];

yield 'it_generates_non_entity_no_password' => [$this->createMakerTest()
->addExtraDependencies('doctrine')
->run(function (MakerTestRunner $runner) {
Expand Down
Loading

0 comments on commit 970f4c0

Please sign in to comment.