Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[make:reset-password] Translate exception reasons provided by ResetPasswordBundle #1059

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions src/Maker/MakeRegistrationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Common\Annotations\Annotation;
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\MakerBundle\ConsoleStyle;
Expand Down Expand Up @@ -370,24 +371,40 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
);
$userManipulator->setIo($io);

$userManipulator->addAnnotationToClass(
UniqueEntity::class,
[
'fields' => [$usernameField],
'message' => sprintf('There is already an account with this %s', $usernameField),
]
);
if ($this->doctrineHelper->isDoctrineSupportingAttributes()) {
$userManipulator->addAttributeToClass(
UniqueEntity::class,
['fields' => [$usernameField], 'message' => sprintf('There is already an account with this %s', $usernameField)]
);
} else {
$userManipulator->addAnnotationToClass(
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(
file_get_contents($classDetails->getPath())
file_get_contents($classDetails->getPath()),
false,
$this->doctrineHelper->isClassAnnotated($this->userClass),
true,
$this->doctrineHelper->doesClassUsesAttributes($this->userClass)
);
$userManipulator->setIo($io);

$userManipulator->addProperty('isVerified', ['@ORM\Column(type="boolean")'], false);
$userManipulator->addProperty(
'isVerified',
['@ORM\Column(type="boolean")'],
false,
[$userManipulator->buildAttributeNode(Column::class, ['type' => 'boolean'], 'ORM')]
);
$userManipulator->addAccessorMethod('isVerified', 'isVerified', 'bool', false);
$userManipulator->addSetter('isVerified', 'bool', false);

Expand Down
17 changes: 17 additions & 0 deletions src/Maker/MakeResetPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
Expand Down Expand Up @@ -236,6 +238,18 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
EntityManagerInterface::class,
];

// Namespace for ResetPasswordExceptionInterface was imported above
$problemValidateMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE')
? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE'
: "'There was a problem validating your password reset request'";
$problemHandleMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE')
? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE'
: "'There was a problem handling your password reset request'";

if ($isTranslatorAvailable = class_exists(Translator::class)) {
$useStatements[] = TranslatorInterface::class;
}

$generator->generateController(
$controllerClassNameDetails->getFullName(),
'resetPassword/ResetPasswordController.tpl.php',
Expand All @@ -253,6 +267,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'password_hasher_class_details' => ($passwordClassDetails = $generator->createClassNameDetails($passwordHasher, '\\')),
'password_hasher_variable_name' => str_replace('Interface', '', sprintf('$%s', lcfirst($passwordClassDetails->getShortName()))), // @legacy see passwordHasher conditional above
'use_password_hasher' => UserPasswordHasherInterface::class === $passwordHasher, // @legacy see passwordHasher conditional above
'problem_validate_message_or_constant' => $problemValidateMessageOrConstant,
'problem_handle_message_or_constant' => $problemHandleMessageOrConstant,
'translator_available' => $isTranslatorAvailable,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
<?= $use_legacy_passport_interface ? 'use Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\PassportInterface;'."\n" : '' ?>
use <?= $use_legacy_passport_interface ? 'Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\PassportInterface' : 'Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport' ?>;

class <?php echo $class_name ?> extends AbstractAuthenticator
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, E
* @Route("", name="app_forgot_password_request")
*/
<?php } ?>
public function request(Request $request, MailerInterface $mailer): Response
public function request(Request $request, MailerInterface $mailer<?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>): Response
{
$form = $this->createForm(<?= $request_form_type_class_name ?>::class);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
return $this->processSendingPasswordResetEmail(
$form->get('<?= $email_field ?>')->getData(),
$mailer
$mailer<?php if ($translator_available): ?>,
$translator<?php endif ?><?= "\n" ?>
);
}

Expand Down Expand Up @@ -84,7 +85,7 @@ public function checkEmail(): Response
* @Route("/reset/{token}", name="app_reset_password")
*/
<?php } ?>
public function reset(Request $request, <?= $password_hasher_class_details->getShortName() ?> <?= $password_hasher_variable_name ?>, string $token = null): Response
public function reset(Request $request, <?= $password_hasher_class_details->getShortName() ?> <?= $password_hasher_variable_name ?><?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>, string $token = null): Response
{
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
Expand All @@ -103,8 +104,9 @@ public function reset(Request $request, <?= $password_hasher_class_details->getS
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
$this->addFlash('reset_password_error', sprintf(
'There was a problem validating your reset request - %s',
$e->getReason()
'%s - %s',
<?php if ($translator_available): ?>$translator->trans(<?= $problem_validate_message_or_constant ?>, [], 'ResetPasswordBundle')<?php else: ?><?= $problem_validate_message_or_constant ?><?php endif ?>,
<?php if ($translator_available): ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php else: ?>$e->getReason()<?php endif ?><?= "\n" ?>
));

return $this->redirectToRoute('app_forgot_password_request');
Expand Down Expand Up @@ -138,7 +140,7 @@ public function reset(Request $request, <?= $password_hasher_class_details->getS
]);
}

private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer<?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>): RedirectResponse
{
$user = $this->entityManager->getRepository(<?= $user_class_name ?>::class)->findOneBy([
'<?= $email_field ?>' => $emailFormData,
Expand All @@ -157,8 +159,9 @@ private function processSendingPasswordResetEmail(string $emailFormData, MailerI
// Caution: This may reveal if a user is registered or not.
//
// $this->addFlash('reset_password_error', sprintf(
// 'There was a problem handling your password reset request - %s',
// $e->getReason()
// '%s - %s',
// <?php if ($translator_available): ?>$translator->trans(<?= $problem_handle_message_or_constant ?>, [], 'ResetPasswordBundle')<?php else: ?><?= $problem_handle_message_or_constant ?><?php endif ?>,
// <?php if ($translator_available): ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php else: ?>$e->getReason()<?php endif ?><?= "\n" ?>
// ));

return $this->redirectToRoute('app_check_email');
Expand Down
38 changes: 29 additions & 9 deletions src/Test/MakerTestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,38 @@ public function removeFromFile(string $filename, string $find, bool $allowNotFou

public function configureDatabase(bool $createSchema = true): void
{
$this->replaceInFile(
'.env',
'postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8',
getenv('TEST_DATABASE_DSN')
);
// @legacy Drop conditional when Symfony 4.4 is no longer supported.
if (50000 > $this->environment->getSymfonyVersionInApp()) {
$this->replaceInFile(
'.env',
'postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8',
getenv('TEST_DATABASE_DSN')
);
} else {
$this->replaceInFile(
'.env',
'postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8',
getenv('TEST_DATABASE_DSN')
);
}

// Flex includes a recipe to suffix the dbname w/ "_test" - lets keep
// things simple for these tests and not do that.
$this->removeFromFile(
'config/packages/test/doctrine.yaml',
"dbname_suffix: '_test%env(default::TEST_TOKEN)%'"
);
$this->modifyYamlFile('config/packages/doctrine.yaml', function (array $config) {
if (isset($config['when@test']['doctrine']['dbal']['dbname_suffix'])) {
unset($config['when@test']['doctrine']['dbal']['dbname_suffix']);
}

return $config;
});

// @legacy DoctrineBundle 2.4 recipe uses when@test instead of a test/doctrine.yaml config
if ($this->filesystem->exists('config/packages/test/doctrine.yaml')) {
$this->removeFromFile(
'config/packages/test/doctrine.yaml',
"dbname_suffix: '_test%env(default::TEST_TOKEN)%'"
);
}

// this looks silly, but it's the only way to drop the database *for sure*,
// as doctrine:database:drop will error if there is no database
Expand Down
99 changes: 61 additions & 38 deletions src/Util/ClassSourceManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embedded;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use PhpParser\Builder;
use PhpParser\BuilderHelpers;
use PhpParser\Comment\Doc;
Expand Down Expand Up @@ -93,7 +100,7 @@ public function addEntityField(string $propertyName, array $columnOptions, array
$attributes = [];

if ($this->useAttributesForDoctrineMapping) {
$attributes[] = $this->buildAttributeNode('ORM\Column', $columnOptions);
$attributes[] = $this->buildAttributeNode(Column::class, $columnOptions, 'ORM');
} else {
$comments[] = $this->buildAnnotationLine('@ORM\Column', $columnOptions);
}
Expand Down Expand Up @@ -138,10 +145,9 @@ public function addEmbeddedEntity(string $propertyName, string $className): void
} else {
$attributes = [
$this->buildAttributeNode(
'ORM\\Embedded',
[
'class' => new ClassNameValue($className, $typeHint),
]
Embedded::class,
['class' => new ClassNameValue($className, $typeHint)],
'ORM'
),
];
}
Expand Down Expand Up @@ -333,6 +339,9 @@ public function createMethodLevelBlankLine()
return $this->createBlankLineNode(self::CONTEXT_CLASS_METHOD);
}

/**
* @param array<Node\Attribute|Node\AttributeGroup> $attributes
*/
public function addProperty(string $name, array $annotationLines = [], $defaultValue = null, array $attributes = []): void
{
if ($this->propertyExists($name)) {
Expand All @@ -342,14 +351,14 @@ public function addProperty(string $name, array $annotationLines = [], $defaultV

$newPropertyBuilder = (new Builder\Property($name))->makePrivate();

if ($annotationLines && $this->useAnnotations) {
if ($this->useAttributesForDoctrineMapping) {
foreach ($attributes as $attribute) {
$newPropertyBuilder->addAttribute($attribute);
}
} elseif ($annotationLines && $this->useAnnotations) {
$newPropertyBuilder->setDocComment($this->createDocBlock($annotationLines));
}

foreach ($attributes as $attribute) {
$newPropertyBuilder->addAttribute($attribute);
}

if (null !== $defaultValue) {
$newPropertyBuilder->setDefault($defaultValue);
}
Expand All @@ -358,6 +367,17 @@ public function addProperty(string $name, array $annotationLines = [], $defaultV
$this->addNodeAfterProperties($newPropertyNode);
}

public function addAttributeToClass(string $attributeClass, array $options): void
{
$this->addUseStatementIfNecessary($attributeClass);

$classNode = $this->getClassNode();

$classNode->attrGroups[] = new Node\AttributeGroup([$this->buildAttributeNode($attributeClass, $options)]);

$this->updateSourceCodeFromNewStmts();
}

public function addAnnotationToClass(string $annotationClass, array $options): void
{
$annotationClassAlias = $this->addUseStatementIfNecessary($annotationClass);
Expand Down Expand Up @@ -532,8 +552,9 @@ private function addSingularRelation(BaseRelation $relation): void
} else {
$attributes = [
$this->buildAttributeNode(
$relation instanceof RelationManyToOne ? 'ORM\\ManyToOne' : 'ORM\\OneToOne',
$annotationOptions
$relation instanceof RelationManyToOne ? ManyToOne::class : OneToOne::class,
$annotationOptions,
'ORM'
),
];
}
Expand All @@ -544,9 +565,7 @@ private function addSingularRelation(BaseRelation $relation): void
'nullable' => false,
]);
} else {
$attributes[] = $this->buildAttributeNode('ORM\\JoinColumn', [
'nullable' => false,
]);
$attributes[] = $this->buildAttributeNode(JoinColumn::class, ['nullable' => false], 'ORM');
}
}

Expand Down Expand Up @@ -628,8 +647,9 @@ private function addCollectionRelation(BaseCollectionRelation $relation): void
} else {
$attributes = [
$this->buildAttributeNode(
$relation instanceof RelationManyToMany ? 'ORM\\ManyToMany' : 'ORM\\OneToMany',
$annotationOptions
$relation instanceof RelationManyToMany ? ManyToMany::class : OneToMany::class,
$annotationOptions,
'ORM'
),
];
}
Expand Down Expand Up @@ -900,6 +920,30 @@ public function addUseStatementIfNecessary(string $class): string
return $shortClassName;
}

/**
* Builds a PHPParser attribute node.
*
* @param string $attributeClass The attribute class which should be used for the attribute E.g. #[Column()]
* @param array $options The named arguments for the attribute ($key = argument name, $value = argument value)
* @param ?string $attributePrefix If a prefix is provided, the node is built using the prefix. E.g. #[ORM\Column()]
*/
public function buildAttributeNode(string $attributeClass, array $options, ?string $attributePrefix = null): Node\Attribute
{
$options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass);

$context = $this;
$nodeArguments = array_map(static function ($option, $value) use ($context) {
return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option));
}, array_keys($options), array_values($options));

$class = $attributePrefix ? sprintf('%s\\%s', $attributePrefix, Str::getShortClassName($attributeClass)) : Str::getShortClassName($attributeClass);

return new Node\Attribute(
new Node\Name($class),
$nodeArguments
);
}

private function updateSourceCodeFromNewStmts(): void
{
$newCode = $this->printer->printFormatPreserving(
Expand Down Expand Up @@ -1421,27 +1465,6 @@ private function buildNodeExprByValue($value): Node\Expr
return $nodeValue;
}

/**
* builds an PHPParser attribute node.
*
* @param string $attributeClass the attribute class which should be used for the attribute
* @param array $options the named arguments for the attribute ($key = argument name, $value = argument value)
*/
private function buildAttributeNode(string $attributeClass, array $options): Node\Attribute
{
$options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass);

$context = $this;
$nodeArguments = array_map(static function ($option, $value) use ($context) {
return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option));
}, array_keys($options), array_values($options));

return new Node\Attribute(
new Node\Name($attributeClass),
$nodeArguments
);
}

/**
* sort the given options based on the constructor parameters for the given $classString
* this prevents code inspections warnings for IDEs like intellij/phpstorm.
Expand Down
Loading