Skip to content

Commit 3c82ba6

Browse files
committed
Merge branch 'master' into feature/custom-namespaces
2 parents c6217bf + 674aa82 commit 3c82ba6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2971
-1446
lines changed

phpunit.xml.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
<testsuites>
1818
<testsuite name="Project Test Suite">
1919
<directory>tests/</directory>
20+
<exclude>tests/Maker</exclude>
2021
<exclude>tests/fixtures</exclude>
2122
<exclude>tests/tmp</exclude>
2223
</testsuite>
24+
<testsuite name="Maker Test Suite">
25+
<directory>tests/Maker</directory>
26+
</testsuite>
2327
</testsuites>
2428

2529
<filter>

src/EventRegistry.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ public function __construct(EventDispatcherInterface $eventDispatcher)
8181
foreach (self::$newEventsMap as $eventName => $newEventClass) {
8282
//Check if the new event classes exist, if so replace the old one with the new.
8383
if (isset(self::$eventsMap[$eventName]) && class_exists($newEventClass)) {
84-
unset(self::$eventsMap[$eventName]);
85-
self::$eventsMap[$newEventClass] = $newEventClass;
84+
self::$eventsMap[$eventName] = $newEventClass;
8685
}
8786
}
8887
}
@@ -160,4 +159,13 @@ public function getEventClassName(string $event)
160159

161160
return null;
162161
}
162+
163+
public function listActiveEvents(array $events)
164+
{
165+
foreach ($events as $key => $event) {
166+
$events[$key] = sprintf('%s (<fg=yellow>%s</>)', $event, self::$eventsMap[$event]);
167+
}
168+
169+
return $events;
170+
}
163171
}

src/Maker/MakeForgottenPassword.php

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
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\FileManager;
19+
use Symfony\Bundle\MakerBundle\Generator;
20+
use Symfony\Bundle\MakerBundle\InputConfiguration;
21+
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
22+
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
23+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
24+
use Symfony\Bundle\SecurityBundle\SecurityBundle;
25+
use Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle;
26+
use Symfony\Bundle\TwigBundle\TwigBundle;
27+
use Symfony\Component\Console\Command\Command;
28+
use Symfony\Component\Console\Input\InputInterface;
29+
use Symfony\Component\Form\AbstractType;
30+
use Symfony\Component\Form\Extension\Core\Type\EmailType;
31+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
32+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
33+
use Symfony\Component\Routing\RouterInterface;
34+
use Symfony\Component\Validator\Validation;
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+
$repositoryClassNameDetails = $generator->createClassNameDetails(
152+
'PasswordResetTokenRepository',
153+
'Repository\\'
154+
);
155+
156+
// 1) Create a new "PasswordResetToken" entity and its repository
157+
$generator->generateClass(
158+
$tokenClassNameDetails->getFullName(),
159+
'forgottenPassword/PasswordResetToken.tpl.php',
160+
[
161+
'repository_class_name' => $repositoryClassNameDetails->getFullName(),
162+
'user_class_name' => $userClassNameDetails->getShortName(),
163+
'user_full_class_name' => $userClassNameDetails->getFullName(),
164+
]
165+
);
166+
$generator->generateClass(
167+
$repositoryClassNameDetails->getFullName(),
168+
'forgottenPassword/PasswordResetTokenRepository.tpl.php',
169+
[
170+
'token_class_name' => $tokenClassNameDetails->getShortName(),
171+
'token_full_class_name' => $tokenClassNameDetails->getFullName(),
172+
'user_class_name' => $userClassNameDetails->getShortName(),
173+
'user_full_class_name' => $userClassNameDetails->getFullName(),
174+
]
175+
);
176+
177+
// 2) Generate the "request" (email) form class
178+
$emailField = $input->getArgument('email-field');
179+
$requestFormClassDetails = $this->generateRequestFormClass(
180+
$generator,
181+
$emailField
182+
);
183+
184+
// 3) Generate the "new password" form class
185+
$resettingFormClassDetails = $this->generateResettingFormClass($generator);
186+
187+
// 4) Generate the controller
188+
$controllerClassNameDetails = $generator->createClassNameDetails(
189+
'ForgottenPasswordController',
190+
'Controller\\'
191+
);
192+
193+
$generator->generateController(
194+
$controllerClassNameDetails->getFullName(),
195+
'forgottenPassword/ForgottenPasswordController.tpl.php',
196+
[
197+
'request_form_class_name' => $requestFormClassDetails->getShortName(),
198+
'request_form_full_class_name' => $requestFormClassDetails->getFullName(),
199+
'resetting_form_class_name' => $resettingFormClassDetails->getShortName(),
200+
'resetting_form_full_class_name' => $resettingFormClassDetails->getFullName(),
201+
'user_class_name' => $userClassNameDetails->getShortName(),
202+
'user_full_class_name' => $userClassNameDetails->getFullName(),
203+
'email_field' => $emailField,
204+
'email_getter' => $input->getArgument('email-getter'),
205+
'password_setter' => $input->getArgument('password-setter'),
206+
'login_route' => 'app_login',
207+
'token_class_name' => $tokenClassNameDetails->getShortName(),
208+
'token_full_class_name' => $tokenClassNameDetails->getFullName(),
209+
]
210+
);
211+
212+
// 5) Generate the "request" template
213+
$generator->generateFile(
214+
'templates/forgotten_password/request.html.twig',
215+
'forgottenPassword/twig_request.tpl.php',
216+
[
217+
'email_field' => $emailField,
218+
]
219+
);
220+
221+
// 6) Generate the reset e-mail template
222+
$generator->generateFile(
223+
'templates/forgotten_password/email.txt.twig',
224+
'forgottenPassword/twig_email.tpl.php',
225+
[]
226+
);
227+
228+
// 7) Generate the "checkEmail" template
229+
$generator->generateFile(
230+
'templates/forgotten_password/check_email.html.twig',
231+
'forgottenPassword/twig_check_email.tpl.php',
232+
[]
233+
);
234+
235+
// 8) Generate the "reset" template
236+
$generator->generateFile(
237+
'templates/forgotten_password/reset.html.twig',
238+
'forgottenPassword/twig_reset.tpl.php',
239+
[]
240+
);
241+
242+
$generator->writeChanges();
243+
$this->writeSuccessMessage($io);
244+
245+
$io->text('Done! A new entity was added: PasswordResetToken. You should now generate a migration (make:migration) and run it to update your database.');
246+
$io->text('Next: Please review ForgottenPasswordController. Then you can add a link to "app_forgotten_password_request" path anywhere you like, typically below your login form!');
247+
}
248+
249+
private function generateRequestFormClass(Generator $generator, string $emailField)
250+
{
251+
$formClassDetails = $generator->createClassNameDetails(
252+
'PasswordRequestFormType',
253+
'Form\\'
254+
);
255+
256+
$formFields = [
257+
$emailField => [
258+
'type' => EmailType::class,
259+
'options_code' => <<<EOF
260+
'constraints' => [
261+
new NotBlank([
262+
'message' => 'Please enter your $emailField',
263+
]),
264+
],
265+
EOF
266+
],
267+
];
268+
269+
$this->formTypeRenderer->render(
270+
$formClassDetails,
271+
$formFields,
272+
null,
273+
[
274+
'Symfony\Component\Validator\Constraints\NotBlank',
275+
]
276+
);
277+
278+
return $formClassDetails;
279+
}
280+
281+
private function generateResettingFormClass(Generator $generator)
282+
{
283+
$formClassDetails = $generator->createClassNameDetails(
284+
'PasswordResettingFormType',
285+
'Form\\'
286+
);
287+
288+
$formFields = [
289+
'plainPassword' => [
290+
'type' => RepeatedType::class,
291+
'options_code' => <<<EOF
292+
'type' => PasswordType::class,
293+
'first_options' => [
294+
'constraints' => [
295+
new NotBlank([
296+
'message' => 'Please enter a password',
297+
]),
298+
new Length([
299+
'min' => 6,
300+
'minMessage' => 'Your password should be at least {{ limit }} characters',
301+
// max length allowed by Symfony for security reasons
302+
'max' => 4096,
303+
]),
304+
],
305+
'label' => 'New password',
306+
],
307+
'second_options' => [
308+
'label' => 'Repeat Password',
309+
],
310+
'invalid_message' => 'The password fields must match.',
311+
// Instead of being set onto the object directly,
312+
// this is read and encoded in the controller
313+
'mapped' => false,
314+
EOF
315+
],
316+
];
317+
318+
$this->formTypeRenderer->render(
319+
$formClassDetails,
320+
$formFields,
321+
null,
322+
[
323+
'Symfony\Component\Validator\Constraints\Length',
324+
'Symfony\Component\Validator\Constraints\NotBlank',
325+
],
326+
[
327+
PasswordType::class,
328+
]
329+
);
330+
331+
return $formClassDetails;
332+
}
333+
}

src/Maker/MakeSubscriber.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
5959
$events = $this->eventRegistry->getAllActiveEvents();
6060

6161
$io->writeln(' <fg=green>Suggested Events:</>');
62-
$io->listing($events);
62+
$io->listing($this->eventRegistry->listActiveEvents($events));
6363
$question = new Question(sprintf(' <fg=green>%s</>', $command->getDefinition()->getArgument('event')->getDescription()));
6464
$question->setAutocompleterValues($events);
6565
$question->setValidator([Validator::class, 'notBlank']);

0 commit comments

Comments
 (0)