Skip to content

Commit 9ecc684

Browse files
weaverryanSimon MarxGeekimoadlpzjrushlow
committed
Adding Entity attribute support
Co-authored-by: Simon Marx <s.marx@shopmacher.de> Co-authored-by: Morgan ABRAHAM <morgan@geekimo.me> Co-authored-by: Adria Lopez <adria@prealfa.com> Co-authored-by: Jesse Rushlow <jr@rushlow.dev>
1 parent 884f10d commit 9ecc684

File tree

135 files changed

+4085
-639
lines changed

Some content is hidden

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

135 files changed

+4085
-639
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"require": {
1616
"php": ">=7.1.3",
1717
"doctrine/inflector": "^1.2|^2.0",
18-
"nikic/php-parser": "^4.0",
18+
"nikic/php-parser": "^4.11",
1919
"symfony/config": "^4.0|^5.0",
2020
"symfony/console": "^4.0|^5.0",
2121
"symfony/dependency-injection": "^4.0|^5.0",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DependencyInjection\CompilerPass;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
class DoctrineAttributesCheckPass implements CompilerPassInterface
18+
{
19+
public function process(ContainerBuilder $container): void
20+
{
21+
$container->setParameter(
22+
'maker.compatible_check.doctrine.supports_attributes',
23+
$container->hasParameter('doctrine.orm.metadata.attribute.class')
24+
);
25+
}
26+
}

src/DependencyInjection/CompilerPass/SetDoctrineAnnotatedPrefixesPass.php

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass;
1313

14-
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
15-
use Doctrine\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver;
1614
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1715
use Symfony\Component\DependencyInjection\ContainerBuilder;
1816
use Symfony\Component\DependencyInjection\Definition;
@@ -49,28 +47,23 @@ public function process(ContainerBuilder $container): void
4947
$class = $arguments[0]->getClass();
5048
$namespace = substr($class, 0, strrpos($class, '\\'));
5149

52-
if ('Doctrine\ORM\Mapping\Driver' === $namespace ? AnnotationDriver::class !== $class : !is_subclass_of($class, AbstractAnnotationDriver::class)) {
53-
continue;
54-
}
55-
56-
$id = sprintf('.%d_annotation_metadata_driver~%s', $i, ContainerBuilder::hash($arguments));
50+
$id = sprintf('.%d_doctrine_metadata_driver~%s', $i, ContainerBuilder::hash($arguments));
5751
$container->setDefinition($id, $arguments[0]);
5852
$arguments[0] = new Reference($id);
5953
$methodCalls[$i] = [$method, $arguments];
6054
}
6155

62-
$isAnnotated = false !== strpos($arguments[0], '_annotation_metadata_driver');
6356
$annotatedPrefixes[$managerName][] = [
6457
$arguments[1],
65-
$isAnnotated ? new Reference($arguments[0]) : null,
58+
new Reference($arguments[0]),
6659
];
6760
}
6861

6962
$metadataDriverImpl->setMethodCalls($methodCalls);
7063
}
7164

7265
if (null !== $annotatedPrefixes) {
73-
$container->getDefinition('maker.doctrine_helper')->setArgument(2, $annotatedPrefixes);
66+
$container->getDefinition('maker.doctrine_helper')->setArgument(4, $annotatedPrefixes);
7467
}
7568
}
7669
}

src/Doctrine/DoctrineHelper.php

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@
1616
use Doctrine\Common\Persistence\Mapping\MappingException as LegacyPersistenceMappingException;
1717
use Doctrine\DBAL\Connection;
1818
use Doctrine\ORM\EntityManagerInterface;
19+
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
1920
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
2021
use Doctrine\ORM\Mapping\NamingStrategy;
2122
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
2223
use Doctrine\Persistence\ManagerRegistry;
2324
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
2425
use Doctrine\Persistence\Mapping\ClassMetadata;
2526
use Doctrine\Persistence\Mapping\Driver\AnnotationDriver;
27+
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
2628
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
2729
use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException;
2830
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
31+
use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;
2932

3033
/**
3134
* @author Fabien Potencier <fabien@symfony.com>
@@ -40,6 +43,7 @@ final class DoctrineHelper
4043
* @var string
4144
*/
4245
private $entityNamespace;
46+
private $phpCompatUtil;
4347

4448
/**
4549
* @var ManagerRegistry
@@ -49,16 +53,20 @@ final class DoctrineHelper
4953
/**
5054
* @var array|null
5155
*/
52-
private $annotatedPrefixes;
56+
private $mappingDriversByPrefix;
57+
58+
private $attributeMappingSupport;
5359

5460
/**
5561
* @var ManagerRegistry|LegacyManagerRegistry
5662
*/
57-
public function __construct(string $entityNamespace, $registry = null, array $annotatedPrefixes = null)
63+
public function __construct(string $entityNamespace, PhpCompatUtil $phpCompatUtil, $registry = null, bool $attributeMappingSupport = false, array $annotatedPrefixes = null)
5864
{
5965
$this->entityNamespace = trim($entityNamespace, '\\');
66+
$this->phpCompatUtil = $phpCompatUtil;
6067
$this->registry = $registry;
61-
$this->annotatedPrefixes = $annotatedPrefixes;
68+
$this->attributeMappingSupport = $attributeMappingSupport;
69+
$this->mappingDriversByPrefix = $annotatedPrefixes;
6270
}
6371

6472
/**
@@ -85,43 +93,67 @@ public function getEntityNamespace(): string
8593
return $this->entityNamespace;
8694
}
8795

88-
public function isClassAnnotated(string $className): bool
96+
public function doesClassUseDriver(string $className, string $driverClass): bool
8997
{
90-
/** @var EntityManagerInterface $em */
91-
$em = $this->getRegistry()->getManagerForClass($className);
98+
try {
99+
/** @var EntityManagerInterface $em */
100+
$em = $this->getRegistry()->getManagerForClass($className);
101+
} catch (\ReflectionException $exception) {
102+
// this exception will be thrown by the registry if the class isn't created yet.
103+
// an example case is the "make:entity" command, which needs to know which driver is used for the class to determine
104+
// if the class should be generated with attributes or annotations. If this exception is thrown, we will check based on the
105+
// namespaces for the given $className and compare it with the doctrine configuration to get the correct MappingDriver.
106+
107+
return $this->isInstanceOf($this->getMappingDriverForNamespace($className), $driverClass);
108+
}
92109

93110
if (null === $em) {
94111
throw new \InvalidArgumentException(sprintf('Cannot find the entity manager for class "%s"', $className));
95112
}
96113

97-
if (null === $this->annotatedPrefixes) {
114+
if (null === $this->mappingDriversByPrefix) {
98115
// doctrine-bundle <= 2.2
99116
$metadataDriver = $em->getConfiguration()->getMetadataDriverImpl();
100117

101118
if (!$this->isInstanceOf($metadataDriver, MappingDriverChain::class)) {
102-
return $metadataDriver instanceof AnnotationDriver;
119+
return $this->isInstanceOf($metadataDriver, $driverClass);
103120
}
104121

105122
foreach ($metadataDriver->getDrivers() as $namespace => $driver) {
106123
if (0 === strpos($className, $namespace)) {
107-
return $driver instanceof AnnotationDriver;
124+
return $this->isInstanceOf($driver, $driverClass);
108125
}
109126
}
110127

111-
return $metadataDriver->getDefaultDriver() instanceof AnnotationDriver;
128+
return $this->isInstanceOf($metadataDriver->getDefaultDriver(), $driverClass);
112129
}
113130

114131
$managerName = array_search($em, $this->getRegistry()->getManagers(), true);
115132

116-
foreach ($this->annotatedPrefixes[$managerName] as [$prefix, $annotationDriver]) {
133+
foreach ($this->mappingDriversByPrefix[$managerName] as [$prefix, $prefixDriver]) {
117134
if (0 === strpos($className, $prefix)) {
118-
return null !== $annotationDriver;
135+
return $this->isInstanceOf($prefixDriver, $driverClass);
119136
}
120137
}
121138

122139
return false;
123140
}
124141

142+
public function isClassAnnotated(string $className): bool
143+
{
144+
return $this->doesClassUseDriver($className, AnnotationDriver::class);
145+
}
146+
147+
public function doesClassUsesAttributes(string $className): bool
148+
{
149+
return $this->doesClassUseDriver($className, AttributeDriver::class);
150+
}
151+
152+
public function isDoctrineSupportingAttributes(): bool
153+
{
154+
return $this->isDoctrineInstalled() && $this->attributeMappingSupport && $this->phpCompatUtil->canUseAttributes();
155+
}
156+
125157
public function getEntitiesForAutocomplete(): array
126158
{
127159
$entities = [];
@@ -150,7 +182,7 @@ public function getMetadata(string $classOrNamespace = null, bool $disconnected
150182
$classNames->setAccessible(true);
151183

152184
// Invalidating the cached AnnotationDriver::$classNames to find new Entity classes
153-
foreach ($this->annotatedPrefixes ?? [] as $managerName => $prefixes) {
185+
foreach ($this->mappingDriversByPrefix ?? [] as $managerName => $prefixes) {
154186
foreach ($prefixes as [$prefix, $annotationDriver]) {
155187
if (null !== $annotationDriver) {
156188
$classNames->setValue($annotationDriver, null);
@@ -182,7 +214,7 @@ public function getMetadata(string $classOrNamespace = null, bool $disconnected
182214
$cmf->setMetadataFor($m->getName(), $m);
183215
}
184216

185-
if (null === $this->annotatedPrefixes) {
217+
if (null === $this->mappingDriversByPrefix) {
186218
// Invalidating the cached AnnotationDriver::$classNames to find new Entity classes
187219
$metadataDriver = $em->getConfiguration()->getMetadataDriverImpl();
188220
if ($this->isInstanceOf($metadataDriver, MappingDriverChain::class)) {
@@ -265,4 +297,31 @@ public function isKeyword(string $name): bool
265297

266298
return $connection->getDatabasePlatform()->getReservedKeywordsList()->isKeyword($name);
267299
}
300+
301+
/**
302+
* this method tries to find the correct MappingDriver for the given namespace/class
303+
* To determine which MappingDriver belongs to the class we check the prefixes configured in Doctrine and use the
304+
* prefix that has the closest match to the given $namespace.
305+
*
306+
* this helper function is needed to create entities with the configuration of doctrine if they are not yet been registered
307+
* in the ManagerRegistry
308+
*/
309+
private function getMappingDriverForNamespace(string $namespace): ?MappingDriver
310+
{
311+
$lowestCharacterDiff = null;
312+
$foundDriver = null;
313+
314+
foreach ($this->mappingDriversByPrefix as $key => $mappings) {
315+
foreach ($mappings as [$prefix, $driver]) {
316+
$diff = substr_compare($namespace, $prefix, 0);
317+
318+
if ($diff >= 0 && (null === $lowestCharacterDiff || $diff < $lowestCharacterDiff)) {
319+
$lowestCharacterDiff = $diff;
320+
$foundDriver = $driver;
321+
}
322+
}
323+
}
324+
325+
return $foundDriver;
326+
}
268327
}

src/Doctrine/EntityClassGenerator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
5353
'broadcast' => $broadcast,
5454
'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName),
5555
'table_name' => $tableName,
56+
'doctrine_use_attributes' => $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName()),
5657
]
5758
);
5859

src/Maker/MakeEntity.php

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
160160
'Entity\\'
161161
);
162162

163+
if (!$this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName())) {
164+
throw new RuntimeCommandException('To use Doctrine entity attributes you\'ll need PHP 8, doctrine/orm 2.9, doctrine/doctrine-bundle 2.4 and symfony/framework-bundle 5.2.');
165+
}
166+
163167
$classExists = class_exists($entityClassDetails->getFullName());
164168
if (!$classExists) {
165169
$broadcast = $input->getOption('broadcast');
@@ -186,8 +190,11 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
186190
$generator->writeChanges();
187191
}
188192

189-
if (!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())) {
190-
throw new RuntimeCommandException(sprintf('Only annotation mapping is supported by make:entity, but the <info>%s</info> class uses a different format. If you would like this command to generate the properties & getter/setter methods, add your mapping configuration, and then re-run this command with the <info>--regenerate</info> flag.', $entityClassDetails->getFullName()));
193+
if (
194+
!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())
195+
&& !$this->doesEntityUseAttributeMapping($entityClassDetails->getFullName())
196+
) {
197+
throw new RuntimeCommandException(sprintf('Only annotation or attribute mapping is supported by make:entity, but the <info>%s</info> class uses a different format. If you would like this command to generate the properties & getter/setter methods, add your mapping configuration, and then re-run this command with the <info>--regenerate</info> flag.', $entityClassDetails->getFullName()));
191198
}
192199

193200
if ($classExists) {
@@ -204,7 +211,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
204211
}
205212

206213
$currentFields = $this->getPropertyNames($entityClassDetails->getFullName());
207-
$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite);
214+
$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite, $entityClassDetails->getFullName());
208215

209216
$isFirstField = true;
210217
while (true) {
@@ -232,7 +239,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
232239
$otherManipulator = $manipulator;
233240
} else {
234241
$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
235-
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
242+
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName());
236243
}
237244
switch ($newField->getType()) {
238245
case EntityRelation::MANY_TO_ONE:
@@ -247,7 +254,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
247254
// the new field being added to THIS entity is the inverse
248255
$newFieldName = $newField->getInverseProperty();
249256
$otherManipulatorFilename = $this->getPathOfClass($newField->getOwningClass());
250-
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
257+
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName());
251258

252259
// The *other* class will receive the ManyToOne
253260
$otherManipulator->addManyToOneRelation($newField->getOwningRelation());
@@ -793,9 +800,13 @@ private function askRelationType(ConsoleStyle $io, string $entityClass, string $
793800
return $io->askQuestion($question);
794801
}
795802

796-
private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
803+
private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite, string $className): ClassSourceManipulator
797804
{
798-
$manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite);
805+
$useAttributes = $this->doctrineHelper->doesClassUsesAttributes($className) && $this->doctrineHelper->isDoctrineSupportingAttributes();
806+
$useAnnotations = $this->doctrineHelper->isClassAnnotated($className) || !$useAttributes;
807+
808+
$manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite, $useAnnotations, true, $useAttributes);
809+
799810
$manipulator->setIo($io);
800811

801812
return $manipulator;
@@ -850,6 +861,26 @@ private function doesEntityUseAnnotationMapping(string $className): bool
850861
return $this->doctrineHelper->isClassAnnotated($className);
851862
}
852863

864+
private function doesEntityUseAttributeMapping(string $className): bool
865+
{
866+
if (\PHP_VERSION < 80000) {
867+
return false;
868+
}
869+
870+
if (!class_exists($className)) {
871+
$otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true);
872+
873+
// if we have no metadata, we should assume this is the first class being mapped
874+
if (empty($otherClassMetadatas)) {
875+
return false;
876+
}
877+
878+
$className = reset($otherClassMetadatas)->getName();
879+
}
880+
881+
return $this->doctrineHelper->doesClassUsesAttributes($className);
882+
}
883+
853884
private function getEntityNamespace(): string
854885
{
855886
return $this->doctrineHelper->getEntityNamespace();

src/Maker/MakeResetPassword.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,14 @@ private function generateRequestEntity(Generator $generator, ClassNameDetails $r
371371

372372
$generator->writeChanges();
373373

374+
$useAttributesForDoctrineMapping = $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($requestClassNameDetails->getFullName());
375+
374376
$manipulator = new ClassSourceManipulator(
375-
$this->fileManager->getFileContents($requestEntityPath)
377+
$this->fileManager->getFileContents($requestEntityPath),
378+
false,
379+
!$useAttributesForDoctrineMapping,
380+
true,
381+
$useAttributesForDoctrineMapping
376382
);
377383

378384
$manipulator->addInterface(ResetPasswordRequestInterface::class);

0 commit comments

Comments
 (0)