Skip to content

Commit 3ca4dcc

Browse files
committed
feature #978 Adding Entity attribute support (weaverryan)
This PR was merged into the 1.0-dev branch. Discussion ---------- Adding Entity attribute support Hi! This is #920 cleaned up. Somehow, in that PR, the commits got tanged. For example, some commits were done twice, like SimonMarx@d12c2f9 and SimonMarx@3848f4f - but the bigger problem is that some commits (like the 2nd listed) have one date on GitHub (e.g. Aug 12th) but another date when pulled down locally (July 4th). Whatever is causing this seems to also cause, during a rebase, for these commits to be rebased on reverse. In short, it was an odd mess. To fix this, I've created a single commit with co-authors. I need to verify that this is ok with `@SimonMarx` and determine where these other two contributors came from :) (see #920 (comment)) Cheers! ``` 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> ``` Commits ------- 9ecc684 Adding Entity attribute support
2 parents 884f10d + 9ecc684 commit 3ca4dcc

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)