Skip to content

Commit 8132374

Browse files
committed
WIP - introduce reset password maker
1 parent 17ac560 commit 8132374

11 files changed

+660
-0
lines changed

src/Maker/MakeResetPassword.php

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\MakerBundle\Maker;
4+
5+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
6+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
7+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
8+
use Symfony\Bundle\MakerBundle\FileManager;
9+
use Symfony\Bundle\MakerBundle\Generator;
10+
use Symfony\Bundle\MakerBundle\InputConfiguration;
11+
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
12+
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
13+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
14+
use Symfony\Bundle\MakerBundle\Validator;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Question\Question;
19+
use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle;
20+
21+
class MakeResetPassword
22+
{
23+
/**
24+
* @var FileManager
25+
*/
26+
private $fileManager;
27+
28+
public function __construct(FileManager $fileManager)
29+
{
30+
$this->fileManager = $fileManager;
31+
}
32+
33+
public static function getCommandName(): string
34+
{
35+
return 'make:reset-password';
36+
}
37+
38+
public function configureCommand(
39+
Command $command,
40+
InputConfiguration $inputConfig
41+
) {
42+
$command
43+
->setDescription('Create controller, entity, and repositories for use with SymfonyCasts Reset Password Bundle.')
44+
;
45+
}
46+
47+
public function configureDependencies(DependencyBuilder $dependencies)
48+
{
49+
$dependencies->addClassDependency(SymfonyCastsResetPasswordBundle::class, 'symfonycasts/reset-password-bundle');
50+
}
51+
52+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
53+
{
54+
$io->title('Reset Password Bundle Requires:');
55+
$requirements[] = '1) A user entity has been created.';
56+
$requirements[] = '2) The user entity contains an email property with a getter method.';
57+
$requirements[] = '3) A user repository exists for the user entity.'."\n";
58+
$requirements[] = '<fg=yellow>bin/console make:user</> will generate the user entity and it\'s repository...'."\n";
59+
$io->text($requirements);
60+
61+
// initialize arguments & commands that are internal (i.e. meant only to be asked)
62+
$command
63+
->addArgument('from-email-address', InputArgument::REQUIRED)
64+
->addArgument('from-email-name', InputArgument::REQUIRED)
65+
->addArgument('controller-reset-success-redirect', InputArgument::REQUIRED)
66+
->addArgument('user-class')
67+
->addArgument('email-property-name')
68+
->addArgument('email-getter')
69+
->addArgument('password-setter')
70+
;
71+
72+
$io->section('- Email Templates -');
73+
$emailText[] = 'Please answer the following questions that will be used to generate the email templates.';
74+
$emailText[] = 'If you are unsure of what these answers should be, that\'s ok.';
75+
$emailText[] = 'You can change these later after the templates have been generated.';
76+
$io->text($emailText);
77+
78+
$emailAddressQuestion = new Question('What email address will be used to send reset confirmations? I.e. [email protected]');
79+
$emailAddressQuestion->setValidator(
80+
static function ($answer) {
81+
// @TODO - In maker-bundle PR, introduce new native Validator::emailAddress()...
82+
$validatedAnswer = filter_var($answer, FILTER_VALIDATE_EMAIL);
83+
84+
if (!$validatedAnswer) {
85+
throw new RuntimeCommandException(sprintf('"%s" is not a valid email address.', $answer));
86+
}
87+
88+
return $validatedAnswer;
89+
}
90+
);
91+
92+
$input->setArgument('from-email-address', $io->askQuestion($emailAddressQuestion));
93+
$input->setArgument('from-email-name', $io->ask(
94+
'What name will be associated with the email address used to send password reset confirmations? I.e. John Doe or Your Company, LLC.',
95+
null,
96+
[Validator::class, 'notBlank']
97+
)
98+
);
99+
100+
$interactiveSecurityHelper = new InteractiveSecurityHelper();
101+
102+
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
103+
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build the reset password form.');
104+
}
105+
106+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
107+
$securityData = $manipulator->getData();
108+
$providersData = $securityData['security']['providers'] ?? [];
109+
110+
$input->setArgument(
111+
'user-class',
112+
$userClass = $interactiveSecurityHelper->guessUserClass(
113+
$io,
114+
$providersData,
115+
'Enter the User class that should be used with the "forgotten password" feature (e.g. <fg=yellow>App\\Entity\\User</>)'
116+
)
117+
);
118+
119+
$io->section('- Controller Template -');
120+
$io->comment('<fg=yellow>A named route is required for redirection after a successful reset. Even routes that do not yet exist can be used here.</>');
121+
$input->setArgument('controller-reset-success-redirect', $io->ask(
122+
'What route should users be redirected to after their password has been successfully reset?',
123+
'app_home',
124+
[Validator::class, 'notBlank']
125+
)
126+
);
127+
128+
$io->text(sprintf('Implementing reset password for <info>%s</info>', $userClass));
129+
130+
$input->setArgument(
131+
'email-property-name',
132+
$interactiveSecurityHelper->guessEmailField($io, $userClass)
133+
);
134+
$input->setArgument(
135+
'email-getter',
136+
$interactiveSecurityHelper->guessEmailGetter($io, $userClass)
137+
);
138+
$input->setArgument(
139+
'password-setter',
140+
$interactiveSecurityHelper->guessPasswordSetter($io, $userClass)
141+
);
142+
}
143+
144+
public function generate(
145+
InputInterface $input,
146+
ConsoleStyle $io,
147+
Generator $generator
148+
) {
149+
$userClass = $input->getArgument('user-class');
150+
$userClassNameDetails = $generator->createClassNameDetails(
151+
'\\'.$userClass,
152+
'Entity\\'
153+
);
154+
155+
$controllerClassNameDetails = $generator->createClassNameDetails(
156+
'ResetPasswordController',
157+
'Controller\\'
158+
);
159+
160+
$requestClassNameDetails = $generator->createClassNameDetails(
161+
'ResetPasswordRequest',
162+
'Entity\\'
163+
);
164+
165+
$repositoryClassNameDetails = $generator->createClassNameDetails(
166+
'ResetPasswordRequestRepository',
167+
'Repository\\'
168+
);
169+
170+
$requestFormTypeClassNameDetails = $generator->createClassNameDetails(
171+
'ResetPasswordRequestFormType',
172+
'Form\\'
173+
);
174+
175+
$changePasswordFormTypeClassNameDetails = $generator->createClassNameDetails(
176+
'ChangePasswordFormType',
177+
'Form\\'
178+
);
179+
180+
$templatePath = 'src/Resource/templates/';
181+
182+
$generator->generateController(
183+
$controllerClassNameDetails->getFullName(),
184+
$templatePath.'ResetPasswordController.tpl.php',
185+
[
186+
'user_full_class_name' => $userClassNameDetails->getFullName(),
187+
'user_class_name' => $userClassNameDetails->getShortName(),
188+
'request_form_type_full_class_name' => $requestFormTypeClassNameDetails->getFullName(),
189+
'request_form_type_class_name' => $requestFormTypeClassNameDetails->getShortName(),
190+
'reset_form_type_full_class_name' => $changePasswordFormTypeClassNameDetails->getFullName(),
191+
'reset_form_type_class_name' => $changePasswordFormTypeClassNameDetails->getShortName(),
192+
'password_setter' => $input->getArgument('password-setter'),
193+
'success_redirect_route' => $input->getArgument('controller-reset-success-redirect'),
194+
'from_email' => $input->getArgument('from-email-address'),
195+
'from_email_name' => $input->getArgument('from-email-name'),
196+
'email_getter' => $input->getArgument('email-getter'),
197+
]
198+
);
199+
200+
$generator->generateClass(
201+
$requestClassNameDetails->getFullName(),
202+
$templatePath.'ResetPasswordRequest.tpl.php',
203+
[
204+
'repository_class_name' => $repositoryClassNameDetails->getFullName(),
205+
'user_full_class_name' => $userClassNameDetails->getFullName(),
206+
]
207+
);
208+
209+
$generator->generateClass(
210+
$repositoryClassNameDetails->getFullName(),
211+
$templatePath.'ResetPasswordRequestRepository.tpl.php',
212+
[
213+
'request_class_full_name' => $requestClassNameDetails->getFullName(),
214+
'request_class_name' => $requestClassNameDetails->getShortName(),
215+
]
216+
);
217+
218+
if (!$this->fileManager->fileExists($path = 'config/packages/reset_password.yaml')) {
219+
throw new RuntimeCommandException(sprintf('The file "%s" does not exist. This command needs that file to accurately build the reset password config.', $path));
220+
}
221+
222+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
223+
$data = $manipulator->getData();
224+
225+
$data['symfonycasts_reset_password'] = ['request_password_repository' => $repositoryClassNameDetails->getFullName()];
226+
227+
$manipulator->setData($data);
228+
229+
$generator->dumpFile($path, $manipulator->getContents());
230+
231+
$generator->generateClass(
232+
$requestFormTypeClassNameDetails->getFullName(),
233+
$templatePath.'ResetPasswordRequestFormType.tpl.php',
234+
[
235+
'email_field' => $input->getArgument('email-property-name'),
236+
]
237+
);
238+
239+
$generator->generateClass(
240+
$changePasswordFormTypeClassNameDetails->getFullName(),
241+
$templatePath.'ChangePasswordFormType.tpl.php'
242+
);
243+
244+
$generator->generateTemplate(
245+
'reset_password/check_email.html.twig',
246+
$templatePath.'twig_check_email.tpl.php',
247+
[]
248+
);
249+
250+
$generator->generateTemplate(
251+
'reset_password/email.html.twig',
252+
$templatePath.'twig_email.tpl.php',
253+
[]
254+
);
255+
256+
$generator->generateTemplate(
257+
'reset_password/request.html.twig',
258+
$templatePath.'twig_request.tpl.php',
259+
[
260+
'email_field' => $input->getArgument('email-property-name'),
261+
]
262+
);
263+
264+
$generator->generateTemplate(
265+
'reset_password/reset.html.twig',
266+
$templatePath.'twig_reset.tpl.php',
267+
[]
268+
);
269+
270+
$generator->writeChanges();
271+
272+
$this->successMessage($input, $io, $controllerClassNameDetails->getFullName());
273+
}
274+
275+
private function successMessage(InputInterface $input, ConsoleStyle $io, string $userClassName): void
276+
{
277+
$io->title('The src files required by Reset Password Bundle have been successfully created.');
278+
$closing[] = \sprintf('Users will be redirect to <info>%s</info> after a password reset is successfully completed.', $input->getArgument('controller-reset-success-redirect'));
279+
$closing[] = \sprintf('The route can be changed later in <info>%s::reset()</info>', $userClassName);
280+
$closing[] = 'The "from" email address and name values for the email template can be changed in <info>ResetPasswordController::processRequestForm()</info>';
281+
$closing[] = 'Ensure <info>MAILER_DSN</info> has the correct host for sending emails.';
282+
$io->text($closing);
283+
}
284+
}
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)