Skip to content

Commit 8f46cc2

Browse files
committed
Asking to add UniqueEntity during make:registration-form
1 parent a018245 commit 8f46cc2

File tree

7 files changed

+241
-6
lines changed

7 files changed

+241
-6
lines changed

src/Maker/MakeEntity.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Bundle\MakerBundle\Str;
2727
use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator;
2828
use Symfony\Bundle\MakerBundle\FileManager;
29+
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
2930
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
3031
use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation;
3132
use Symfony\Bundle\MakerBundle\Validator;
@@ -774,7 +775,9 @@ private function createClassManipulator(string $path, ConsoleStyle $io, bool $ov
774775

775776
private function getPathOfClass(string $class): string
776777
{
777-
return (new \ReflectionClass($class))->getFileName();
778+
$classDetails = new ClassDetails($class);
779+
780+
return $classDetails->getPath();
778781
}
779782

780783
private function isClassInVendor(string $class): bool

src/Maker/MakeRegistrationForm.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
1515
use Doctrine\Common\Annotations\Annotation;
16+
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
1617
use Symfony\Bundle\MakerBundle\ConsoleStyle;
1718
use Symfony\Bundle\MakerBundle\DependencyBuilder;
1819
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
@@ -22,7 +23,9 @@
2223
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
2324
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
2425
use Symfony\Bundle\MakerBundle\Str;
26+
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
2527
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
28+
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
2629
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
2730
use Symfony\Bundle\SecurityBundle\SecurityBundle;
2831
use Symfony\Bundle\TwigBundle\TwigBundle;
@@ -80,6 +83,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
8083
->addOption('auto-login-authenticator')
8184
->addOption('firewall-name')
8285
->addOption('redirect-route-name')
86+
->addOption('add-unique-entity-constraint')
8387
;
8488

8589
$interactiveSecurityHelper = new InteractiveSecurityHelper();
@@ -112,6 +116,17 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
112116
$interactiveSecurityHelper->guessPasswordField($io, $userClass)
113117
);
114118

119+
// see if it makes sense to add the UniqueEntity constraint
120+
$userClassDetails = new ClassDetails($userClass);
121+
$addAnnotation = false;
122+
if (!$userClassDetails->doesDocBlockContainAnnotation('@UniqueEntity')) {
123+
$addAnnotation = $io->confirm(sprintf('Do you want to add a <comment>@UniqueEntity</comment> validation annotation on your <comment>%s</comment> class to make sure duplicate accounts aren\'t created?', Str::getShortClassName($userClass)));
124+
}
125+
$input->setOption(
126+
'add-unique-entity-constraint',
127+
$addAnnotation
128+
);
129+
115130
if ($io->confirm('Do you want to automatically authenticate the user after registration?')) {
116131
$this->interactAuthenticatorQuestions(
117132
$input,
@@ -172,13 +187,15 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
172187
'Entity\\'
173188
);
174189

190+
// 1) Generate the form class
175191
$usernameField = $input->getArgument('username-field');
176192
$formClassDetails = $this->generateFormClass(
177193
$userClassNameDetails,
178194
$generator,
179195
$usernameField
180196
);
181197

198+
// 2) Generate the controller
182199
$controllerClassNameDetails = $generator->createClassNameDetails(
183200
'RegistrationController',
184201
'Controller\\'
@@ -203,6 +220,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
203220
]
204221
);
205222

223+
// 3) Generate the template
206224
$generator->generateFile(
207225
'templates/registration/register.html.twig',
208226
'registration/twig_template.tpl.php',
@@ -211,11 +229,29 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
211229
]
212230
);
213231

232+
// 4) Update the User class if necessary
233+
if ($input->getOption('add-unique-entity-constraint')) {
234+
$classDetails = new ClassDetails($userClass);
235+
$userManipulator = new ClassSourceManipulator(
236+
file_get_contents($classDetails->getPath())
237+
);
238+
$userManipulator->setIo($io);
239+
240+
$userManipulator->addAnnotationToClass(
241+
UniqueEntity::class,
242+
[
243+
'fields' => [$usernameField],
244+
'message' => sprintf('There is already an account with this '.$usernameField),
245+
]
246+
);
247+
$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());
248+
}
249+
214250
$generator->writeChanges();
215251

216252
$this->writeSuccessMessage($io);
217253
$io->text('Next: Go to /register to check out your new form!');
218-
$io->text('Then, make any changes you need to the form, controller & template.');
254+
$io->text('Make any changes you need to the form, controller & template.');
219255
}
220256

221257
public function configureDependencies(DependencyBuilder $dependencies)

src/Util/ClassDetails.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,25 @@ private function getProperties(): array
5353

5454
return $propertiesList;
5555
}
56+
57+
public function getPath(): string
58+
{
59+
return (new \ReflectionClass($this->fullClassName))->getFileName();
60+
}
61+
62+
/**
63+
* An imperfect, but simple way to check for the presence of an annotation.
64+
*
65+
* @param string $annotation The annotation - e.g. @UniqueEntity
66+
*/
67+
public function doesDocBlockContainAnnotation(string $annotation): bool
68+
{
69+
$docComment = (new \ReflectionClass($this->fullClassName))->getDocComment();
70+
71+
if (false === $docComment) {
72+
return false;
73+
}
74+
75+
return false !== strpos($docComment, $annotation);
76+
}
5677
}

src/Util/ClassSourceManipulator.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\Common\Collections\ArrayCollection;
1515
use Doctrine\Common\Collections\Collection;
1616
use PhpParser\BuilderHelpers;
17+
use PhpParser\Comment\Doc;
1718
use PhpParser\Lexer;
1819
use PhpParser\Node;
1920
use PhpParser\Parser;
@@ -270,6 +271,44 @@ public function addProperty(string $name, array $annotationLines = [], $defaultV
270271
$this->addNodeAfterProperties($newPropertyNode);
271272
}
272273

274+
public function addAnnotationToClass(string $annotationClass, array $options)
275+
{
276+
$annotationClassAlias = $this->addUseStatementIfNecessary($annotationClass);
277+
$docComment = $this->getClassNode()->getDocComment();
278+
279+
$docLines = $docComment ? explode("\n", $docComment->getText()) : [];
280+
if (0 === \count($docLines)) {
281+
$docLines = ['/**', ' */'];
282+
} elseif (1 === \count($docLines)) {
283+
// /** inline doc syntax */
284+
// imperfect way to try to find where to split the lines
285+
$endOfOpening = strpos($docLines[0], '* ');
286+
$endingPosition = strrpos($docLines[0], ' *', $endOfOpening);
287+
$extraComments = trim(substr($docLines[0], $endOfOpening + 2, $endingPosition - $endOfOpening - 2));
288+
$newDocLines = [
289+
substr($docLines[0], 0, $endOfOpening + 1),
290+
];
291+
292+
if ($extraComments) {
293+
$newDocLines[] = ' * '.$extraComments;
294+
}
295+
296+
$newDocLines[] = substr($docLines[0], $endingPosition);
297+
$docLines = $newDocLines;
298+
}
299+
300+
array_splice(
301+
$docLines,
302+
\count($docLines) - 1,
303+
0,
304+
' * '.$this->buildAnnotationLine('@'.$annotationClassAlias, $options)
305+
);
306+
307+
$docComment = new Doc(implode("\n", $docLines));
308+
$this->getClassNode()->setDocComment($docComment);
309+
$this->updateSourceCodeFromNewStmts();
310+
}
311+
273312
private function addCustomGetter(string $propertyName, string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = [], $typeCast = null)
274313
{
275314
$propertyFetch = new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName);

tests/Maker/FunctionalTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,7 @@ function (string $output, string $directory) {
695695
// username field guessed
696696
// password guessed
697697
// firewall name guessed
698+
'', // yes to add UniqueEntity
698699
'', // yes authenticate after
699700
// 1 authenticator will be guessed
700701
])
@@ -710,6 +711,7 @@ function (string $output, string $directory) {
710711
'App\\Entity\\User',
711712
'emailAlt', // username field
712713
'passwordAlt', // password field
714+
'n', // no UniqueEntity
713715
'', // yes authenticate after
714716
'main', // firewall
715717
'1' // authenticator
@@ -721,6 +723,7 @@ function (string $output, string $directory) {
721723
$this->getMakerInstance(MakeRegistrationForm::class),
722724
[
723725
// all basic data guessed
726+
'y', // add UniqueEntity
724727
'n', // no authenticate after
725728
'app_anonymous', // route name to redirect to
726729
])

tests/Util/ClassSourceManipulatorTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,4 +565,136 @@ public function testAddMethodWithBody()
565565

566566
$this->assertSame($expectedSource, $manipulator->getSourceCode());
567567
}
568+
569+
/**
570+
* @dataProvider getTestsForAddAnnotationToClass
571+
*/
572+
public function testAddAnnotationToClass(string $source, string $expectedSource)
573+
{
574+
$manipulator = new ClassSourceManipulator($source);
575+
$manipulator->addAnnotationToClass('Bar\\SomeAnnotation', [
576+
'message' => 'Foo'
577+
]);
578+
579+
$this->assertEquals($expectedSource, $manipulator->getSourceCode());
580+
}
581+
582+
public function getTestsForAddAnnotationToClass()
583+
{
584+
yield 'no_doc_block' => [
585+
<<<EOF
586+
<?php
587+
588+
namespace Acme;
589+
590+
class Foo
591+
{
592+
}
593+
EOF
594+
,
595+
<<<EOF
596+
<?php
597+
598+
namespace Acme;
599+
600+
use Bar\SomeAnnotation;
601+
602+
/**
603+
* @SomeAnnotation(message="Foo")
604+
*/
605+
class Foo
606+
{
607+
}
608+
EOF
609+
];
610+
611+
yield 'normal_doc_block' => [
612+
<<<EOF
613+
<?php
614+
615+
namespace Acme;
616+
617+
/**
618+
* I'm a class!
619+
*/
620+
class Foo
621+
{
622+
}
623+
EOF
624+
,
625+
<<<EOF
626+
<?php
627+
628+
namespace Acme;
629+
630+
use Bar\SomeAnnotation;
631+
632+
/**
633+
* I'm a class!
634+
* @SomeAnnotation(message="Foo")
635+
*/
636+
class Foo
637+
{
638+
}
639+
EOF
640+
];
641+
642+
yield 'simple_inline_doc_block' => [
643+
<<<EOF
644+
<?php
645+
646+
namespace Acme;
647+
648+
/** I'm a class! */
649+
class Foo
650+
{
651+
}
652+
EOF
653+
,
654+
<<<EOF
655+
<?php
656+
657+
namespace Acme;
658+
659+
use Bar\SomeAnnotation;
660+
661+
/**
662+
* I'm a class!
663+
* @SomeAnnotation(message="Foo")
664+
*/
665+
class Foo
666+
{
667+
}
668+
EOF
669+
];
670+
671+
yield 'weird_inline_doc_block' => [
672+
<<<EOF
673+
<?php
674+
675+
namespace Acme;
676+
677+
/** **I'm a class!** ***/
678+
class Foo
679+
{
680+
}
681+
EOF
682+
,
683+
<<<EOF
684+
<?php
685+
686+
namespace Acme;
687+
688+
use Bar\SomeAnnotation;
689+
690+
/**
691+
* **I'm a class!**
692+
* @SomeAnnotation(message="Foo")
693+
***/
694+
class Foo
695+
{
696+
}
697+
EOF
698+
];
699+
}
568700
}

tests/fixtures/MakeRegistrationFormEntity/tests/RegistrationFormTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ public function testRegistrationValidationError()
5151
$crawler = $client->request('GET', '/register');
5252
$form = $crawler->selectButton('Register')->form();
5353
$form['registration_form[email]'] = '[email protected]';
54-
$form['registration_form[password]'] = 'foo';
54+
$form['registration_form[plainPassword]'] = 'foo';
5555
$client->submit($form);
5656

5757
$this->assertSame(200, $client->getResponse()->getStatusCode());
58+
$this->assertContains(
59+
'There is already an account with this email',
60+
$client->getResponse()->getContent()
61+
);
5862
$this->assertContains(
5963
'Your password should be at least 6 characters',
6064
$client->getResponse()->getContent()
6165
);
62-
$client->followRedirect();
63-
$this->assertSame(200, $client->getResponse()->getStatusCode());
64-
$this->assertSame('Page Success', $client->getResponse()->getContent());
6566
}
6667
}

0 commit comments

Comments
 (0)