Skip to content

Commit 14ade82

Browse files
committed
Adding the initial version of make:registration-form 🎉
1 parent b7699a1 commit 14ade82

File tree

15 files changed

+672
-7
lines changed

15 files changed

+672
-7
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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;
13+
14+
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
15+
use Doctrine\Common\Annotations\Annotation;
16+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
17+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
18+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
19+
use Symfony\Bundle\MakerBundle\FileManager;
20+
use Symfony\Bundle\MakerBundle\Generator;
21+
use Symfony\Bundle\MakerBundle\InputConfiguration;
22+
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
23+
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
24+
use Symfony\Bundle\MakerBundle\Str;
25+
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
26+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
27+
use Symfony\Bundle\SecurityBundle\SecurityBundle;
28+
use Symfony\Bundle\TwigBundle\TwigBundle;
29+
use Symfony\Component\Console\Command\Command;
30+
use Symfony\Component\Console\Input\InputArgument;
31+
use Symfony\Component\Console\Input\InputInterface;
32+
use Symfony\Component\Console\Input\InputOption;
33+
use Symfony\Component\Form\AbstractType;
34+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
35+
use Symfony\Component\Validator\Constraints\NotBlank;
36+
use Symfony\Component\Validator\Validation;
37+
38+
/**
39+
* @author Ryan Weaver <[email protected]>
40+
*/
41+
final class MakeRegistrationForm extends AbstractMaker
42+
{
43+
private $fileManager;
44+
45+
private $formTypeRenderer;
46+
47+
public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer)
48+
{
49+
$this->fileManager = $fileManager;
50+
$this->formTypeRenderer = $formTypeRenderer;
51+
}
52+
53+
public static function getCommandName(): string
54+
{
55+
return 'make:registration-form';
56+
}
57+
58+
public function configureCommand(Command $command, InputConfiguration $inputConf)
59+
{
60+
$command
61+
->setDescription('Creates a new registration form system')
62+
->addArgument('user-class', InputArgument::OPTIONAL, 'Full user class')
63+
->addArgument('username-field', InputArgument::OPTIONAL, 'Field on your User class used to login')
64+
->addArgument('password-field', InputArgument::OPTIONAL, 'Field on your User class that stores the hashed password')
65+
->addOption('auto-login-authenticator', null, InputOption::VALUE_REQUIRED, 'Authenticator class to use for logging in')
66+
->addOption('firewall-name', null, InputOption::VALUE_REQUIRED, 'Firewall key used for authentication')
67+
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeRegistrationForm.txt'))
68+
;
69+
70+
$inputConf->setArgumentAsNonInteractive('user-class');
71+
$inputConf->setArgumentAsNonInteractive('username-field');
72+
$inputConf->setArgumentAsNonInteractive('password-field');
73+
}
74+
75+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
76+
{
77+
$interactiveSecurityHelper = new InteractiveSecurityHelper();
78+
79+
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
80+
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build your registration form.');
81+
}
82+
83+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
84+
$securityData = $manipulator->getData();
85+
$providersData = $securityData['security']['providers'];
86+
87+
$input->setArgument(
88+
'user-class',
89+
$userClass = $interactiveSecurityHelper->guessUserClass(
90+
$io,
91+
$providersData,
92+
'Enter the User class that you want to create during registration (e.g. <fg=yellow>App\\Entity\\User</>)'
93+
)
94+
);
95+
$io->text(sprintf('Creating a registration form for <info>%s</info>', $userClass));
96+
97+
$input->setArgument(
98+
'username-field',
99+
$interactiveSecurityHelper->guessUserNameField($io, $userClass, $providersData)
100+
);
101+
102+
$input->setArgument(
103+
'password-field',
104+
$interactiveSecurityHelper->guessPasswordField($io, $userClass)
105+
);
106+
107+
if ($io->confirm('Do you want to automatically authenticate the user after registration?')) {
108+
$this->interactAuthenticatorQuestions(
109+
$input,
110+
$io,
111+
$interactiveSecurityHelper,
112+
$securityData,
113+
$command
114+
);
115+
}
116+
}
117+
118+
private function interactAuthenticatorQuestions(InputInterface $input, ConsoleStyle $io, InteractiveSecurityHelper $interactiveSecurityHelper, array $securityData, Command $command)
119+
{
120+
$firewallsData = $securityData['security']['firewalls'] ?? [];
121+
$firewallName = $interactiveSecurityHelper->guessFirewallName(
122+
$io,
123+
$securityData,
124+
'Which firewall key in security.yaml holds the authenticator you want to use for logging in?'
125+
);
126+
127+
if (!isset($firewallsData[$firewallName])) {
128+
$io->note('No firewalls found - skipping authentication after registration. You might want to configure your security before running this command.');
129+
130+
return;
131+
}
132+
133+
$input->setOption('firewall-name', $firewallName);
134+
135+
// get list of guard authenticators
136+
$authenticatorClasses = $interactiveSecurityHelper->getAuthenticatorClasses($firewallsData[$firewallName]);
137+
if (empty($authenticatorClasses)) {
138+
$io->note('No Guard authenticators found - so your user won\'t be automatically authenticated after registering.');
139+
}
140+
141+
$input->setOption(
142+
'auto-login-authenticator',
143+
count($authenticatorClasses) === 1 ? $authenticatorClasses[0] : $io->choice(
144+
'Which authenticator\'s onAuthenticationSuccess() should be used after logging in?',
145+
$authenticatorClasses
146+
)
147+
);
148+
}
149+
150+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
151+
{
152+
$userClass = $input->getArgument('user-class');
153+
$userClassNameDetails = $generator->createClassNameDetails(
154+
'\\'.$userClass,
155+
'Entity\\'
156+
);
157+
158+
$usernameField = $input->getArgument('username-field');
159+
$formClassDetails = $this->generateFormClass(
160+
$userClassNameDetails,
161+
$generator,
162+
$usernameField
163+
);
164+
165+
$controllerClassNameDetails = $generator->createClassNameDetails(
166+
'RegistrationController',
167+
'Controller\\'
168+
);
169+
170+
$authenticatorClassName = $input->getOption('auto-login-authenticator');
171+
$generator->generateController(
172+
$controllerClassNameDetails->getFullName(),
173+
'registration/RegistrationController.tpl.php',
174+
[
175+
'route_path' => '/register',
176+
'route_name' => 'app_register',
177+
'form_class_name' => $formClassDetails->getShortName(),
178+
'form_full_class_name' => $formClassDetails->getFullName(),
179+
'user_class_name' => $userClassNameDetails->getShortName(),
180+
'user_full_class_name' => $userClassNameDetails->getFullName(),
181+
'password_field' => $input->getArgument('password-field'),
182+
'authenticator_class_name' => $authenticatorClassName ? Str::getShortClassName($authenticatorClassName) : null,
183+
'authenticator_full_class_name' => $authenticatorClassName,
184+
'firewall_name' => $input->getOption('firewall-name'),
185+
]
186+
);
187+
188+
$generator->generateFile(
189+
'templates/registration/register.html.twig',
190+
'registration/twig_template.tpl.php',
191+
[
192+
'username_field' => $usernameField,
193+
]
194+
);
195+
196+
$generator->writeChanges();
197+
198+
$this->writeSuccessMessage($io);
199+
$io->text('Next: Go to /register to check out your new form!');
200+
$io->text('Then, make any changes you need to the form, controller & template.');
201+
}
202+
203+
public function configureDependencies(DependencyBuilder $dependencies)
204+
{
205+
$dependencies->addClassDependency(
206+
// we only need doctrine/annotations, which contains
207+
// the recipe that loads annotation routes
208+
Annotation::class,
209+
'annotations'
210+
);
211+
212+
$dependencies->addClassDependency(
213+
AbstractType::class,
214+
'form'
215+
);
216+
217+
$dependencies->addClassDependency(
218+
Validation::class,
219+
'validator'
220+
);
221+
222+
$dependencies->addClassDependency(
223+
TwigBundle::class,
224+
'twig-bundle'
225+
);
226+
227+
$dependencies->addClassDependency(
228+
DoctrineBundle::class,
229+
'orm-pack'
230+
);
231+
232+
$dependencies->addClassDependency(
233+
SecurityBundle::class,
234+
'security'
235+
);
236+
}
237+
238+
private function generateFormClass(ClassNameDetails $userClassDetails, Generator $generator, string $usernameField)
239+
{
240+
$formClassDetails = $generator->createClassNameDetails(
241+
'RegistrationFormType',
242+
'Form\\'
243+
);
244+
245+
$formFields = [
246+
$usernameField => null,
247+
'plainPassword' => [
248+
'type' => PasswordType::class,
249+
'options' => [
250+
'mapped' => false,
251+
// TODO - NotBlank constraint
252+
]
253+
],
254+
];
255+
256+
$this->formTypeRenderer->render(
257+
$formClassDetails,
258+
$formFields,
259+
$userClassDetails
260+
);
261+
262+
return $formClassDetails;
263+
}
264+
}

‎src/Renderer/FormTypeRenderer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function render(ClassNameDetails $formClassDetails, array $formFields, Cl
2727

2828
if (isset($fieldTypeOptions['type'])) {
2929
$fieldTypeUseStatements[] = $fieldTypeOptions['type'];
30+
$fieldTypeOptions['type'] = Str::getShortClassName($fieldTypeOptions['type']);
3031
}
3132

3233
$fields[$name] = $fieldTypeOptions;

‎src/Resources/config/makers.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
<tag name="maker.command" />
5353
</service>
5454

55+
<service id="maker.maker.make_registration_form" class="Symfony\Bundle\MakerBundle\Maker\MakeRegistrationForm">
56+
<argument type="service" id="maker.file_manager" />
57+
<argument type="service" id="maker.renderer.form_type_renderer" />
58+
<tag name="maker.command" />
59+
</service>
60+
5561
<service id="maker.maker.make_serializer_encoder" class="Symfony\Bundle\MakerBundle\Maker\MakeSerializerEncoder">
5662
<tag name="maker.command" />
5763
</service>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The <info>%command.name%</info> command generates a complete registration form, controller & template.
2+
3+
<info>php %command.full_name%</info>
4+
5+
The command will ask for several pieces of information to build your form.

‎src/Resources/skeleton/form/Type.tpl.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace <?= $namespace ?>;
44

5-
<?php if (isset($bounded_full_class_name)): ?>
5+
<?php if ($bounded_full_class_name): ?>
66
use <?= $bounded_full_class_name ?>;
77
<?php endif ?>
88
use Symfony\Component\Form\AbstractType;
9+
<?php foreach ($field_type_use_statements as $className): ?>
10+
use <?= $className ?>;
11+
<?php endforeach; ?>
912
use Symfony\Component\Form\FormBuilderInterface;
1013
use Symfony\Component\OptionsResolver\OptionsResolver;
1114

@@ -14,16 +17,26 @@ class <?= $class_name ?> extends AbstractType
1417
public function buildForm(FormBuilderInterface $builder, array $options)
1518
{
1619
$builder
17-
<?php foreach ($form_fields as $form_field): ?>
20+
<?php foreach ($form_fields as $form_field => $typeOptions): ?>
21+
<?php if (null === $typeOptions['type'] && empty($typeOptions['options'])): ?>
1822
->add('<?= $form_field ?>')
23+
<?php elseif (null !== $typeOptions['type'] && empty($typeOptions['options'])): ?>
24+
->add('<?= $form_field ?>', <?= $typeOptions['type'] ?>::class)
25+
<?php else: ?>
26+
->add('<?= $form_field ?>', <?= $typeOptions['type'] ? ($typeOptions['type'].'::class') : 'null' ?>, [
27+
<?php foreach ($typeOptions['options'] as $key => $val): ?>
28+
'<?= $key; ?>' => <?= var_export($val, true) ?>,
29+
<?php endforeach; ?>
30+
])
31+
<?php endif; ?>
1932
<?php endforeach; ?>
2033
;
2134
}
2235

2336
public function configureOptions(OptionsResolver $resolver)
2437
{
2538
$resolver->setDefaults([
26-
<?php if (isset($bounded_full_class_name)): ?>
39+
<?php if ($bounded_full_class_name): ?>
2740
'data_class' => <?= $bounded_class_name ?>::class,
2841
<?php else: ?>
2942
// Configure your form options here

0 commit comments

Comments
 (0)