diff --git a/.github/workflows/ci-linux.yaml b/.github/workflows/ci-linux.yaml index b771d6ee8..083c68956 100644 --- a/.github/workflows/ci-linux.yaml +++ b/.github/workflows/ci-linux.yaml @@ -11,6 +11,7 @@ on: env: PHPUNIT_FLAGS: "-v" SYMFONY_PHPUNIT_DIR: "$HOME/symfony-bridge/.phpunit" + MAKER_SKIP_MERCURE_TEST: 1 jobs: coding-standards: diff --git a/composer.json b/composer.json index 2c7db2232..00f629c37 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "require-dev": { "composer/semver": "^3.0", "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.10.0", + "doctrine/orm": "^2.15|^3", "symfony/http-client": "^6.4|^7.0", "symfony/phpunit-bridge": "^6.4.1|^7.0", "symfony/security-core": "^6.4|^7.0", @@ -41,8 +41,8 @@ "sort-packages": true }, "conflict": { - "doctrine/orm": "<2.10", - "doctrine/doctrine-bundle": "<2.4" + "doctrine/orm": "<2.15", + "doctrine/doctrine-bundle": "<2.10" }, "autoload": { "psr-4": { "Symfony\\Bundle\\MakerBundle\\": "src/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 38d9c5c04..d35e0fca4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,8 @@ + + diff --git a/src/Doctrine/DoctrineHelper.php b/src/Doctrine/DoctrineHelper.php index 07f76fdfb..53ae3cfef 100644 --- a/src/Doctrine/DoctrineHelper.php +++ b/src/Doctrine/DoctrineHelper.php @@ -17,13 +17,13 @@ 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\MappingDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; +use Doctrine\Persistence\Mapping\StaticReflectionService; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -174,8 +174,8 @@ public function getMetadata(string $classOrNamespace = null, bool $disconnected $loaded = $this->isInstanceOf($cmf, AbstractClassMetadataFactory::class) ? $cmf->getLoadedMetadata() : []; } - $cmf = new DisconnectedClassMetadataFactory(); - $cmf->setEntityManager($em); + // Set the reflection service that was used in the now removed DisconnectedClassMetadataFactory::class + $cmf->setReflectionService(new StaticReflectionService()); foreach ($loaded as $m) { $cmf->setMetadataFor($m->getName(), $m); diff --git a/src/Doctrine/EntityRegenerator.php b/src/Doctrine/EntityRegenerator.php index 8dee3eabe..df0473a59 100644 --- a/src/Doctrine/EntityRegenerator.php +++ b/src/Doctrine/EntityRegenerator.php @@ -12,11 +12,13 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\EmbeddedClassMapping; use Doctrine\ORM\Mapping\MappingException; use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; /** @@ -75,7 +77,8 @@ public function regenerateEntities(string $classOrNamespace): void continue; } - $className = $mapping['class']; + /** @legacy - Remove conditional when ORM 2.x is no longer supported. */ + $className = ($mapping instanceof EmbeddedClassMapping) ? $mapping->class : $mapping['class']; $embeddedClasses[$fieldName] = $this->getPathOfClass($className); @@ -93,7 +96,10 @@ public function regenerateEntities(string $classOrNamespace): void if (str_contains($fieldName, '.')) { [$fieldName, $embeddedFiledName] = explode('.', $fieldName); - $operations[$embeddedClasses[$fieldName]]->addEntityField($embeddedFiledName, $mapping); + $property = ClassProperty::createFromObject($mapping); + $property->propertyName = $embeddedFiledName; + + $operations[$embeddedClasses[$fieldName]]->addEntityField($property); continue; } @@ -102,18 +108,9 @@ public function regenerateEntities(string $classOrNamespace): void continue; } - $manipulator->addEntityField($fieldName, $mapping); + $manipulator->addEntityField(ClassProperty::createFromObject($mapping)); } - $getIsNullable = function (array $mapping) { - if (!isset($mapping['joinColumns'][0]['nullable'])) { - // the default for relationships IS nullable - return true; - } - - return $mapping['joinColumns'][0]['nullable']; - }; - foreach ($classMetadata->associationMappings as $fieldName => $mapping) { if (!\in_array($fieldName, $mappedFields)) { continue; @@ -121,52 +118,19 @@ public function regenerateEntities(string $classOrNamespace): void switch ($mapping['type']) { case ClassMetadata::MANY_TO_ONE: - $relation = (new RelationManyToOne( - propertyName: $mapping['fieldName'], - targetClassName: $mapping['targetEntity'], - targetPropertyName: $mapping['inversedBy'], - mapInverseRelation: null !== $mapping['inversedBy'], - isOwning: true, - isNullable: $getIsNullable($mapping), - )); - - $manipulator->addManyToOneRelation($relation); + $manipulator->addManyToOneRelation(RelationManyToOne::createFromObject($mapping)); break; case ClassMetadata::ONE_TO_MANY: - $relation = (new RelationOneToMany( - propertyName: $mapping['fieldName'], - targetClassName: $mapping['targetEntity'], - targetPropertyName: $mapping['mappedBy'], - orphanRemoval: $mapping['orphanRemoval'], - )); - - $manipulator->addOneToManyRelation($relation); + $manipulator->addOneToManyRelation(RelationOneToMany::createFromObject($mapping)); break; case ClassMetadata::MANY_TO_MANY: - $relation = (new RelationManyToMany( - propertyName: $mapping['fieldName'], - targetClassName: $mapping['targetEntity'], - targetPropertyName: $mapping['mappedBy'], - mapInverseRelation: $mapping['isOwningSide'] ? (null !== $mapping['inversedBy']) : true, - isOwning: $mapping['isOwningSide'], - )); - - $manipulator->addManyToManyRelation($relation); + $manipulator->addManyToManyRelation(RelationManyToMany::createFromObject($mapping)); break; case ClassMetadata::ONE_TO_ONE: - $relation = (new RelationOneToOne( - propertyName: $mapping['fieldName'], - targetClassName: $mapping['targetEntity'], - targetPropertyName: $mapping['isOwningSide'] ? $mapping['inversedBy'] : $mapping['mappedBy'], - mapInverseRelation: $mapping['isOwningSide'] ? (null !== $mapping['inversedBy']) : true, - isOwning: $mapping['isOwningSide'], - isNullable: $getIsNullable($mapping), - )); - - $manipulator->addOneToOneRelation($relation); + $manipulator->addOneToOneRelation(RelationOneToOne::createFromObject($mapping)); break; default: diff --git a/src/Doctrine/RelationManyToMany.php b/src/Doctrine/RelationManyToMany.php index eaf0599d1..f9aa5ef2a 100644 --- a/src/Doctrine/RelationManyToMany.php +++ b/src/Doctrine/RelationManyToMany.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; +use Doctrine\ORM\Mapping\ManyToManyInverseSideMapping; +use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; use Symfony\Bundle\MakerBundle\Str; /** @@ -27,4 +29,36 @@ public function getTargetRemoverMethodName(): string { return 'remove'.Str::asCamelCase(Str::pluralCamelCaseToSingular($this->getTargetPropertyName())); } + + public static function createFromObject(ManyToManyInverseSideMapping|ManyToManyOwningSideMapping|array $mapping): self + { + /* @legacy Remove conditional when ORM 2.x is no longer supported! */ + if (\is_array($mapping)) { + return new self( + propertyName: $mapping['fieldName'], + targetClassName: $mapping['targetEntity'], + targetPropertyName: $mapping['mappedBy'], + mapInverseRelation: !$mapping['isOwningSide'] || null !== $mapping['inversedBy'], + isOwning: $mapping['isOwningSide'], + ); + } + + if ($mapping instanceof ManyToManyOwningSideMapping) { + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->inversedBy, + mapInverseRelation: (null !== $mapping->inversedBy), + isOwning: $mapping->isOwningSide(), + ); + } + + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->mappedBy, + mapInverseRelation: true, + isOwning: $mapping->isOwningSide(), + ); + } } diff --git a/src/Doctrine/RelationManyToOne.php b/src/Doctrine/RelationManyToOne.php index 97c26c619..d7bfa6ba1 100644 --- a/src/Doctrine/RelationManyToOne.php +++ b/src/Doctrine/RelationManyToOne.php @@ -11,9 +11,34 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; +use Doctrine\ORM\Mapping\ManyToOneAssociationMapping; + /** * @internal */ final class RelationManyToOne extends BaseRelation { + public static function createFromObject(ManyToOneAssociationMapping|array $mapping): self + { + /* @legacy Remove conditional when ORM 2.x is no longer supported! */ + if (\is_array($mapping)) { + return new self( + propertyName: $mapping['fieldName'], + targetClassName: $mapping['targetEntity'], + targetPropertyName: $mapping['inversedBy'], + mapInverseRelation: null !== $mapping['inversedBy'], + isOwning: true, + isNullable: $mapping['joinColumns'][0]['nullable'] ?? true, + ); + } + + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->inversedBy, + mapInverseRelation: null !== $mapping->inversedBy, + isOwning: true, + isNullable: $mapping->joinColumns[0]->nullable ?? true, + ); + } } diff --git a/src/Doctrine/RelationOneToMany.php b/src/Doctrine/RelationOneToMany.php index 1fa43dd30..7970f2f29 100644 --- a/src/Doctrine/RelationOneToMany.php +++ b/src/Doctrine/RelationOneToMany.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; +use Doctrine\ORM\Mapping\OneToManyAssociationMapping; use Symfony\Bundle\MakerBundle\Str; /** @@ -32,4 +33,24 @@ public function isMapInverseRelation(): bool { throw new \Exception('OneToMany IS the inverse side!'); } + + public static function createFromObject(OneToManyAssociationMapping|array $mapping): self + { + /* @legacy Remove conditional when ORM 2.x is no longer supported! */ + if (\is_array($mapping)) { + return new self( + propertyName: $mapping['fieldName'], + targetClassName: $mapping['targetEntity'], + targetPropertyName: $mapping['mappedBy'], + orphanRemoval: $mapping['orphanRemoval'], + ); + } + + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->mappedBy, + orphanRemoval: $mapping->orphanRemoval, + ); + } } diff --git a/src/Doctrine/RelationOneToOne.php b/src/Doctrine/RelationOneToOne.php index 8b970a9bd..72ecd5cc7 100644 --- a/src/Doctrine/RelationOneToOne.php +++ b/src/Doctrine/RelationOneToOne.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; +use Doctrine\ORM\Mapping\OneToOneInverseSideMapping; +use Doctrine\ORM\Mapping\OneToOneOwningSideMapping; use Symfony\Bundle\MakerBundle\Str; /** @@ -27,4 +29,39 @@ public function getTargetSetterMethodName(): string { return 'set'.Str::asCamelCase($this->getTargetPropertyName()); } + + public static function createFromObject(OneToOneInverseSideMapping|OneToOneOwningSideMapping|array $mapping): self + { + /* @legacy Remove conditional when ORM 2.x is no longer supported! */ + if (\is_array($mapping)) { + return new self( + propertyName: $mapping['fieldName'], + targetClassName: $mapping['targetEntity'], + targetPropertyName: $mapping['isOwningSide'] ? $mapping['inversedBy'] : $mapping['mappedBy'], + mapInverseRelation: !$mapping['isOwningSide'] || null !== $mapping['inversedBy'], + isOwning: $mapping['isOwningSide'], + isNullable: $mapping['joinColumns'][0]['nullable'] ?? true, + ); + } + + if ($mapping instanceof OneToOneOwningSideMapping) { + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->inversedBy, + mapInverseRelation: (null !== $mapping->inversedBy), + isOwning: true, + isNullable: $mapping->joinColumns[0]->nullable ?? true, + ); + } + + return new self( + propertyName: $mapping->fieldName, + targetClassName: $mapping->targetEntity, + targetPropertyName: $mapping->mappedBy, + mapInverseRelation: true, + isOwning: false, + isNullable: $mapping->joinColumns[0]->nullable ?? true, + ); + } } diff --git a/src/Maker/MakeEntity.php b/src/Maker/MakeEntity.php index 07896c675..cc3818185 100644 --- a/src/Maker/MakeEntity.php +++ b/src/Maker/MakeEntity.php @@ -28,6 +28,7 @@ use Symfony\Bundle\MakerBundle\Util\ClassDetails; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\CliOutputHelper; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Command\Command; @@ -227,12 +228,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $fileManagerOperations = []; $fileManagerOperations[$entityPath] = $manipulator; - if (\is_array($newField)) { - $annotationOptions = $newField; - unset($annotationOptions['fieldName']); - $manipulator->addEntityField($newField['fieldName'], $annotationOptions); + if ($newField instanceof ClassProperty) { + $manipulator->addEntityField($newField); - $currentFields[] = $newField['fieldName']; + $currentFields[] = $newField->propertyName; } elseif ($newField instanceof EntityRelation) { // both overridden below for OneToMany $newFieldName = $newField->getOwningProperty(); @@ -329,7 +328,7 @@ public function configureDependencies(DependencyBuilder $dependencies, InputInte ORMDependencyBuilder::buildDependencies($dependencies); } - private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField): EntityRelation|array|null + private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField): EntityRelation|ClassProperty|null { $io->writeln(''); @@ -407,23 +406,24 @@ private function askForNextField(ConsoleStyle $io, array $fields, string $entity } // this is a normal field - $data = ['fieldName' => $fieldName, 'type' => $type]; + $classProperty = new ClassProperty(propertyName: $fieldName, type: $type); + if ('string' === $type) { // default to 255, avoid the question - $data['length'] = $io->ask('Field length', 255, [Validator::class, 'validateLength']); + $classProperty->length = $io->ask('Field length', 255, [Validator::class, 'validateLength']); } elseif ('decimal' === $type) { // 10 is the default value given in \Doctrine\DBAL\Schema\Column::$_precision - $data['precision'] = $io->ask('Precision (total number of digits stored: 100.00 would be 5)', 10, [Validator::class, 'validatePrecision']); + $classProperty->precision = $io->ask('Precision (total number of digits stored: 100.00 would be 5)', 10, [Validator::class, 'validatePrecision']); // 0 is the default value given in \Doctrine\DBAL\Schema\Column::$_scale - $data['scale'] = $io->ask('Scale (number of decimals to store: 100.00 would be 2)', 0, [Validator::class, 'validateScale']); + $classProperty->scale = $io->ask('Scale (number of decimals to store: 100.00 would be 2)', 0, [Validator::class, 'validateScale']); } if ($io->confirm('Can this field be null in the database (nullable)', false)) { - $data['nullable'] = true; + $classProperty->nullable = true; } - return $data; + return $classProperty; } private function printAvailableTypes(ConsoleStyle $io): void diff --git a/src/Security/UserClassBuilder.php b/src/Security/UserClassBuilder.php index 376879f11..f77bfd132 100644 --- a/src/Security/UserClassBuilder.php +++ b/src/Security/UserClassBuilder.php @@ -13,6 +13,7 @@ use PhpParser\Node; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -52,13 +53,12 @@ private function addGetUsername(ClassSourceManipulator $manipulator, UserClassCo if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( - $userClassConfig->getIdentityPropertyName(), - [ - 'type' => 'string', - // https://github.com/FriendsOfSymfony/FOSUserBundle/issues/1919 - 'length' => 180, - 'unique' => true, - ] + new ClassProperty( + propertyName: $userClassConfig->getIdentityPropertyName(), + type: 'string', + length: 180, + unique: true, + ) ); } else { // add normal property @@ -99,10 +99,7 @@ private function addGetRoles(ClassSourceManipulator $manipulator, UserClassConfi if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( - 'roles', - [ - 'type' => 'json', - ] + new ClassProperty(propertyName: 'roles', type: 'json') ); } else { // add normal property @@ -202,11 +199,7 @@ private function addGetPassword(ClassSourceManipulator $manipulator, UserClassCo if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( - 'password', - [ - 'type' => 'string', - ], - [$propertyDocs] + new ClassProperty(propertyName: 'password', type: 'string', comments: [$propertyDocs]) ); } else { // add normal property diff --git a/src/Test/MakerTestProcess.php b/src/Test/MakerTestProcess.php index 9257c7206..dc3acb301 100644 --- a/src/Test/MakerTestProcess.php +++ b/src/Test/MakerTestProcess.php @@ -45,6 +45,11 @@ public function setInput($input): self public function run($allowToFail = false, array $envVars = []): self { + if (false !== ($timeout = getenv('MAKER_PROCESS_TIMEOUT'))) { + // Setting a value of null allows for step debugging + $this->process->setTimeout($timeout); + } + $this->process->run(null, $envVars); if (!$allowToFail && !$this->process->isSuccessful()) { diff --git a/src/Test/MakerTestRunner.php b/src/Test/MakerTestRunner.php index 72583409c..3ef18139f 100644 --- a/src/Test/MakerTestRunner.php +++ b/src/Test/MakerTestRunner.php @@ -173,13 +173,18 @@ public function configureDatabase(bool $createSchema = true): void // this looks silly, but it's the only way to drop the database *for sure*, // as doctrine:database:drop will error if there is no database - // also, skip for SQLITE, as it does not support --if-not-exists - if (!str_starts_with(getenv('TEST_DATABASE_DSN'), 'sqlite://')) { + if (!$usingSqlite = str_starts_with(getenv('TEST_DATABASE_DSN'), 'sqlite')) { + // --if-not-exists not supported on SQLite $this->runConsole('doctrine:database:create', [], '--env=test --if-not-exists'); } + $this->runConsole('doctrine:database:drop', [], '--env=test --force'); - $this->runConsole('doctrine:database:create', [], '--env=test'); + if (!$usingSqlite) { + // d:d:create not supported on SQLite + $this->runConsole('doctrine:database:create', [], '--env=test'); + } + if ($createSchema) { $this->runConsole('doctrine:schema:create', [], '--env=test'); } diff --git a/src/Util/ClassSource/Model/ClassProperty.php b/src/Util/ClassSource/Model/ClassProperty.php new file mode 100644 index 000000000..220c42029 --- /dev/null +++ b/src/Util/ClassSource/Model/ClassProperty.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Util\ClassSource\Model; + +use Doctrine\ORM\Mapping\FieldMapping; +use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; + +/** + * @author Jesse Rushlow + * + * @internal + */ +final class ClassProperty +{ + public function __construct( + public string $propertyName, + public string $type, + public array $comments = [], + public ?int $length = null, + public ?bool $id = null, + public ?bool $nullable = null, + public array $options = [], + public ?int $precision = null, + public ?int $scale = null, + public bool $needsTypeHint = true, + public bool $unique = false, + ) { + } + + public function getAttributes(): array + { + $attributes = []; + + if ($this->needsTypeHint) { + $attributes['type'] = $this->type; + } + + if (!empty($this->options)) { + $attributes['options'] = $this->options; + } + + if ($this->unique) { + $attributes['unique'] = true; + } + + foreach (['length', 'id', 'nullable', 'precision', 'scale'] as $property) { + if (null !== $this->$property) { + $attributes[$property] = $this->$property; + } + } + + return $attributes; + } + + public static function createFromObject(FieldMapping|array $data): self + { + if ($data instanceof FieldMapping) { + return new self( + propertyName: $data->fieldName, + type: $data->type, + length: $data->length, + id: $data->id ?? false, + nullable: $data->nullable ?? false, + options: $data->options ?? [], + precision: $data->precision, + scale: $data->scale, + unique: $data->unique ?? false, + ); + } + + /* @legacy Remove when ORM 2.x is no longer supported. */ + if (empty($data['fieldName']) || empty($data['type'])) { + throw new RuntimeCommandException('Cannot create property model - "fieldName" & "type" are required.'); + } + + return new self( + propertyName: $data['fieldName'], + type: $data['type'], + comments: $data['comments'] ?? [], + length: $data['length'] ?? null, + id: $data['id'] ?? false, + nullable: $data['nullable'] ?? false, + options: $data['options'] ?? [], + precision: $data['precision'] ?? null, + scale: $data['scale'] ?? null, + unique: $data['unique'] ?? false, + ); + } +} diff --git a/src/Util/ClassSourceManipulator.php b/src/Util/ClassSourceManipulator.php index 833b80dcb..9d429624d 100644 --- a/src/Util/ClassSourceManipulator.php +++ b/src/Util/ClassSourceManipulator.php @@ -38,6 +38,7 @@ use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToMany; use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToOne; use Symfony\Bundle\MakerBundle\Str; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty; /** * @internal @@ -96,27 +97,27 @@ public function getSourceCode(): string return $this->sourceCode; } - public function addEntityField(string $propertyName, array $columnOptions, array $comments = []): void + public function addEntityField(ClassProperty $mapping): void { - $typeHint = DoctrineHelper::getPropertyTypeForColumn($columnOptions['type']); - if ($typeHint && DoctrineHelper::canColumnTypeBeInferredByPropertyType($columnOptions['type'], $typeHint)) { - unset($columnOptions['type']); + $typeHint = DoctrineHelper::getPropertyTypeForColumn($mapping->type); + if ($typeHint && DoctrineHelper::canColumnTypeBeInferredByPropertyType($mapping->type, $typeHint)) { + $mapping->needsTypeHint = false; } - if (isset($columnOptions['type'])) { - $typeConstant = DoctrineHelper::getTypeConstant($columnOptions['type']); + if ($mapping->needsTypeHint) { + $typeConstant = DoctrineHelper::getTypeConstant($mapping->type); if ($typeConstant) { $this->addUseStatementIfNecessary(Types::class); - $columnOptions['type'] = $typeConstant; + $mapping->type = $typeConstant; } } // 2) USE property type on property below, nullable // 3) If default value, then NOT nullable - $nullable = $columnOptions['nullable'] ?? false; - $isId = (bool) ($columnOptions['id'] ?? false); - $attributes[] = $this->buildAttributeNode(Column::class, $columnOptions, 'ORM'); + $nullable = $mapping->nullable ?? false; + + $attributes[] = $this->buildAttributeNode(Column::class, $mapping->getAttributes(), 'ORM'); $defaultValue = null; if ('array' === $typeHint && !$nullable) { @@ -132,15 +133,15 @@ public function addEntityField(string $propertyName, array $columnOptions, array } $this->addProperty( - name: $propertyName, + name: $mapping->propertyName, defaultValue: $defaultValue, attributes: $attributes, - comments: $comments, + comments: $mapping->comments, propertyType: $propertyType ); $this->addGetter( - $propertyName, + $mapping->propertyName, $typeHint, // getter methods always have nullable return values // because even though these are required in the db, they may not be set yet @@ -149,8 +150,8 @@ public function addEntityField(string $propertyName, array $columnOptions, array ); // don't generate setters for id fields - if (!$isId) { - $this->addSetter($propertyName, $typeHint, $nullable); + if (!($mapping->id ?? false)) { + $this->addSetter($mapping->propertyName, $typeHint, $nullable); } } @@ -310,7 +311,7 @@ public function addConstructor(array $params, string $methodBody): void /** * @param Node[] $params */ - public function addMethodBuilder(Builder\Method $methodBuilder, array $params = [], string $methodBody = null): void + public function addMethodBuilder(Builder\Method $methodBuilder, array $params = [], ?string $methodBody = null): void { $this->addMethodParams($methodBuilder, $params); @@ -359,7 +360,7 @@ public function createMethodLevelBlankLine() /** * @param array $attributes */ - public function addProperty(string $name, $defaultValue = self::DEFAULT_VALUE_NONE, array $attributes = [], array $comments = [], string $propertyType = null): void + public function addProperty(string $name, $defaultValue = self::DEFAULT_VALUE_NONE, array $attributes = [], array $comments = [], ?string $propertyType = null): void { if ($this->propertyExists($name)) { // we never overwrite properties @@ -843,7 +844,7 @@ public function addUseStatementIfNecessary(string $class): string * @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 + public function buildAttributeNode(string $attributeClass, array $options, ?string $attributePrefix = null): Node\Attribute { $options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass); diff --git a/tests/Util/ClassSourceManipulatorTest.php b/tests/Util/ClassSourceManipulatorTest.php index 33c44fb9c..193587b79 100644 --- a/tests/Util/ClassSourceManipulatorTest.php +++ b/tests/Util/ClassSourceManipulatorTest.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\FieldMapping; use PhpParser\Builder\Param; use PHPUnit\Framework\TestCase; use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToMany; @@ -20,6 +21,7 @@ use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToMany; use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToOne; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; +use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty; use Symfony\Component\Security\Core\User\UserInterface; class ClassSourceManipulatorTest extends TestCase @@ -213,97 +215,70 @@ public function getAttributeClassTests(): \Generator /** * @dataProvider getAddEntityFieldTests */ - public function testAddEntityField(string $sourceFilename, string $propertyName, array $fieldOptions, $expectedSourceFilename): void + public function testAddEntityField(string $sourceFilename, ClassProperty $propertyModel, $expectedSourceFilename): void { $sourcePath = __DIR__.'/fixtures/source'; $expectedPath = __DIR__.'/fixtures/add_entity_field'; $this->runAddEntityFieldTests( file_get_contents(sprintf('%s/%s', $sourcePath, $sourceFilename)), - $propertyName, - $fieldOptions, + $propertyModel, file_get_contents(sprintf('%s/%s', $expectedPath, $expectedSourceFilename)) ); } - private function runAddEntityFieldTests(string $source, string $propertyName, array $fieldOptions, string $expected): void + private function runAddEntityFieldTests(string $source, ClassProperty $fieldOptions, string $expected): void { $manipulator = new ClassSourceManipulator($source, false); - $manipulator->addEntityField($propertyName, $fieldOptions); + $manipulator->addEntityField($fieldOptions); $this->assertSame($expected, $manipulator->getSourceCode()); } public function getAddEntityFieldTests(): \Generator { + /** @legacy - Remove when Doctrine/ORM 2.x is no longer supported. */ + $isLegacy = !class_exists(FieldMapping::class); + yield 'entity_normal_add' => [ 'User_simple.php', - 'fooProp', - [ - 'type' => 'string', - 'length' => 255, - 'nullable' => false, - 'options' => ['comment' => 'new field'], - ], + new ClassProperty(propertyName: 'fooProp', type: 'string', length: 255, nullable: false, options: ['comment' => 'new field']), 'User_simple.php', ]; yield 'entity_add_datetime' => [ 'User_simple.php', - 'createdAt', - [ - 'type' => 'datetime', - 'nullable' => true, - ], + new ClassProperty(propertyName: 'createdAt', type: 'datetime', nullable: true), 'User_simple_datetime.php', ]; yield 'entity_field_property_already_exists' => [ 'User_some_props.php', - 'firstName', - [ - 'type' => 'string', - 'length' => 255, - 'nullable' => false, - ], + new ClassProperty(propertyName: 'firstName', type: 'string', length: 255, nullable: false), 'User_simple_prop_already_exists.php', ]; yield 'entity_field_property_zero' => [ 'User_simple.php', - 'decimal', - [ - 'type' => 'decimal', - 'precision' => 6, - 'scale' => 0, - ], + new ClassProperty(propertyName: 'decimal', type: 'decimal', precision: 6, scale: 0), 'User_simple_prop_zero.php', ]; yield 'entity_add_object' => [ 'User_simple.php', - 'someObject', - [ - 'type' => 'object', - ], - 'User_simple_object.php', + new ClassProperty(propertyName: 'someObject', type: 'object'), + $isLegacy ? 'legacy/User_simple_object.php' : 'User_simple_object.php', ]; yield 'entity_add_uuid' => [ 'User_simple.php', - 'uuid', - [ - 'type' => 'uuid', - ], + new ClassProperty(propertyName: 'uuid', type: 'uuid'), 'User_simple_uuid.php', ]; yield 'entity_add_ulid' => [ 'User_simple.php', - 'ulid', - [ - 'type' => 'ulid', - ], + new ClassProperty(propertyName: 'ulid', type: 'ulid'), 'User_simple_ulid.php', ]; } @@ -414,6 +389,11 @@ public function testAddOneToManyRelation(string $sourceFilename, string $expecte $sourcePath = __DIR__.'/fixtures/source'; $expectedPath = __DIR__.'/fixtures/add_one_to_many_relation'; + /** @legacy - Remove when Doctrine/ORM 2.x is no longer supported. */ + if (!class_exists(FieldMapping::class)) { + $expectedPath.= '/legacy'; + } + $this->runAddOneToManyRelationTests( file_get_contents(sprintf('%s/%s', $sourcePath, $sourceFilename)), file_get_contents(sprintf('%s/%s', $expectedPath, $expectedSourceFilename)), @@ -551,6 +531,9 @@ private function runAddOneToOneRelation(string $source, string $expected, Relati public function getAddOneToOneRelationTests(): \Generator { + /** @legacy - Remove when Doctrine/ORM 2.x is no longer supported. */ + $isLegacy = !class_exists(FieldMapping::class); + yield 'one_to_one_owning' => [ 'User_simple.php', 'User_simple_owning.php', @@ -566,7 +549,7 @@ public function getAddOneToOneRelationTests(): \Generator // a relationship to yourself - return type is self yield 'one_to_one_owning_self' => [ 'User_simple.php', - 'User_simple_self.php', + $isLegacy ? 'legacy/User_simple_self.php' : 'User_simple_self.php', new RelationOneToOne( propertyName: 'embeddedUser', targetClassName: \App\Entity\User::class, diff --git a/tests/Util/fixtures/add_entity_field/User_simple_object.php b/tests/Util/fixtures/add_entity_field/User_simple_object.php index 0ed05a87b..c4c9524f9 100644 --- a/tests/Util/fixtures/add_entity_field/User_simple_object.php +++ b/tests/Util/fixtures/add_entity_field/User_simple_object.php @@ -2,7 +2,6 @@ namespace App\Entity; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] @@ -13,7 +12,7 @@ class User #[ORM\Column()] private ?int $id = null; - #[ORM\Column(type: Types::OBJECT)] + #[ORM\Column(type: 'object')] private ?object $someObject = null; public function getId(): ?int diff --git a/tests/Util/fixtures/add_entity_field/legacy/User_simple_object.php b/tests/Util/fixtures/add_entity_field/legacy/User_simple_object.php new file mode 100644 index 000000000..0ed05a87b --- /dev/null +++ b/tests/Util/fixtures/add_entity_field/legacy/User_simple_object.php @@ -0,0 +1,35 @@ +id; + } + + public function getSomeObject(): ?object + { + return $this->someObject; + } + + public function setSomeObject(object $someObject): static + { + $this->someObject = $someObject; + + return $this; + } +} diff --git a/tests/Util/fixtures/add_one_to_many_relation/User_simple.php b/tests/Util/fixtures/add_one_to_many_relation/User_simple.php index e742e7ef0..915cae2a6 100644 --- a/tests/Util/fixtures/add_one_to_many_relation/User_simple.php +++ b/tests/Util/fixtures/add_one_to_many_relation/User_simple.php @@ -14,7 +14,7 @@ class User #[ORM\Column()] private ?int $id = null; - #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserAvatarPhoto::class)] + #[ORM\OneToMany(targetEntity: UserAvatarPhoto::class, mappedBy: 'user')] private Collection $avatarPhotos; public function __construct() diff --git a/tests/Util/fixtures/add_one_to_many_relation/User_simple_orphan_removal.php b/tests/Util/fixtures/add_one_to_many_relation/User_simple_orphan_removal.php index e76e70d81..effc715bf 100644 --- a/tests/Util/fixtures/add_one_to_many_relation/User_simple_orphan_removal.php +++ b/tests/Util/fixtures/add_one_to_many_relation/User_simple_orphan_removal.php @@ -14,7 +14,7 @@ class User #[ORM\Column()] private ?int $id = null; - #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserAvatarPhoto::class, orphanRemoval: true)] + #[ORM\OneToMany(targetEntity: UserAvatarPhoto::class, mappedBy: 'user', orphanRemoval: true)] private Collection $avatarPhotos; public function __construct() diff --git a/tests/Util/fixtures/add_one_to_many_relation/User_with_use_statements.php b/tests/Util/fixtures/add_one_to_many_relation/User_with_use_statements.php index 698741299..bf907e58c 100644 --- a/tests/Util/fixtures/add_one_to_many_relation/User_with_use_statements.php +++ b/tests/Util/fixtures/add_one_to_many_relation/User_with_use_statements.php @@ -17,7 +17,7 @@ class User #[ORM\Column()] private ?int $id = null; - #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserAvatarPhoto::class)] + #[ORM\OneToMany(targetEntity: UserAvatarPhoto::class, mappedBy: 'user')] private Collection $avatarPhotos; public function __construct() diff --git a/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple.php b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple.php new file mode 100644 index 000000000..e742e7ef0 --- /dev/null +++ b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple.php @@ -0,0 +1,59 @@ +avatarPhotos = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getAvatarPhotos(): Collection + { + return $this->avatarPhotos; + } + + public function addAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if (!$this->avatarPhotos->contains($avatarPhoto)) { + $this->avatarPhotos->add($avatarPhoto); + $avatarPhoto->setUser($this); + } + + return $this; + } + + public function removeAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if ($this->avatarPhotos->removeElement($avatarPhoto)) { + // set the owning side to null (unless already changed) + if ($avatarPhoto->getUser() === $this) { + $avatarPhoto->setUser(null); + } + } + + return $this; + } +} diff --git a/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple_orphan_removal.php b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple_orphan_removal.php new file mode 100644 index 000000000..e76e70d81 --- /dev/null +++ b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_simple_orphan_removal.php @@ -0,0 +1,59 @@ +avatarPhotos = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getAvatarPhotos(): Collection + { + return $this->avatarPhotos; + } + + public function addAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if (!$this->avatarPhotos->contains($avatarPhoto)) { + $this->avatarPhotos->add($avatarPhoto); + $avatarPhoto->setUser($this); + } + + return $this; + } + + public function removeAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if ($this->avatarPhotos->removeElement($avatarPhoto)) { + // set the owning side to null (unless already changed) + if ($avatarPhoto->getUser() === $this) { + $avatarPhoto->setUser(null); + } + } + + return $this; + } +} diff --git a/tests/Util/fixtures/add_one_to_many_relation/legacy/User_with_use_statements.php b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_with_use_statements.php new file mode 100644 index 000000000..698741299 --- /dev/null +++ b/tests/Util/fixtures/add_one_to_many_relation/legacy/User_with_use_statements.php @@ -0,0 +1,62 @@ +avatarPhotos = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getAvatarPhotos(): Collection + { + return $this->avatarPhotos; + } + + public function addAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if (!$this->avatarPhotos->contains($avatarPhoto)) { + $this->avatarPhotos->add($avatarPhoto); + $avatarPhoto->setUser($this); + } + + return $this; + } + + public function removeAvatarPhoto(UserAvatarPhoto $avatarPhoto): static + { + if ($this->avatarPhotos->removeElement($avatarPhoto)) { + // set the owning side to null (unless already changed) + if ($avatarPhoto->getUser() === $this) { + $avatarPhoto->setUser(null); + } + } + + return $this; + } +} diff --git a/tests/Util/fixtures/add_one_to_one_relation/User_simple_self.php b/tests/Util/fixtures/add_one_to_one_relation/User_simple_self.php index 22e44cc9d..6ee090a86 100644 --- a/tests/Util/fixtures/add_one_to_one_relation/User_simple_self.php +++ b/tests/Util/fixtures/add_one_to_one_relation/User_simple_self.php @@ -12,7 +12,7 @@ class User #[ORM\Column()] private ?int $id = null; - #[ORM\OneToOne(inversedBy: 'user', targetEntity: self::class, cascade: ['persist', 'remove'])] + #[ORM\OneToOne(targetEntity: self::class, inversedBy: 'user', cascade: ['persist', 'remove'])] private ?self $embeddedUser = null; public function getId(): ?int diff --git a/tests/Util/fixtures/add_one_to_one_relation/legacy/User_simple_self.php b/tests/Util/fixtures/add_one_to_one_relation/legacy/User_simple_self.php new file mode 100644 index 000000000..22e44cc9d --- /dev/null +++ b/tests/Util/fixtures/add_one_to_one_relation/legacy/User_simple_self.php @@ -0,0 +1,34 @@ +id; + } + + public function getEmbeddedUser(): ?self + { + return $this->embeddedUser; + } + + public function setEmbeddedUser(?self $embeddedUser): static + { + $this->embeddedUser = $embeddedUser; + + return $this; + } +} diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php index 6652306b8..5be663aa6 100644 --- a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php +++ b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php @@ -6,10 +6,10 @@ use App\Entity\Book; use App\Form\BookType; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; @@ -80,7 +80,7 @@ protected function getExtensions(): array [Library::class, new ClassMetadata(Library::class)], ]); - $execute = $this->createMock(AbstractQuery::class); + $execute = $this->createMock(Query::class); $execute->method('execute') ->willReturn([ (new Library())->setName('foo'), diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php index 49e5626c0..f73845cd5 100644 --- a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php +++ b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php @@ -5,10 +5,10 @@ use App\Entity\Author; use App\Entity\Book; use App\Form\BookType; -use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; @@ -53,17 +53,16 @@ protected function getExtensions(): array ]) ; - $execute = $this->createMock(AbstractQuery::class); + $execute = $this->createMock(Query::class); $execute->method('execute') ->willReturn([ - (new Author())->setName('foo') + (new Author())->setName('foo'), ]); $query = $this->createMock(QueryBuilder::class); $query->method('getQuery') ->willReturn($execute); - $entityRepository = $this->createMock(EntityRepository::class); $entityRepository->method('createQueryBuilder') ->willReturn($query) diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php index ef5b6de47..deb501726 100644 --- a/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php +++ b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php @@ -5,10 +5,10 @@ use App\Entity\Librarian; use App\Entity\Library; use App\Form\LibraryType; -use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; @@ -53,17 +53,16 @@ protected function getExtensions(): array ]) ; - $execute = $this->createMock(AbstractQuery::class); + $execute = $this->createMock(Query::class); $execute->method('execute') ->willReturn([ - (new Librarian())->setName('foo') + (new Librarian())->setName('foo'), ]); $query = $this->createMock(QueryBuilder::class); $query->method('getQuery') ->willReturn($execute); - $entityRepository = $this->createMock(EntityRepository::class); $entityRepository->method('createQueryBuilder') ->willReturn($query)