Skip to content

Commit b2294a6

Browse files
committed
Added a make:forgotten-password maker
1 parent 08fabae commit b2294a6

11 files changed

+751
-1
lines changed

src/Maker/MakeForgottenPassword.php

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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\Doctrine\ORMDependencyBuilder;
17+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
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\ClassNameDetails;
22+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
23+
use Symfony\Bundle\SecurityBundle\SecurityBundle;
24+
use Symfony\Bundle\TwigBundle\TwigBundle;
25+
use Symfony\Component\Console\Command\Command;
26+
use Symfony\Component\Console\Input\InputInterface;
27+
use Symfony\Component\Form\AbstractType;
28+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
29+
use Symfony\Component\Form\Extension\Core\Type\TextType;
30+
use Symfony\Component\Routing\RouterInterface;
31+
use Symfony\Bundle\MakerBundle\FileManager;
32+
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
33+
use Symfony\Component\Validator\Validation;
34+
use Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle;
35+
36+
/**
37+
* @author Romaric Drigon <[email protected]>
38+
*
39+
* @internal
40+
*/
41+
final class MakeForgottenPassword extends AbstractMaker
42+
{
43+
private $fileManager;
44+
45+
private $formTypeRenderer;
46+
47+
private $router;
48+
49+
public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer, RouterInterface $router)
50+
{
51+
$this->fileManager = $fileManager;
52+
$this->formTypeRenderer = $formTypeRenderer;
53+
$this->router = $router;
54+
}
55+
56+
public static function getCommandName(): string
57+
{
58+
return 'make:forgotten-password';
59+
}
60+
61+
public function configureCommand(Command $command, InputConfiguration $inputConfig)
62+
{
63+
$command
64+
->setDescription('Creates a "forgotten password" mechanism')
65+
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeForgottenPassword.txt'))
66+
;
67+
}
68+
69+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
70+
{
71+
// initialize arguments & commands that are internal (i.e. meant only to be asked)
72+
$command
73+
->addArgument('user-class')
74+
->addArgument('email-field')
75+
->addArgument('email-getter')
76+
->addArgument('password-setter')
77+
;
78+
79+
$interactiveSecurityHelper = new InteractiveSecurityHelper();
80+
81+
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
82+
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build the forgotten password form.');
83+
}
84+
85+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
86+
$securityData = $manipulator->getData();
87+
$providersData = $securityData['security']['providers'] ?? [];
88+
89+
$input->setArgument(
90+
'user-class',
91+
$userClass = $interactiveSecurityHelper->guessUserClass(
92+
$io,
93+
$providersData,
94+
'Enter the User class that should be used with the "forgotten password" feature (e.g. <fg=yellow>App\\Entity\\User</>)'
95+
)
96+
);
97+
$io->text(sprintf('Implementing forgotten password for <info>%s</info>', $userClass));
98+
99+
$input->setArgument(
100+
'email-field',
101+
$interactiveSecurityHelper->guessEmailField($io, $userClass)
102+
);
103+
$input->setArgument(
104+
'email-getter',
105+
$interactiveSecurityHelper->guessEmailGetter($io, $userClass)
106+
);
107+
$input->setArgument(
108+
'password-setter',
109+
$interactiveSecurityHelper->guessPasswordSetter($io, $userClass)
110+
);
111+
}
112+
113+
public function configureDependencies(DependencyBuilder $dependencies)
114+
{
115+
// This recipe depends upon Doctrine ORM, to save the token and update the user
116+
ORMDependencyBuilder::buildDependencies($dependencies);
117+
118+
$dependencies->addClassDependency(
119+
AbstractType::class,
120+
'form'
121+
);
122+
$dependencies->addClassDependency(
123+
Validation::class,
124+
'validator'
125+
);
126+
$dependencies->addClassDependency(
127+
TwigBundle::class,
128+
'twig-bundle'
129+
);
130+
$dependencies->addClassDependency(
131+
SecurityBundle::class,
132+
'security'
133+
);
134+
$dependencies->addClassDependency(
135+
SwiftmailerBundle::class,
136+
'mail'
137+
);
138+
}
139+
140+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
141+
{
142+
$userClass = $input->getArgument('user-class');
143+
$userClassNameDetails = $generator->createClassNameDetails(
144+
'\\'.$userClass,
145+
'Entity\\'
146+
);
147+
$tokenClassNameDetails = $generator->createClassNameDetails(
148+
'PasswordResetToken',
149+
'Entity\\'
150+
);
151+
152+
// 1) Create a new "PasswordResetToken" entity
153+
$generator->generateClass(
154+
$tokenClassNameDetails->getFullName(),
155+
'forgottenPassword/PasswordResetToken.tpl.php',
156+
[
157+
'user_class_name' => $userClassNameDetails->getShortName(),
158+
'user_full_class_name' => $userClassNameDetails->getFullName(),
159+
]
160+
);
161+
162+
// 2) Generate the "request" (email) form class
163+
$emailField = $input->getArgument('email-field');
164+
$requestFormClassDetails = $this->generateRequestFormClass(
165+
$generator,
166+
$emailField
167+
);
168+
169+
// 3) Generate the "new password" form class
170+
$resettingFormClassDetails = $this->generateResettingFormClass(
171+
$userClassNameDetails,
172+
$generator
173+
);
174+
175+
// 4) Generate the controller
176+
$controllerClassNameDetails = $generator->createClassNameDetails(
177+
'ForgottenPasswordController',
178+
'Controller\\'
179+
);
180+
181+
$generator->generateController(
182+
$controllerClassNameDetails->getFullName(),
183+
'forgottenPassword/ForgottenPasswordController.tpl.php',
184+
[
185+
'request_form_class_name' => $requestFormClassDetails->getShortName(),
186+
'request_form_full_class_name' => $requestFormClassDetails->getFullName(),
187+
'resetting_form_class_name' => $resettingFormClassDetails->getShortName(),
188+
'resetting_form_full_class_name' => $resettingFormClassDetails->getFullName(),
189+
'user_class_name' => $userClassNameDetails->getShortName(),
190+
'user_full_class_name' => $userClassNameDetails->getFullName(),
191+
'email_field' => $emailField,
192+
'email_getter' => $input->getArgument('email-getter'),
193+
'password_setter' => $input->getArgument('password-setter'),
194+
'login_route' => 'app_login',
195+
'token_class_name' => $tokenClassNameDetails->getShortName(),
196+
'token_full_class_name' => $tokenClassNameDetails->getFullName(),
197+
]
198+
);
199+
200+
// 5) Generate the "request" template
201+
$generator->generateFile(
202+
'templates/forgottenPassword/request.html.twig',
203+
'forgottenPassword/twig_request.tpl.php',
204+
[
205+
'email_field' => $emailField,
206+
]
207+
);
208+
209+
// 6) Generate the reset e-mail template
210+
$generator->generateFile(
211+
'templates/forgottenPassword/email.txt.twig',
212+
'forgottenPassword/twig_email.tpl.php',
213+
[]
214+
);
215+
216+
// 7) Generate the "checkEmail" template
217+
$generator->generateFile(
218+
'templates/forgottenPassword/check_email.html.twig',
219+
'forgottenPassword/twig_check_email.tpl.php',
220+
[]
221+
);
222+
223+
// 8) Generate the "reset" template
224+
$generator->generateFile(
225+
'templates/forgottenPassword/reset.html.twig',
226+
'forgottenPassword/twig_reset.tpl.php',
227+
[]
228+
);
229+
230+
$generator->writeChanges();
231+
$this->writeSuccessMessage($io);
232+
233+
$io->text('Done! A new entity was added: PasswordResetToken. You should now generate a migration (make:migration) and run it to update your database.');
234+
$io->text('Next: Please review ForgottenPasswordController. Then you can add a link to "app_forgotten_password" path anywhere you like, typically below your login form!');
235+
}
236+
237+
private function generateRequestFormClass(Generator $generator, string $emailField)
238+
{
239+
$formClassDetails = $generator->createClassNameDetails(
240+
'PasswordRequestFormType',
241+
'Form\\'
242+
);
243+
244+
$formFields = [
245+
$emailField => [
246+
'type' => TextType::class,
247+
'options_code' => <<<EOF
248+
'constraints' => [
249+
new NotBlank([
250+
'message' => 'Please enter your $emailField',
251+
]),
252+
],
253+
EOF
254+
],
255+
];
256+
257+
$this->formTypeRenderer->render(
258+
$formClassDetails,
259+
$formFields,
260+
null,
261+
[
262+
'Symfony\Component\Validator\Constraints\NotBlank',
263+
]
264+
);
265+
266+
return $formClassDetails;
267+
}
268+
269+
private function generateResettingFormClass(ClassNameDetails $userClassDetails, Generator $generator)
270+
{
271+
$formClassDetails = $generator->createClassNameDetails(
272+
'PasswordResettingFormType',
273+
'Form\\'
274+
);
275+
276+
$formFields = [
277+
'plainPassword' => [
278+
'type' => PasswordType::class,
279+
'options_code' => <<<EOF
280+
// instead of being set onto the object directly,
281+
// this is read and encoded in the controller
282+
'mapped' => false,
283+
'constraints' => [
284+
new NotBlank([
285+
'message' => 'Please enter a password',
286+
]),
287+
new Length([
288+
'min' => 6,
289+
'minMessage' => 'Your password should be at least {{ limit }} characters',
290+
// max length allowed by Symfony for security reasons
291+
'max' => 4096,
292+
]),
293+
],
294+
'label' => 'New password',
295+
EOF
296+
],
297+
];
298+
299+
$this->formTypeRenderer->render(
300+
$formClassDetails,
301+
$formFields,
302+
$userClassDetails,
303+
[
304+
'Symfony\Component\Validator\Constraints\NotBlank',
305+
'Symfony\Component\Validator\Constraints\Length',
306+
]
307+
);
308+
309+
return $formClassDetails;
310+
}
311+
}

src/Resources/config/makers.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
<tag name="maker.command" />
4343
</service>
4444

45+
<service id="maker.maker.make_forgotten_password" class="Symfony\Bundle\MakerBundle\Maker\MakeForgottenPassword">
46+
<argument type="service" id="maker.file_manager" />
47+
<argument type="service" id="maker.renderer.form_type_renderer" />
48+
<argument type="service" id="router" />
49+
<tag name="maker.command" />
50+
</service>
51+
4552
<service id="maker.maker.make_form" class="Symfony\Bundle\MakerBundle\Maker\MakeForm">
4653
<argument type="service" id="maker.doctrine_helper" />
4754
<argument type="service" id="maker.renderer.form_type_renderer" />
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 reset password process, including forms, controllers & templates.
2+
3+
<info>php %command.full_name%</info>
4+
5+
The command will ask for several pieces of information to build your process.

0 commit comments

Comments
 (0)