Skip to content

Commit

Permalink
Adding Entity attribute support
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
5 people committed Sep 24, 2021
1 parent 884f10d commit 9ecc684
Show file tree
Hide file tree
Showing 135 changed files with 4,085 additions and 639 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"require": {
"php": ">=7.1.3",
"doctrine/inflector": "^1.2|^2.0",
"nikic/php-parser": "^4.0",
"nikic/php-parser": "^4.11",
"symfony/config": "^4.0|^5.0",
"symfony/console": "^4.0|^5.0",
"symfony/dependency-injection": "^4.0|^5.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class DoctrineAttributesCheckPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$container->setParameter(
'maker.compatible_check.doctrine.supports_attributes',
$container->hasParameter('doctrine.orm.metadata.attribute.class')
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass;

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

if ('Doctrine\ORM\Mapping\Driver' === $namespace ? AnnotationDriver::class !== $class : !is_subclass_of($class, AbstractAnnotationDriver::class)) {
continue;
}

$id = sprintf('.%d_annotation_metadata_driver~%s', $i, ContainerBuilder::hash($arguments));
$id = sprintf('.%d_doctrine_metadata_driver~%s', $i, ContainerBuilder::hash($arguments));
$container->setDefinition($id, $arguments[0]);
$arguments[0] = new Reference($id);
$methodCalls[$i] = [$method, $arguments];
}

$isAnnotated = false !== strpos($arguments[0], '_annotation_metadata_driver');
$annotatedPrefixes[$managerName][] = [
$arguments[1],
$isAnnotated ? new Reference($arguments[0]) : null,
new Reference($arguments[0]),
];
}

$metadataDriverImpl->setMethodCalls($methodCalls);
}

if (null !== $annotatedPrefixes) {
$container->getDefinition('maker.doctrine_helper')->setArgument(2, $annotatedPrefixes);
$container->getDefinition('maker.doctrine_helper')->setArgument(4, $annotatedPrefixes);
}
}
}
87 changes: 73 additions & 14 deletions src/Doctrine/DoctrineHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
use Doctrine\Common\Persistence\Mapping\MappingException as LegacyPersistenceMappingException;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Mapping\NamingStrategy;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\AnnotationDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;

/**
* @author Fabien Potencier <fabien@symfony.com>
Expand All @@ -40,6 +43,7 @@ final class DoctrineHelper
* @var string
*/
private $entityNamespace;
private $phpCompatUtil;

/**
* @var ManagerRegistry
Expand All @@ -49,16 +53,20 @@ final class DoctrineHelper
/**
* @var array|null
*/
private $annotatedPrefixes;
private $mappingDriversByPrefix;

private $attributeMappingSupport;

/**
* @var ManagerRegistry|LegacyManagerRegistry
*/
public function __construct(string $entityNamespace, $registry = null, array $annotatedPrefixes = null)
public function __construct(string $entityNamespace, PhpCompatUtil $phpCompatUtil, $registry = null, bool $attributeMappingSupport = false, array $annotatedPrefixes = null)
{
$this->entityNamespace = trim($entityNamespace, '\\');
$this->phpCompatUtil = $phpCompatUtil;
$this->registry = $registry;
$this->annotatedPrefixes = $annotatedPrefixes;
$this->attributeMappingSupport = $attributeMappingSupport;
$this->mappingDriversByPrefix = $annotatedPrefixes;
}

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

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

return $this->isInstanceOf($this->getMappingDriverForNamespace($className), $driverClass);
}

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

if (null === $this->annotatedPrefixes) {
if (null === $this->mappingDriversByPrefix) {
// doctrine-bundle <= 2.2
$metadataDriver = $em->getConfiguration()->getMetadataDriverImpl();

if (!$this->isInstanceOf($metadataDriver, MappingDriverChain::class)) {
return $metadataDriver instanceof AnnotationDriver;
return $this->isInstanceOf($metadataDriver, $driverClass);
}

foreach ($metadataDriver->getDrivers() as $namespace => $driver) {
if (0 === strpos($className, $namespace)) {
return $driver instanceof AnnotationDriver;
return $this->isInstanceOf($driver, $driverClass);
}
}

return $metadataDriver->getDefaultDriver() instanceof AnnotationDriver;
return $this->isInstanceOf($metadataDriver->getDefaultDriver(), $driverClass);
}

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

foreach ($this->annotatedPrefixes[$managerName] as [$prefix, $annotationDriver]) {
foreach ($this->mappingDriversByPrefix[$managerName] as [$prefix, $prefixDriver]) {
if (0 === strpos($className, $prefix)) {
return null !== $annotationDriver;
return $this->isInstanceOf($prefixDriver, $driverClass);
}
}

return false;
}

public function isClassAnnotated(string $className): bool
{
return $this->doesClassUseDriver($className, AnnotationDriver::class);
}

public function doesClassUsesAttributes(string $className): bool
{
return $this->doesClassUseDriver($className, AttributeDriver::class);
}

public function isDoctrineSupportingAttributes(): bool
{
return $this->isDoctrineInstalled() && $this->attributeMappingSupport && $this->phpCompatUtil->canUseAttributes();
}

public function getEntitiesForAutocomplete(): array
{
$entities = [];
Expand Down Expand Up @@ -150,7 +182,7 @@ public function getMetadata(string $classOrNamespace = null, bool $disconnected
$classNames->setAccessible(true);

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

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

return $connection->getDatabasePlatform()->getReservedKeywordsList()->isKeyword($name);
}

/**
* this method tries to find the correct MappingDriver for the given namespace/class
* To determine which MappingDriver belongs to the class we check the prefixes configured in Doctrine and use the
* prefix that has the closest match to the given $namespace.
*
* this helper function is needed to create entities with the configuration of doctrine if they are not yet been registered
* in the ManagerRegistry
*/
private function getMappingDriverForNamespace(string $namespace): ?MappingDriver
{
$lowestCharacterDiff = null;
$foundDriver = null;

foreach ($this->mappingDriversByPrefix as $key => $mappings) {
foreach ($mappings as [$prefix, $driver]) {
$diff = substr_compare($namespace, $prefix, 0);

if ($diff >= 0 && (null === $lowestCharacterDiff || $diff < $lowestCharacterDiff)) {
$lowestCharacterDiff = $diff;
$foundDriver = $driver;
}
}
}

return $foundDriver;
}
}
1 change: 1 addition & 0 deletions src/Doctrine/EntityClassGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $
'broadcast' => $broadcast,
'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName),
'table_name' => $tableName,
'doctrine_use_attributes' => $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName()),
]
);

Expand Down
45 changes: 38 additions & 7 deletions src/Maker/MakeEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'Entity\\'
);

if (!$this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName())) {
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.');
}

$classExists = class_exists($entityClassDetails->getFullName());
if (!$classExists) {
$broadcast = $input->getOption('broadcast');
Expand All @@ -186,8 +190,11 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$generator->writeChanges();
}

if (!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())) {
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()));
if (
!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())
&& !$this->doesEntityUseAttributeMapping($entityClassDetails->getFullName())
) {
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()));
}

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

$currentFields = $this->getPropertyNames($entityClassDetails->getFullName());
$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite);
$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite, $entityClassDetails->getFullName());

$isFirstField = true;
while (true) {
Expand Down Expand Up @@ -232,7 +239,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$otherManipulator = $manipulator;
} else {
$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName());
}
switch ($newField->getType()) {
case EntityRelation::MANY_TO_ONE:
Expand All @@ -247,7 +254,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
// the new field being added to THIS entity is the inverse
$newFieldName = $newField->getInverseProperty();
$otherManipulatorFilename = $this->getPathOfClass($newField->getOwningClass());
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName());

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

private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite, string $className): ClassSourceManipulator
{
$manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite);
$useAttributes = $this->doctrineHelper->doesClassUsesAttributes($className) && $this->doctrineHelper->isDoctrineSupportingAttributes();
$useAnnotations = $this->doctrineHelper->isClassAnnotated($className) || !$useAttributes;

$manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite, $useAnnotations, true, $useAttributes);

$manipulator->setIo($io);

return $manipulator;
Expand Down Expand Up @@ -850,6 +861,26 @@ private function doesEntityUseAnnotationMapping(string $className): bool
return $this->doctrineHelper->isClassAnnotated($className);
}

private function doesEntityUseAttributeMapping(string $className): bool
{
if (\PHP_VERSION < 80000) {
return false;
}

if (!class_exists($className)) {
$otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true);

// if we have no metadata, we should assume this is the first class being mapped
if (empty($otherClassMetadatas)) {
return false;
}

$className = reset($otherClassMetadatas)->getName();
}

return $this->doctrineHelper->doesClassUsesAttributes($className);
}

private function getEntityNamespace(): string
{
return $this->doctrineHelper->getEntityNamespace();
Expand Down
8 changes: 7 additions & 1 deletion src/Maker/MakeResetPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,14 @@ private function generateRequestEntity(Generator $generator, ClassNameDetails $r

$generator->writeChanges();

$useAttributesForDoctrineMapping = $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($requestClassNameDetails->getFullName());

$manipulator = new ClassSourceManipulator(
$this->fileManager->getFileContents($requestEntityPath)
$this->fileManager->getFileContents($requestEntityPath),
false,
!$useAttributesForDoctrineMapping,
true,
$useAttributesForDoctrineMapping
);

$manipulator->addInterface(ResetPasswordRequestInterface::class);
Expand Down
Loading

0 comments on commit 9ecc684

Please sign in to comment.