File: /var/www/payments-gateway/vendor/symfony/maker-bundle/src/Maker/MakeRegistrationForm.php
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Maker;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Column;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\CanGenerateTestsTrait;
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Security\Model\Authenticator;
use Symfony\Bundle\MakerBundle\Security\Model\AuthenticatorType;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\CliOutputHelper;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelper;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Jesse Rushlow <jr@rushlow.dev>
*
* @internal
*/
final class MakeRegistrationForm extends AbstractMaker
{
use CanGenerateTestsTrait;
private string $userClass;
private string $usernameField;
private string $passwordField;
private bool $willVerifyEmail = false;
private bool $verifyEmailAnonymously = false;
private string $idGetter;
private string $emailGetter;
private string $fromEmailAddress;
private string $fromEmailName;
private ?Authenticator $autoLoginAuthenticator = null;
private string $redirectRouteName;
private bool $addUniqueEntityConstraint = false;
public function __construct(
private FileManager $fileManager,
private FormTypeRenderer $formTypeRenderer,
private DoctrineHelper $doctrineHelper,
private ?RouterInterface $router = null,
) {
}
public static function getCommandName(): string
{
return 'make:registration-form';
}
public static function getCommandDescription(): string
{
return 'Create a new registration form system';
}
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
$command
->setHelp($this->getHelpFileContents('MakeRegistrationForm.txt'))
;
$this->configureCommandWithTestsOption($command);
}
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$interactiveSecurityHelper = new InteractiveSecurityHelper();
if (null === $this->router) {
throw new RuntimeCommandException('Router have been explicitly disabled in your configuration. This command needs to use the router.');
}
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. PHP & XML configuration formats are currently not supported.');
}
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$securityData = $manipulator->getData();
$providersData = $securityData['security']['providers'] ?? [];
$this->userClass = $interactiveSecurityHelper->guessUserClass(
$io,
$providersData,
'Enter the User class that you want to create during registration (e.g. <fg=yellow>App\\Entity\\User</>)'
);
$io->text(\sprintf('Creating a registration form for <info>%s</info>', $this->userClass));
$this->usernameField = $interactiveSecurityHelper->guessUserNameField($io, $this->userClass, $providersData);
$this->passwordField = $interactiveSecurityHelper->guessPasswordField($io, $this->userClass);
// see if it makes sense to add the UniqueEntity constraint
$userClassDetails = new ClassDetails($this->userClass);
if (!$userClassDetails->hasAttribute(UniqueEntity::class)) {
$this->addUniqueEntityConstraint = (bool) $io->confirm(\sprintf('Do you want to add a <comment>#[UniqueEntity]</comment> validation attribute to your <comment>%s</comment> class to make sure duplicate accounts aren\'t created?', Str::getShortClassName($this->userClass)));
}
$this->willVerifyEmail = (bool) $io->confirm('Do you want to send an email to verify the user\'s email address after registration?');
if ($this->willVerifyEmail) {
$this->checkComponentsExist($io);
$emailText[] = 'By default, users are required to be authenticated when they click the verification link that is emailed to them.';
$emailText[] = 'This prevents the user from registering on their laptop, then clicking the link on their phone, without';
$emailText[] = 'having to log in. To allow multi device email verification, we can embed a user id in the verification link.';
$io->text($emailText);
$io->newLine();
$this->verifyEmailAnonymously = (bool) $io->confirm('Would you like to include the user id in the verification link to allow anonymous email verification?', false);
$this->idGetter = $interactiveSecurityHelper->guessIdGetter($io, $this->userClass);
$this->emailGetter = $interactiveSecurityHelper->guessEmailGetter($io, $this->userClass, 'email');
$this->fromEmailAddress = $io->ask(
'What email address will be used to send registration confirmations? (e.g. <fg=yellow>mailer@your-domain.com</>)',
null,
Validator::validateEmailAddress(...)
);
$this->fromEmailName = $io->ask(
'What "name" should be associated with that email address? (e.g. <fg=yellow>Acme Mail Bot</>)',
null,
Validator::notBlank(...)
);
}
if ($io->confirm('Do you want to automatically authenticate the user after registration?')) {
$this->interactAuthenticatorQuestions(
$io,
$interactiveSecurityHelper,
$securityData
);
}
if (!$this->autoLoginAuthenticator) {
$routeNames = array_keys($this->router->getRouteCollection()->all());
$this->redirectRouteName = $io->choice('What route should the user be redirected to after registration?', $routeNames);
}
$this->interactSetGenerateTests($input, $io);
}
/** @param array<string, mixed> $securityData */
private function interactAuthenticatorQuestions(ConsoleStyle $io, InteractiveSecurityHelper $interactiveSecurityHelper, array $securityData): void
{
// get list of authenticators
$authenticators = $interactiveSecurityHelper->getAuthenticatorsFromConfig($securityData['security']['firewalls'] ?? []);
if (empty($authenticators)) {
$io->note('No authenticators found - so your user won\'t be automatically authenticated after registering.');
return;
}
$this->autoLoginAuthenticator =
1 === \count($authenticators) ? $authenticators[0] : $io->choice(
'Which authenticator should be used to login the user?',
$authenticators
);
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$userClassNameDetails = $generator->createClassNameDetails(
'\\'.$this->userClass,
'Entity\\'
);
$userDoctrineDetails = $this->doctrineHelper->createDoctrineDetails($userClassNameDetails->getFullName());
$userRepoVars = [
'repository_full_class_name' => EntityManagerInterface::class,
'repository_class_name' => 'EntityManagerInterface',
'repository_var' => '$manager',
];
$userRepository = $userDoctrineDetails->getRepositoryClass();
if (null !== $userRepository) {
$userRepoClassDetails = $generator->createClassNameDetails('\\'.$userRepository, 'Repository\\', 'Repository');
$userRepoVars = [
'repository_full_class_name' => $userRepoClassDetails->getFullName(),
'repository_class_name' => $userRepoClassDetails->getShortName(),
'repository_var' => \sprintf('$%s', lcfirst($userRepoClassDetails->getShortName())),
];
}
$verifyEmailServiceClassNameDetails = $generator->createClassNameDetails(
'EmailVerifier',
'Security\\'
);
$verifyEmailVars = ['will_verify_email' => $this->willVerifyEmail];
if ($this->willVerifyEmail) {
$verifyEmailVars = [
'will_verify_email' => $this->willVerifyEmail,
'email_verifier_class_details' => $verifyEmailServiceClassNameDetails,
'verify_email_anonymously' => $this->verifyEmailAnonymously,
'from_email' => $this->fromEmailAddress,
'from_email_name' => addslashes($this->fromEmailName),
'email_getter' => $this->emailGetter,
];
$useStatements = new UseStatementGenerator([
EntityManagerInterface::class,
TemplatedEmail::class,
Request::class,
MailerInterface::class,
UserInterface::class,
VerifyEmailExceptionInterface::class,
VerifyEmailHelperInterface::class,
$userClassNameDetails->getFullName(),
]);
$generator->generateClass(
$verifyEmailServiceClassNameDetails->getFullName(),
'verifyEmail/EmailVerifier.tpl.php',
array_merge([
'use_statements' => $useStatements,
'id_getter' => $this->idGetter,
'email_getter' => $this->emailGetter,
'verify_email_anonymously' => $this->verifyEmailAnonymously,
'user_class_name' => $userClassNameDetails->getShortName(),
],
$userRepoVars
)
);
$generator->generateTemplate(
'registration/confirmation_email.html.twig',
'registration/twig_email.tpl.php'
);
}
// 1) Generate the form class
$usernameField = $this->usernameField;
$formClassDetails = $this->generateFormClass(
$userClassNameDetails,
$generator,
$usernameField
);
// 2) Generate the controller
$controllerClassNameDetails = $generator->createClassNameDetails(
'RegistrationController',
'Controller\\'
);
$useStatements = new UseStatementGenerator([
AbstractController::class,
$formClassDetails->getFullName(),
$userClassNameDetails->getFullName(),
Request::class,
Response::class,
Route::class,
UserPasswordHasherInterface::class,
EntityManagerInterface::class,
]);
if ($this->willVerifyEmail) {
$useStatements->addUseStatement([
$verifyEmailServiceClassNameDetails->getFullName(),
TemplatedEmail::class,
Address::class,
VerifyEmailExceptionInterface::class,
]);
if ($this->verifyEmailAnonymously) {
$useStatements->addUseStatement($userRepoVars['repository_full_class_name']);
}
}
$autoLoginVars = [
'login_after_registration' => null !== $this->autoLoginAuthenticator,
];
if (null !== $this->autoLoginAuthenticator) {
$useStatements->addUseStatement([
Security::class,
]);
$autoLoginVars['firewall'] = $this->autoLoginAuthenticator->firewallName;
$autoLoginVars['authenticator'] = \sprintf('\'%s\'', $this->autoLoginAuthenticator->type->value);
if (AuthenticatorType::CUSTOM === $this->autoLoginAuthenticator->type) {
$useStatements->addUseStatement($this->autoLoginAuthenticator->authenticatorClass);
$autoLoginVars['authenticator'] = \sprintf('%s::class', Str::getShortClassName($this->autoLoginAuthenticator->authenticatorClass));
}
}
if ($isTranslatorAvailable = class_exists(Translator::class)) {
$useStatements->addUseStatement(TranslatorInterface::class);
}
$generator->generateController(
$controllerClassNameDetails->getFullName(),
'registration/RegistrationController.tpl.php',
array_merge([
'use_statements' => $useStatements,
'route_path' => '/register',
'route_name' => 'app_register',
'form_class_name' => $formClassDetails->getShortName(),
'user_class_name' => $userClassNameDetails->getShortName(),
'password_field' => $this->passwordField,
'redirect_route_name' => $this->redirectRouteName ?? null,
'translator_available' => $isTranslatorAvailable,
],
$userRepoVars,
$autoLoginVars,
$verifyEmailVars,
)
);
// 3) Generate the template
$generator->generateTemplate(
'registration/register.html.twig',
'registration/twig_template.tpl.php',
[
'username_field' => $usernameField,
'will_verify_email' => $this->willVerifyEmail,
]
);
// 4) Update the User class if necessary
if ($this->addUniqueEntityConstraint) {
$classDetails = new ClassDetails($this->userClass);
$userManipulator = new ClassSourceManipulator(
sourceCode: file_get_contents($classDetails->getPath())
);
$userManipulator->setIo($io);
if ($this->doctrineHelper->isDoctrineSupportingAttributes()) {
$userManipulator->addAttributeToClass(
UniqueEntity::class,
['fields' => [$usernameField], 'message' => \sprintf('There is already an account with this %s', $usernameField)]
);
}
$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());
}
if ($this->willVerifyEmail) {
$classDetails = new ClassDetails($this->userClass);
$userManipulator = new ClassSourceManipulator(
sourceCode: file_get_contents($classDetails->getPath()),
overwrite: false,
);
$userManipulator->setIo($io);
$userManipulator->addProperty(
name: 'isVerified',
defaultValue: false,
attributes: [$userManipulator->buildAttributeNode(attributeClass: Column::class, options: [], attributePrefix: 'ORM')],
propertyType: 'bool'
);
$userManipulator->addAccessorMethod('isVerified', 'isVerified', 'bool', false);
$userManipulator->addSetter('isVerified', 'bool', false);
$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());
}
// Generate PHPUnit Tests
if ($this->shouldGenerateTests()) {
$testClassDetails = $generator->createClassNameDetails(
'RegistrationControllerTest',
'Test\\'
);
$useStatements = new UseStatementGenerator([
EntityManager::class,
KernelBrowser::class,
TemplatedEmail::class,
WebTestCase::class,
$userRepoVars['repository_full_class_name'],
]);
$generator->generateFile(
targetPath: \sprintf('tests/%s.php', $testClassDetails->getShortName()),
templateName: $this->willVerifyEmail ? 'registration/Test.WithVerify.tpl.php' : 'registration/Test.WithoutVerify.tpl.php',
variables: array_merge([
'use_statements' => $useStatements,
'from_email' => $this->fromEmailAddress ?? null,
], $userRepoVars)
);
if (!class_exists(WebTestCase::class)) {
$io->caution('You\'ll need to install the `symfony/test-pack` to execute the tests for your new controller.');
}
}
$generator->writeChanges();
$this->writeSuccessMessage($io);
$this->successMessage($io, $this->willVerifyEmail, $userClassNameDetails->getShortName());
}
private function successMessage(ConsoleStyle $io, bool $emailVerification, string $userClass): void
{
$closing[] = 'Next:';
if (!$emailVerification) {
$closing[] = 'Make any changes you need to the form, controller & template.';
} else {
$index = 1;
if ($missingPackagesMessage = $this->getMissingComponentsComposerMessage()) {
$closing[] = '1) Install some missing packages:';
$closing[] = \sprintf(' <fg=green>%s</>', $missingPackagesMessage);
++$index;
}
$closing[] = \sprintf('%d) In <fg=yellow>RegistrationController::verifyUserEmail()</>:', $index++);
$closing[] = ' * Customize the last <fg=yellow>redirectToRoute()</> after a successful email verification.';
$closing[] = ' * Make sure you\'re rendering <fg=yellow>success</> flash messages or change the <fg=yellow>$this->addFlash()</> line.';
$closing[] = \sprintf('%d) Review and customize the form, controller, and templates as needed.', $index++);
$closing[] = \sprintf('%d) Run <fg=yellow>"%s make:migration"</> to generate a migration for the newly added <fg=yellow>%s::isVerified</> property.', $index++, CliOutputHelper::getCommandPrefix(), $userClass);
}
$io->text($closing);
$io->newLine();
$io->text('Then open your browser, go to "/register" and enjoy your new form!');
$io->newLine();
}
private function checkComponentsExist(ConsoleStyle $io): void
{
$message = $this->getMissingComponentsComposerMessage();
if ($message) {
$io->warning([
'We\'re missing some important components. Don\'t forget to install these after you\'re finished.',
$message,
]);
}
}
private function getMissingComponentsComposerMessage(): ?string
{
$missing = false;
$composerMessage = 'composer require';
// verify-email-bundle 1.17.0 includes the new validateEmailConfirmationFromRequest method.
// we need to check that if the bundle is installed, it is version 1.17.0 or greater
if (class_exists(SymfonyCastsVerifyEmailBundle::class)) {
$reflectedComponents = new \ReflectionClass(VerifyEmailHelper::class);
if (!$reflectedComponents->hasMethod('validateEmailConfirmationFromRequest')) {
throw new RuntimeCommandException('Please upgrade symfonycasts/verify-email-bundle to version 1.17.0 or greater.');
}
} else {
$missing = true;
$composerMessage = \sprintf('%s symfonycasts/verify-email-bundle', $composerMessage);
}
if (!interface_exists(MailerInterface::class)) {
$missing = true;
$composerMessage = \sprintf('%s symfony/mailer', $composerMessage);
}
if (!$missing) {
return null;
}
return $composerMessage;
}
public function configureDependencies(DependencyBuilder $dependencies): void
{
$dependencies->addClassDependency(
AbstractType::class,
'form'
);
$dependencies->addClassDependency(
Validation::class,
'validator'
);
$dependencies->addClassDependency(
TwigBundle::class,
'twig-bundle'
);
$dependencies->addClassDependency(
DoctrineBundle::class,
'orm'
);
$dependencies->addClassDependency(
SecurityBundle::class,
'security'
);
}
private function generateFormClass(ClassNameDetails $userClassDetails, Generator $generator, string $usernameField): ClassNameDetails
{
$formClassDetails = $generator->createClassNameDetails(
'RegistrationForm',
'Form\\'
);
$formFields = [
$usernameField => null,
'agreeTerms' => [
'type' => CheckboxType::class,
'options_code' => <<<EOF
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
EOF
],
'plainPassword' => [
'type' => PasswordType::class,
'options_code' => <<<EOF
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
EOF
],
];
$this->formTypeRenderer->render(
$formClassDetails,
$formFields,
$userClassDetails,
[
'Symfony\Component\Validator\Constraints\IsTrue',
'Symfony\Component\Validator\Constraints\Length',
'Symfony\Component\Validator\Constraints\NotBlank',
]
);
return $formClassDetails;
}
}