Skip to content

Adding Entity attribute support #978

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
Sep 27, 2021
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
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 <[email protected]>
*
* 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 <[email protected]>
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