Skip to content

Commit 0bfd028

Browse files
committed
Handle when securityController provided already exist
1 parent 011b1d6 commit 0bfd028

File tree

7 files changed

+168
-81
lines changed

7 files changed

+168
-81
lines changed

src/Maker/MakeAuthenticator.php

Lines changed: 66 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@
2121
use Symfony\Bundle\MakerBundle\InputConfiguration;
2222
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
2323
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
24+
use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder;
2425
use Symfony\Bundle\MakerBundle\Str;
26+
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
2527
use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
2628
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
29+
use Symfony\Bundle\MakerBundle\Validator;
2730
use Symfony\Bundle\SecurityBundle\SecurityBundle;
2831
use Symfony\Component\Console\Command\Command;
2932
use Symfony\Component\Console\Input\InputArgument;
3033
use Symfony\Component\Console\Input\InputInterface;
3134
use Symfony\Component\Console\Input\InputOption;
32-
use Symfony\Component\Security\Core\User\UserInterface;
35+
use Symfony\Component\Console\Question\Question;
3336
use Symfony\Component\Yaml\Yaml;
3437

3538
/**
@@ -72,6 +75,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
7275

7376
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
7477
{
78+
// authenticator type
7579
$authenticatorTypeValues = [
7680
'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR,
7781
'Form login' => self::AUTH_TYPE_FORM_LOGIN,
@@ -87,97 +91,67 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
8791
$authenticatorTypeValues[$authenticatorType]
8892
);
8993

90-
$command->addArgument('authenticator-class', InputArgument::REQUIRED);
91-
$input->setArgument(
92-
'authenticator-class',
93-
$io->ask('The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)')
94-
);
95-
96-
// TODO : validate class name
97-
98-
if (null === $input->getArgument('authenticator-class')) {
99-
throw new RuntimeCommandException('The authenticator class could not be empty!');
100-
}
94+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml'));
95+
$securityData = $manipulator->getData();
10196

102-
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
103-
return;
97+
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')
98+
&& !isset($securityData['security']['providers']) || !$securityData['security']['providers']) {
99+
throw new RuntimeCommandException('You need to have at least one provider defined in security.yaml');
104100
}
105101

106-
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
107-
$securityData = $manipulator->getData();
108-
109-
$interactiveSecurityHelper = new InteractiveSecurityHelper();
102+
// authenticator class
103+
$command->addArgument('authenticator-class', InputArgument::REQUIRED);
104+
$questionAuthenticatorClass = new Question('The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)');
105+
$questionAuthenticatorClass->setValidator(
106+
function ($answer) {
107+
Validator::notBlank($answer);
108+
return Validator::validateClassDoesNotExist(
109+
$this->generator->createClassNameDetails(
110+
$answer,
111+
'Security\\'
112+
)->getFullName()
113+
);
114+
}
115+
);
116+
$input->setArgument('authenticator-class', $io->askQuestion($questionAuthenticatorClass));
110117

111118
$command->addOption('firewall-name', null, InputOption::VALUE_OPTIONAL);
112-
$input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData));
119+
$input->setOption('firewall-name', $firewallName = InteractiveSecurityHelper::guessFirewallName($io, $securityData));
113120

114121
$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL);
115-
116-
$authenticatorClassNameDetails = $this->generator->createClassNameDetails(
117-
$input->getArgument('authenticator-class'),
118-
'Security\\'
119-
);
120-
121122
$input->setOption(
122123
'entry-point',
123-
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $authenticatorClassNameDetails->getFullName(), $firewallName)
124+
InteractiveSecurityHelper::guessEntryPoint($io, $securityData, $input->getArgument('authenticator-class'), $firewallName)
124125
);
125126

126127
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
127-
$command->addArgument('controller-class', InputArgument::OPTIONAL, 'Choose a name for the controller class (e.g. <fg=yellow>SecurityController</>)', 'SecurityController');
128-
129-
$controllerClass = $io->ask(
130-
$command->getDefinition()->getArgument('controller-class')->getDescription(),
131-
$command->getDefinition()->getArgument('controller-class')->getDefault()
128+
$command->addArgument('controller-class', InputArgument::OPTIONAL);
129+
$input->setArgument(
130+
'controller-class',
131+
$io->ask(
132+
'Choose a name for the controller class (e.g. <fg=yellow>SecurityController</>)',
133+
'SecurityController',
134+
[Validator::class, 'validateClassName']
135+
)
132136
);
133-
// TODO : validate class name
134-
$input->setArgument('controller-class', $controllerClass);
135-
136-
if (!isset($securityData['security']['providers']) || !$securityData['security']['providers']) {
137-
throw new RuntimeCommandException('You need to have at least one provider defined in security.yaml');
138-
}
139137

140138
$command->addArgument('user-class', InputArgument::OPTIONAL);
141-
if (1 === \count($securityData['security']['providers']) && isset(current($securityData['security']['providers'])['entity'])) {
142-
$entityProvider = current($securityData['security']['providers']);
143-
$userClass = $entityProvider['entity']['class'];
144-
} else {
145-
$userClass = $io->ask(
146-
'Enter the User class you want to authenticate (e.g. <fg=yellow>App\\Entity\\User</>)
147-
(It has to be handled by one of the firewall\'s providers)',
148-
class_exists('App\\Entity\\User') && isset(class_implements('App\\Entity\\User')[UserInterface::class]) ? 'App\\Entity\\User'
149-
: class_exists('App\\Security\\User') && isset(class_implements('App\\Security\\User')[UserInterface::class]) ? 'App\\Security\\User' : null
150-
);
151-
152-
if (!class_exists($userClass)) {
153-
throw new RuntimeCommandException(sprintf('The class "%s" does not exist', $userClass));
154-
}
155-
156-
if (!isset(class_implements($userClass)[UserInterface::class])) {
157-
throw new RuntimeCommandException(sprintf('The class "%s" doesn\'t implement "%s"', $userClass, UserInterface::class));
158-
}
159-
}
160-
$input->setArgument('user-class', $userClass);
139+
$input->setArgument('user-class', InteractiveSecurityHelper::guessUserClass($io, $securityData));
161140
}
162141
}
163142

164143
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
165144
{
166-
$classNameDetails = $generator->createClassNameDetails(
167-
$input->getArgument('authenticator-class'),
168-
'Security\\'
169-
);
170-
171-
// TODO update LoginFormNotEntityAuthenticator
172-
145+
// generate authenticator class
173146
$generator->generateClass(
174-
$classNameDetails->getFullName(),
147+
$input->getArgument('authenticator-class'),
175148
self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type') ?
176149
($this->doctrineHelper->isClassAMappedEntity($input->getArgument('user-class')) ? 'authenticator/LoginFormEntityAuthenticator.tpl.php' : 'authenticator/LoginFormNotEntityAuthenticator.tpl.php')
177150
: 'authenticator/Empty.tpl.php',
178151
[]
179152
);
180153

154+
// update security.yaml with guard config
181155
$securityYamlUpdated = false;
182156
$path = 'config/packages/security.yaml';
183157
if ($this->fileManager->fileExists($path)) {
@@ -186,7 +160,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
186160
$this->fileManager->getFileContents($path),
187161
$input->getOption('firewall-name'),
188162
$input->getOption('entry-point'),
189-
$classNameDetails->getFullName()
163+
$input->getArgument('authenticator-class')
190164
);
191165
$generator->dumpFile($path, $newYaml);
192166
$securityYamlUpdated = true;
@@ -195,25 +169,41 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
195169
}
196170

197171
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
198-
// TODO check if SecurityController exists
199172
$controllerClassNameDetails = $generator->createClassNameDetails(
200173
$input->getArgument('controller-class'),
201174
'Controller\\',
202175
'Controller'
203176
);
204177

205-
$controllerPath = $generator->generateClass(
206-
$controllerClassNameDetails->getFullName(),
207-
'login_form/SecurityController.tpl.php',
208-
[
209-
'parent_class_name' => \method_exists(AbstractController::class, 'getParameter') ? 'AbstractController' : 'Controller',
210-
]
211-
);
178+
if (class_exists($controllerClassNameDetails->getFullName())) {
179+
// If provided security controller class exist, add login() method
180+
if (method_exists($controllerClassNameDetails->getFullName(), 'login')) {
181+
throw new RuntimeCommandException(sprintf('Method "login" already exists on class %s', $controllerClassNameDetails->getFullName()));
182+
}
183+
184+
$manipulator = new ClassSourceManipulator(
185+
$this->fileManager->getFileContents($controllerPath = $this->fileManager->getRelativePathForFutureClass($controllerClassNameDetails->getFullName())),
186+
true
187+
);
188+
$securityControllerBuilder = new SecurityControllerBuilder();
189+
$securityControllerBuilder->addLoginMethod($manipulator);
190+
$this->generator->dumpFile($controllerPath, $manipulator->getSourceCode());
191+
} else {
192+
// otherwise, create security controller
193+
$controllerPath = $generator->generateClass(
194+
$controllerClassNameDetails->getFullName(),
195+
'authenticator/SecurityController.tpl.php',
196+
[
197+
'parent_class_name' => \method_exists(AbstractController::class, 'getParameter') ? 'AbstractController' : 'Controller',
198+
]
199+
);
200+
}
212201

202+
// create login form template
213203
$templateName = Str::asFilePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()).'/login.html.twig';
214204
$generator->generateFile(
215205
'templates/'.$templateName,
216-
'login_form/login_form.tpl.php',
206+
'authenticator/login_form.tpl.php',
217207
[
218208
'controller_path' => $controllerPath,
219209
]
@@ -230,7 +220,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
230220
'security: {}',
231221
'main',
232222
null,
233-
$classNameDetails->getFullName()
223+
$input->getArgument('authenticator-class')
234224
);
235225
$text[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
236226
}

src/Resources/skeleton/authenticator/LoginFormNotEntityAuthenticator.tpl.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function getUser($credentials, UserProviderInterface $userProvider)
5959
throw new InvalidCsrfTokenException();
6060
}
6161

62-
return $userProvider->loadUserByUsername($credentials['email']);
62+
return $userProvider->loadUserByUsername($credentials['email']);A
6363
}
6464

6565
public function checkCredentials($credentials, UserInterface $user)

src/Security/InteractiveSecurityHelper.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
namespace Symfony\Bundle\MakerBundle\Security;
1313

1414
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
15+
use Symfony\Bundle\MakerBundle\Validator;
1516
use Symfony\Component\Console\Style\SymfonyStyle;
17+
use Symfony\Component\Security\Core\User\UserInterface;
1618

1719
/**
1820
* @internal
1921
*/
2022
final class InteractiveSecurityHelper
2123
{
22-
public function guessFirewallName(SymfonyStyle $io, array $securityData): string
24+
public static function guessFirewallName(SymfonyStyle $io, array $securityData): string
2325
{
2426
$realFirewalls = array_filter(
2527
$securityData['security']['firewalls'] ?? [],
@@ -39,7 +41,7 @@ function ($item) {
3941
return $io->choice('Which firewall do you want to update ?', array_keys($realFirewalls), key($realFirewalls));
4042
}
4143

42-
public function guessEntryPoint(SymfonyStyle $io, array $securityData, string $authenticatorClass, string $firewallName)
44+
public static function guessEntryPoint(SymfonyStyle $io, array $securityData, string $authenticatorClass, string $firewallName)
4345
{
4446
if (!isset($securityData['security'])) {
4547
$securityData['security'] = [];
@@ -75,4 +77,26 @@ public function guessEntryPoint(SymfonyStyle $io, array $securityData, string $a
7577
current($authenticators)
7678
);
7779
}
80+
81+
public static function guessUserClass(SymfonyStyle $io, array $securityData): string
82+
{
83+
if (1 === \count($securityData['security']['providers']) && isset(current($securityData['security']['providers'])['entity'])) {
84+
$entityProvider = current($securityData['security']['providers']);
85+
$userClass = $entityProvider['entity']['class'];
86+
} else {
87+
$userClass = $io->ask(
88+
'Enter the User class you want to authenticate (e.g. <fg=yellow>App\\Entity\\User</>)
89+
(It has to be handled by one of the firewall\'s providers)',
90+
class_exists('App\\Entity\\User') && isset(class_implements('App\\Entity\\User')[UserInterface::class]) ? 'App\\Entity\\User'
91+
: class_exists('App\\Security\\User') && isset(class_implements('App\\Security\\User')[UserInterface::class]) ? 'App\\Security\\User' : null,
92+
[Validator::class, 'classExists']
93+
);
94+
95+
if (!isset(class_implements($userClass)[UserInterface::class])) {
96+
throw new RuntimeCommandException(sprintf('The class "%s" doesn\'t implement "%s"', $userClass, UserInterface::class));
97+
}
98+
}
99+
100+
return $userClass;
101+
}
78102
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Symfony MakerBundle package.
7+
*
8+
* (c) Fabien Potencier <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Symfony\Bundle\MakerBundle\Security;
15+
16+
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
17+
18+
/**
19+
* @internal
20+
*/
21+
final class SecurityControllerBuilder
22+
{
23+
public function addLoginMethod(ClassSourceManipulator $manipulator)
24+
{
25+
$loginMethodBuilder = $manipulator->createMethodBuilder('login', 'Response', false, ['@Route("/login", name="app_login")']);
26+
$loginMethodBuilder->addParam(
27+
(new \PhpParser\Builder\Param('authenticationUtils'))->setTypeHint('AuthenticationUtils')
28+
);
29+
30+
$manipulator->addMethodBody($loginMethodBuilder, <<<'CODE'
31+
<?php
32+
// get the login error if there is one
33+
$error = $authenticationUtils->getLastAuthenticationError();
34+
// last username entered by the user
35+
$lastUsername = $authenticationUtils->getLastUsername();
36+
CODE
37+
);
38+
$loginMethodBuilder->addStmt($manipulator->createMethodLevelBlankLine());
39+
$manipulator->addMethodBody($loginMethodBuilder, <<<'CODE'
40+
<?php
41+
return $this->render(
42+
'security/login.html.twig',
43+
[
44+
'last_username' => $lastUsername,
45+
'error' => $error,
46+
]
47+
);
48+
CODE
49+
);
50+
$manipulator->addMethodBuilder($loginMethodBuilder);
51+
$manipulator->addUseStatementIfNecessary('Symfony\\Component\\HttpFoundation\\Response');
52+
$manipulator->addUseStatementIfNecessary('Symfony\\Component\\Routing\\Annotation\\Route');
53+
$manipulator->addUseStatementIfNecessary('Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationUtils');
54+
}
55+
}

src/Util/ClassSourceManipulator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ public function addMethodBuilder(Builder\Method $methodBuilder)
217217
$this->addMethod($methodBuilder->getNode());
218218
}
219219

220+
public function addMethodBody(Builder\Method $methodBuilder, string $methodBody)
221+
{
222+
$nodes = $this->parser->parse($methodBody);
223+
$methodBuilder->addStmts($nodes);
224+
}
225+
220226
public function createMethodBuilder(string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = []): Builder\Method
221227
{
222228
$methodNodeBuilder = (new Builder\Method($methodName))
@@ -662,7 +668,7 @@ private function getConstructorNode()
662668
*
663669
* @return string The alias to use when referencing this class
664670
*/
665-
private function addUseStatementIfNecessary(string $class): string
671+
public function addUseStatementIfNecessary(string $class): string
666672
{
667673
$shortClassName = Str::getShortClassName($class);
668674
if ($this->isInSameNamespace($class)) {
@@ -771,6 +777,7 @@ private function setSourceCode(string $sourceCode)
771777
{
772778
$this->sourceCode = $sourceCode;
773779
$this->oldStmts = $this->parser->parse($sourceCode);
780+
dump($this->oldStmts);
774781
$this->oldTokens = $this->lexer->getTokens();
775782

776783
$traverser = new NodeTraverser();

src/Validator.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,15 @@ public static function entityExists(string $className = null, array $entities =
184184

185185
return $className;
186186
}
187+
188+
public static function validateClassDoesNotExist($className)
189+
{
190+
self::notBlank($className);
191+
192+
if (class_exists($className)) {
193+
throw new RuntimeCommandException(sprintf('Class "%s" already exists', $className));
194+
}
195+
196+
return $className;
197+
}
187198
}

0 commit comments

Comments
 (0)