Skip to content

[MakeRegistration] add support for verify email attributes #1062

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

Merged
merged 1 commit into from
Feb 15, 2022
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
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a class level attribute that requires an aliased use statement, e.g. use Doctrine\Mapping as ORM is not possible in this scope without refactoring the ClassSourceManipulator::addUseStatementIfNecessary() method. A task which is not required by our current use case and should be handled in a separate PR.

{
$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
37 changes: 37 additions & 0 deletions tests/Util/ClassSourceManipulatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Symfony\Bundle\MakerBundle\Tests\Util;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use PhpParser\Builder\Param;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToMany;
Expand Down Expand Up @@ -167,6 +169,41 @@ public function getAddSetterTests()
];
}

/**
* @dataProvider getAttributeClassTests
*/
public function testAddAttributeToClass(string $sourceFilename, string $expectedSourceFilename, string $attributeClass, array $attributeOptions, string $attributePrefix = null): void
{
// @legacy Remove conditional when PHP < 8.0 support is dropped.
if ((\PHP_VERSION_ID < 80000)) {
$this->markTestSkipped('Requires PHP >= PHP 8.0');
}

$source = file_get_contents(__DIR__.'/fixtures/source/'.$sourceFilename);
$expectedSource = file_get_contents(__DIR__.'/fixtures/add_class_attribute/'.$expectedSourceFilename);
$manipulator = new ClassSourceManipulator($source);
$manipulator->addAttributeToClass($attributeClass, $attributeOptions, $attributePrefix);

self::assertSame($expectedSource, $manipulator->getSourceCode());
}

public function getAttributeClassTests(): \Generator
{
yield 'Empty class' => [
'User_empty.php',
'User_empty.php',
Entity::class,
[],
];

yield 'Class already has attributes' => [
'User_simple.php',
'User_simple.php',
Column::class,
['message' => 'We use this attribute for class level tests so we dont have to add additional test dependencies.'],
];
}

/**
* @dataProvider getAnnotationTests
*/
Expand Down
10 changes: 10 additions & 0 deletions tests/Util/fixtures/add_class_attribute/User_empty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping\Entity;

#[Entity]
class User
{
}
21 changes: 21 additions & 0 deletions tests/Util/fixtures/add_class_attribute/User_simple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;

#[ORM\Entity]
#[Column(message: 'We use this attribute for class level tests so we dont have to add additional test dependencies.')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;

public function getId(): ?int
{
return $this->id;
}
}