Skip to content

Commit bb1438b

Browse files
jrushlowweaverryan
authored andcommitted
create maker for symfony casts reset password bundle
1 parent 866f01a commit bb1438b

File tree

26 files changed

+1070
-0
lines changed

26 files changed

+1070
-0
lines changed

src/Maker/MakeResetPassword.php

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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 Symfony\Bundle\MakerBundle\ConsoleStyle;
15+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
16+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
17+
use Symfony\Bundle\MakerBundle\FileManager;
18+
use Symfony\Bundle\MakerBundle\Generator;
19+
use Symfony\Bundle\MakerBundle\InputConfiguration;
20+
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
21+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
22+
use Symfony\Bundle\MakerBundle\Validator;
23+
use Symfony\Component\Console\Command\Command;
24+
use Symfony\Component\Console\Input\InputArgument;
25+
use Symfony\Component\Console\Input\InputInterface;
26+
use Symfony\Component\Yaml\Yaml;
27+
use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle;
28+
29+
/**
30+
* @author Romaric Drigon <[email protected]>
31+
* @author Jesse Rushlow <[email protected]>
32+
* @author Ryan Weaver <[email protected]>
33+
*
34+
* @internal
35+
* @final
36+
*/
37+
class MakeResetPassword extends AbstractMaker
38+
{
39+
private $fileManager;
40+
41+
public function __construct(FileManager $fileManager)
42+
{
43+
$this->fileManager = $fileManager;
44+
}
45+
46+
public static function getCommandName(): string
47+
{
48+
return 'make:reset-password';
49+
}
50+
51+
public function configureCommand(Command $command, InputConfiguration $inputConfig)
52+
{
53+
$command
54+
->setDescription('Create controller, entity, and repositories for use with symfonycasts/reset-password-bundle.')
55+
;
56+
}
57+
58+
public function configureDependencies(DependencyBuilder $dependencies)
59+
{
60+
$dependencies->addClassDependency(SymfonyCastsResetPasswordBundle::class, 'symfonycasts/reset-password-bundle');
61+
}
62+
63+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
64+
{
65+
$io->title('Let\'s make a password reset feature!');
66+
67+
// initialize arguments & commands that are internal (i.e. meant only to be asked)
68+
$command
69+
->addArgument('from-email-address', InputArgument::REQUIRED)
70+
->addArgument('from-email-name', InputArgument::REQUIRED)
71+
->addArgument('controller-reset-success-redirect', InputArgument::REQUIRED)
72+
->addArgument('user-class')
73+
->addArgument('email-property-name')
74+
->addArgument('email-getter')
75+
->addArgument('password-setter')
76+
;
77+
78+
$interactiveSecurityHelper = new InteractiveSecurityHelper();
79+
80+
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
81+
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build the reset password form.');
82+
}
83+
84+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
85+
$securityData = $manipulator->getData();
86+
$providersData = $securityData['security']['providers'] ?? [];
87+
88+
$input->setArgument(
89+
'user-class',
90+
$userClass = $interactiveSecurityHelper->guessUserClass(
91+
$io,
92+
$providersData,
93+
'What is the User entity that should be used with the "forgotten password" feature? (e.g. <fg=yellow>App\\Entity\\User</>)'
94+
)
95+
);
96+
97+
$input->setArgument(
98+
'email-property-name',
99+
$interactiveSecurityHelper->guessEmailField($io, $userClass)
100+
);
101+
$input->setArgument(
102+
'email-getter',
103+
$interactiveSecurityHelper->guessEmailGetter($io, $userClass)
104+
);
105+
$input->setArgument(
106+
'password-setter',
107+
$interactiveSecurityHelper->guessPasswordSetter($io, $userClass)
108+
);
109+
110+
$io->text(sprintf('Implementing reset password for <info>%s</info>', $userClass));
111+
112+
$io->section('- ResetPasswordController -');
113+
$io->text('A named route is used for redirecting after a successful reset. Even a route that does not exist yet can be used here.');
114+
$input->setArgument('controller-reset-success-redirect', $io->ask(
115+
'What route should users be redirected to after their password has been successfully reset?',
116+
'app_home',
117+
[Validator::class, 'notBlank']
118+
)
119+
);
120+
121+
$io->section('- Email Templates -');
122+
$emailText[] = 'These are used to generate the email code. Don\'t worry, you can change them in the code later!';
123+
$io->text($emailText);
124+
125+
$input->setArgument('from-email-address', $io->ask(
126+
'What email address will be used to send reset confirmations? e.g. [email protected]',
127+
null,
128+
[Validator::class, 'validateEmailAddress']
129+
));
130+
131+
$input->setArgument('from-email-name', $io->ask(
132+
'What "name" should be associated with that email address? e.g. "Acme Mail Bot"',
133+
null,
134+
[Validator::class, 'notBlank']
135+
)
136+
);
137+
}
138+
139+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
140+
{
141+
$userClass = $input->getArgument('user-class');
142+
$userClassNameDetails = $generator->createClassNameDetails(
143+
'\\'.$userClass,
144+
'Entity\\'
145+
);
146+
147+
$controllerClassNameDetails = $generator->createClassNameDetails(
148+
'ResetPasswordController',
149+
'Controller\\'
150+
);
151+
152+
$requestClassNameDetails = $generator->createClassNameDetails(
153+
'ResetPasswordRequest',
154+
'Entity\\'
155+
);
156+
157+
$repositoryClassNameDetails = $generator->createClassNameDetails(
158+
'ResetPasswordRequestRepository',
159+
'Repository\\'
160+
);
161+
162+
$requestFormTypeClassNameDetails = $generator->createClassNameDetails(
163+
'ResetPasswordRequestFormType',
164+
'Form\\'
165+
);
166+
167+
$changePasswordFormTypeClassNameDetails = $generator->createClassNameDetails(
168+
'ChangePasswordFormType',
169+
'Form\\'
170+
);
171+
172+
$generator->generateController(
173+
$controllerClassNameDetails->getFullName(),
174+
'resetPassword/ResetPasswordController.tpl.php',
175+
[
176+
'user_full_class_name' => $userClassNameDetails->getFullName(),
177+
'user_class_name' => $userClassNameDetails->getShortName(),
178+
'request_form_type_full_class_name' => $requestFormTypeClassNameDetails->getFullName(),
179+
'request_form_type_class_name' => $requestFormTypeClassNameDetails->getShortName(),
180+
'reset_form_type_full_class_name' => $changePasswordFormTypeClassNameDetails->getFullName(),
181+
'reset_form_type_class_name' => $changePasswordFormTypeClassNameDetails->getShortName(),
182+
'password_setter' => $input->getArgument('password-setter'),
183+
'success_redirect_route' => $input->getArgument('controller-reset-success-redirect'),
184+
'from_email' => $input->getArgument('from-email-address'),
185+
'from_email_name' => $input->getArgument('from-email-name'),
186+
'email_getter' => $input->getArgument('email-getter'),
187+
]
188+
);
189+
190+
$generator->generateClass(
191+
$requestClassNameDetails->getFullName(),
192+
'resetPassword/ResetPasswordRequest.tpl.php',
193+
[
194+
'repository_class_name' => $repositoryClassNameDetails->getFullName(),
195+
'user_full_class_name' => $userClassNameDetails->getFullName(),
196+
]
197+
);
198+
199+
$generator->generateClass(
200+
$repositoryClassNameDetails->getFullName(),
201+
'resetPassword/ResetPasswordRequestRepository.tpl.php',
202+
[
203+
'request_class_full_name' => $requestClassNameDetails->getFullName(),
204+
'request_class_name' => $requestClassNameDetails->getShortName(),
205+
]
206+
);
207+
208+
$this->setBundleConfig($io, $generator, $repositoryClassNameDetails->getFullName());
209+
210+
$generator->generateClass(
211+
$requestFormTypeClassNameDetails->getFullName(),
212+
'resetPassword/ResetPasswordRequestFormType.tpl.php',
213+
[
214+
'email_field' => $input->getArgument('email-property-name'),
215+
]
216+
);
217+
218+
$generator->generateClass(
219+
$changePasswordFormTypeClassNameDetails->getFullName(),
220+
'resetPassword/ChangePasswordFormType.tpl.php'
221+
);
222+
223+
$generator->generateTemplate(
224+
'reset_password/check_email.html.twig',
225+
'resetPassword/twig_check_email.tpl.php'
226+
);
227+
228+
$generator->generateTemplate(
229+
'reset_password/email.html.twig',
230+
'resetPassword/twig_email.tpl.php'
231+
);
232+
233+
$generator->generateTemplate(
234+
'reset_password/request.html.twig',
235+
'resetPassword/twig_request.tpl.php',
236+
[
237+
'email_field' => $input->getArgument('email-property-name'),
238+
]
239+
);
240+
241+
$generator->generateTemplate(
242+
'reset_password/reset.html.twig',
243+
'resetPassword/twig_reset.tpl.php'
244+
);
245+
246+
$generator->writeChanges();
247+
248+
$this->writeSuccessMessage($io);
249+
$this->successMessage($input, $io, $requestClassNameDetails->getFullName());
250+
}
251+
252+
private function setBundleConfig(ConsoleStyle $io, Generator $generator, string $repositoryClassFullName)
253+
{
254+
$configFileExists = $this->fileManager->fileExists($path = 'config/packages/reset_password.yaml');
255+
256+
/*
257+
* reset_password.yaml does not exist, we assume flex was present when
258+
* the bundle was installed & a customized configuration is in use.
259+
* Remind the developer to set the repository class accordingly.
260+
*/
261+
if (!$configFileExists) {
262+
$io->text(sprintf('We can\'t find %s. That\'s ok, you probably have a customized configuration.', $path));
263+
$io->text('Just remember to set the <fg=yellow>request_password_repository</> in your configuration.');
264+
$io->newLine();
265+
266+
return;
267+
}
268+
269+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
270+
$data = $manipulator->getData();
271+
272+
$symfonyCastsKey = 'symfonycasts_reset_password';
273+
274+
/*
275+
* reset_password.yaml exists, and was probably created by flex;
276+
* Let's replace it with a "clean" file.
277+
*/
278+
if (1 >= \count($data[$symfonyCastsKey])) {
279+
$yaml = [
280+
$symfonyCastsKey => [
281+
'request_password_repository' => $repositoryClassFullName,
282+
],
283+
];
284+
285+
$generator->dumpFile($path, Yaml::dump($yaml));
286+
287+
return;
288+
}
289+
290+
/*
291+
* reset_password.yaml exists and appears to have been customized
292+
* before running make:reset-password. Let's just change the repository
293+
* value and preserve everything else.
294+
*/
295+
$data[$symfonyCastsKey]['request_password_repository'] = $repositoryClassFullName;
296+
297+
$manipulator->setData($data);
298+
299+
$generator->dumpFile($path, $manipulator->getContents());
300+
}
301+
302+
private function successMessage(InputInterface $input, ConsoleStyle $io, string $requestClassName)
303+
{
304+
$closing[] = 'Next:';
305+
$closing[] = sprintf(' 1) Run <fg=yellow>"php bin/console make:migration"</> to generate a migration for the new <fg=yellow>"%s"</> entity.', $requestClassName);
306+
$closing[] = ' 2) Review forms in <fg=yellow>"src/Form"</> to customize validation and labels.';
307+
$closing[] = ' 3) Review and customize the templates in <fg=yellow>`templates/reset_password`</>.';
308+
$closing[] = ' 4) Make sure your <fg=yellow>MAILER_DSN</> env var has the correct settings.';
309+
310+
$io->text($closing);
311+
$io->newLine();
312+
$io->text('Then open your browser, go to "/reset-password" and enjoy!');
313+
$io->newLine();
314+
}
315+
}

src/Resources/config/makers.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
<tag name="maker.command" />
6969
</service>
7070

71+
<service id="maker.maker.make_reset_password" class="Symfony\Bundle\MakerBundle\Maker\MakeResetPassword">
72+
<argument type="service" id="maker.file_manager" />
73+
<tag name="maker.command" />
74+
</service>
75+
7176
<service id="maker.maker.make_serializer_encoder" class="Symfony\Bundle\MakerBundle\Maker\MakeSerializerEncoder">
7277
<tag name="maker.command" />
7378
</service>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace ?>;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
7+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
8+
use Symfony\Component\Form\FormBuilderInterface;
9+
use Symfony\Component\OptionsResolver\OptionsResolver;
10+
use Symfony\Component\Validator\Constraints\Length;
11+
use Symfony\Component\Validator\Constraints\NotBlank;
12+
13+
class <?= $class_name ?> extends AbstractType
14+
{
15+
public function buildForm(FormBuilderInterface $builder, array $options): void
16+
{
17+
$builder
18+
->add('plainPassword', RepeatedType::class, [
19+
'type' => PasswordType::class,
20+
'first_options' => [
21+
'constraints' => [
22+
new NotBlank([
23+
'message' => 'Please enter a password',
24+
]),
25+
new Length([
26+
'min' => 6,
27+
'minMessage' => 'Your password should be at least {{ limit }} characters',
28+
// max length allowed by Symfony for security reasons
29+
'max' => 4096,
30+
]),
31+
],
32+
'label' => 'New password',
33+
],
34+
'second_options' => [
35+
'label' => 'Repeat Password',
36+
],
37+
'invalid_message' => 'The password fields must match.',
38+
// Instead of being set onto the object directly,
39+
// this is read and encoded in the controller
40+
'mapped' => false,
41+
])
42+
;
43+
}
44+
45+
public function configureOptions(OptionsResolver $resolver): void
46+
{
47+
$resolver->setDefaults([]);
48+
}
49+
}

0 commit comments

Comments
 (0)