vendor/symfony/maker-bundle/src/Maker/MakeEntity.php line 318
<?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\Maker;use ApiPlatform\Core\Annotation\ApiResource as LegacyApiResource;use ApiPlatform\Metadata\ApiResource;use Doctrine\DBAL\Types\Type;use Symfony\Bundle\MakerBundle\ConsoleStyle;use Symfony\Bundle\MakerBundle\DependencyBuilder;use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator;use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation;use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;use Symfony\Bundle\MakerBundle\FileManager;use Symfony\Bundle\MakerBundle\Generator;use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;use Symfony\Bundle\MakerBundle\InputConfiguration;use Symfony\Bundle\MakerBundle\Str;use Symfony\Bundle\MakerBundle\Util\ClassDetails;use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;use Symfony\Bundle\MakerBundle\Validator;use Symfony\Component\Console\Command\Command;use Symfony\Component\Console\Input\InputArgument;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Input\InputOption;use Symfony\Component\Console\Question\ConfirmationQuestion;use Symfony\Component\Console\Question\Question;use Symfony\UX\Turbo\Attribute\Broadcast;/*** @author Javier Eguiluz <javier.eguiluz@gmail.com>* @author Ryan Weaver <weaverryan@gmail.com>* @author Kévin Dunglas <dunglas@gmail.com>*/final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface{private Generator $generator;private EntityClassGenerator $entityClassGenerator;private PhpCompatUtil $phpCompatUtil;public function __construct(private FileManager $fileManager,private DoctrineHelper $doctrineHelper,string $projectDirectory = null,Generator $generator = null,EntityClassGenerator $entityClassGenerator = null,PhpCompatUtil $phpCompatUtil = null,) {if (null !== $projectDirectory) {@trigger_error('The $projectDirectory constructor argument is no longer used since 1.41.0', \E_USER_DEPRECATED);}if (null === $generator) {@trigger_error(sprintf('Passing a "%s" instance as 4th argument is mandatory since version 1.5.', Generator::class), \E_USER_DEPRECATED);$this->generator = new Generator($fileManager, 'App\\');} else {$this->generator = $generator;}if (null === $entityClassGenerator) {@trigger_error(sprintf('Passing a "%s" instance as 5th argument is mandatory since version 1.15.1', EntityClassGenerator::class), \E_USER_DEPRECATED);$this->entityClassGenerator = new EntityClassGenerator($generator, $this->doctrineHelper);} else {$this->entityClassGenerator = $entityClassGenerator;}if (null === $phpCompatUtil) {@trigger_error(sprintf('Passing a "%s" instance as 6th argument is mandatory since version 1.41.0', PhpCompatUtil::class), \E_USER_DEPRECATED);$this->phpCompatUtil = new PhpCompatUtil($this->fileManager);} else {$this->phpCompatUtil = $phpCompatUtil;}}public static function getCommandName(): string{return 'make:entity';}public static function getCommandDescription(): string{return 'Creates or updates a Doctrine entity class, and optionally an API Platform resource';}public function configureCommand(Command $command, InputConfiguration $inputConfig): void{$command->addArgument('name', InputArgument::OPTIONAL, sprintf('Class name of the entity to create or update (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm())))->addOption('api-resource', 'a', InputOption::VALUE_NONE, 'Mark this class as an API Platform resource (expose a CRUD API for it)')->addOption('broadcast', 'b', InputOption::VALUE_NONE, 'Add the ability to broadcast entity updates using Symfony UX Turbo?')->addOption('regenerate', null, InputOption::VALUE_NONE, 'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields')->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite any existing getter/setter methods')->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt'));$inputConfig->setArgumentAsNonInteractive('name');}public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void{if ($input->getArgument('name')) {return;}if ($input->getOption('regenerate')) {$io->block(['This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.','To overwrite any existing methods, re-run this command with the --overwrite flag',], null, 'fg=yellow');$classOrNamespace = $io->ask('Enter a class or namespace to regenerate', $this->getEntityNamespace(), [Validator::class, 'notBlank']);$input->setArgument('name', $classOrNamespace);return;}$argument = $command->getDefinition()->getArgument('name');$question = $this->createEntityClassQuestion($argument->getDescription());$entityClassName = $io->askQuestion($question);$input->setArgument('name', $entityClassName);if (!$input->getOption('api-resource') &&(class_exists(ApiResource::class) || class_exists(LegacyApiResource::class)) &&!class_exists($this->generator->createClassNameDetails($entityClassName, 'Entity\\')->getFullName())) {$description = $command->getDefinition()->getOption('api-resource')->getDescription();$question = new ConfirmationQuestion($description, false);$isApiResource = $io->askQuestion($question);$input->setOption('api-resource', $isApiResource);}if (!$input->getOption('broadcast') &&class_exists(Broadcast::class) &&!class_exists($this->generator->createClassNameDetails($entityClassName, 'Entity\\')->getFullName())) {$description = $command->getDefinition()->getOption('broadcast')->getDescription();$question = new ConfirmationQuestion($description, false);$isBroadcast = $io->askQuestion($question);$input->setOption('broadcast', $isBroadcast);}}public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void{$overwrite = $input->getOption('overwrite');// the regenerate option has entirely custom behaviorif ($input->getOption('regenerate')) {$this->regenerateEntities($input->getArgument('name'), $overwrite, $generator);$this->writeSuccessMessage($io);return;}$entityClassDetails = $generator->createClassNameDetails($input->getArgument('name'),'Entity\\');$classExists = class_exists($entityClassDetails->getFullName());if (!$classExists) {$broadcast = $input->getOption('broadcast');$entityPath = $this->entityClassGenerator->generateEntityClass($entityClassDetails,$input->getOption('api-resource'),false,true,$broadcast);if ($broadcast) {$shortName = $entityClassDetails->getShortName();$generator->generateTemplate(sprintf('broadcast/%s.stream.html.twig', $shortName),'doctrine/broadcast_twig_template.tpl.php',['class_name' => Str::asSnakeCase($shortName),'class_name_plural' => Str::asSnakeCase(Str::singularCamelCaseToPluralCamelCase($shortName)),]);}$generator->writeChanges();}if (!$this->doesEntityUseAttributeMapping($entityClassDetails->getFullName())) {throw new RuntimeCommandException(sprintf('Only 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) {$entityPath = $this->getPathOfClass($entityClassDetails->getFullName());$io->text(['Your entity already exists! So let\'s add some new fields!',]);} else {$io->text(['','Entity generated! Now let\'s add some fields!','You can always add more fields later manually or by re-running this command.',]);}$currentFields = $this->getPropertyNames($entityClassDetails->getFullName());$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite);$isFirstField = true;while (true) {$newField = $this->askForNextField($io, $currentFields, $entityClassDetails->getFullName(), $isFirstField);$isFirstField = false;if (null === $newField) {break;}$fileManagerOperations = [];$fileManagerOperations[$entityPath] = $manipulator;if (\is_array($newField)) {$annotationOptions = $newField;unset($annotationOptions['fieldName']);$manipulator->addEntityField($newField['fieldName'], $annotationOptions);$currentFields[] = $newField['fieldName'];} elseif ($newField instanceof EntityRelation) {// both overridden below for OneToMany$newFieldName = $newField->getOwningProperty();if ($newField->isSelfReferencing()) {$otherManipulatorFilename = $entityPath;$otherManipulator = $manipulator;} else {$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);}switch ($newField->getType()) {case EntityRelation::MANY_TO_ONE:if ($newField->getOwningClass() === $entityClassDetails->getFullName()) {// THIS class will receive the ManyToOne$manipulator->addManyToOneRelation($newField->getOwningRelation());if ($newField->getMapInverseRelation()) {$otherManipulator->addOneToManyRelation($newField->getInverseRelation());}} else {// 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);// The *other* class will receive the ManyToOne$otherManipulator->addManyToOneRelation($newField->getOwningRelation());if (!$newField->getMapInverseRelation()) {throw new \Exception('Somehow a OneToMany relationship is being created, but the inverse side will not be mapped?');}$manipulator->addOneToManyRelation($newField->getInverseRelation());}break;case EntityRelation::MANY_TO_MANY:$manipulator->addManyToManyRelation($newField->getOwningRelation());if ($newField->getMapInverseRelation()) {$otherManipulator->addManyToManyRelation($newField->getInverseRelation());}break;case EntityRelation::ONE_TO_ONE:$manipulator->addOneToOneRelation($newField->getOwningRelation());if ($newField->getMapInverseRelation()) {$otherManipulator->addOneToOneRelation($newField->getInverseRelation());}break;default:throw new \Exception('Invalid relation type');}// save the inverse side if it's being mappedif ($newField->getMapInverseRelation()) {$fileManagerOperations[$otherManipulatorFilename] = $otherManipulator;}$currentFields[] = $newFieldName;} else {throw new \Exception('Invalid value');}foreach ($fileManagerOperations as $path => $manipulatorOrMessage) {if (\is_string($manipulatorOrMessage)) {$io->comment($manipulatorOrMessage);} else {$this->fileManager->dumpFile($path, $manipulatorOrMessage->getSourceCode());}}}$this->writeSuccessMessage($io);$io->text(['Next: When you\'re ready, create a migration with <info>php bin/console make:migration</info>','',]);}public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void{if (null !== $input && $input->getOption('api-resource')) {if (class_exists(ApiResource::class)) {$dependencies->addClassDependency(ApiResource::class,'api');} else {$dependencies->addClassDependency(LegacyApiResource::class,'api');}}if (null !== $input && $input->getOption('broadcast')) {$dependencies->addClassDependency(Broadcast::class,'ux-turbo-mercure');}ORMDependencyBuilder::buildDependencies($dependencies);}private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField): EntityRelation|array|null{$io->writeln('');if ($isFirstField) {$questionText = 'New property name (press <return> to stop adding fields)';} else {$questionText = 'Add another property? Enter the property name (or press <return> to stop adding fields)';}$fieldName = $io->ask($questionText, null, function ($name) use ($fields) {// allow it to be emptyif (!$name) {return $name;}if (\in_array($name, $fields)) {throw new \InvalidArgumentException(sprintf('The "%s" property already exists.', $name));}return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());});if (!$fieldName) {return null;}$defaultType = 'string';// try to guess the type by the field name prefix/suffix// convert to snake case for simplicity$snakeCasedField = Str::asSnakeCase($fieldName);if ('_at' === $suffix = substr($snakeCasedField, -3)) {$defaultType = 'datetime_immutable';} elseif ('_id' === $suffix) {$defaultType = 'integer';} elseif (str_starts_with($snakeCasedField, 'is_')) {$defaultType = 'boolean';} elseif (str_starts_with($snakeCasedField, 'has_')) {$defaultType = 'boolean';} elseif ('uuid' === $snakeCasedField) {$defaultType = Type::hasType('uuid') ? 'uuid' : 'guid';} elseif ('guid' === $snakeCasedField) {$defaultType = 'guid';}$type = null;$types = $this->getTypesMap();$allValidTypes = array_merge(array_keys($types),EntityRelation::getValidRelationTypes(),['relation']);while (null === $type) {$question = new Question('Field type (enter <comment>?</comment> to see all types)', $defaultType);$question->setAutocompleterValues($allValidTypes);$type = $io->askQuestion($question);if ('?' === $type) {$this->printAvailableTypes($io);$io->writeln('');$type = null;} elseif (!\in_array($type, $allValidTypes)) {$this->printAvailableTypes($io);$io->error(sprintf('Invalid type "%s".', $type));$io->writeln('');$type = null;}}if ('relation' === $type || \in_array($type, EntityRelation::getValidRelationTypes())) {return $this->askRelationDetails($io, $entityClass, $type, $fieldName);}// this is a normal field$data = ['fieldName' => $fieldName, 'type' => $type];if ('string' === $type) {// default to 255, avoid the question$data['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']);// 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']);}if ($io->confirm('Can this field be null in the database (nullable)', false)) {$data['nullable'] = true;}return $data;}private function printAvailableTypes(ConsoleStyle $io): void{$allTypes = $this->getTypesMap();if ('Hyper' === getenv('TERM_PROGRAM')) {$wizard = 'wizard 🧙';} else {$wizard = '\\' === \DIRECTORY_SEPARATOR ? 'wizard' : 'wizard 🧙';}$typesTable = ['main' => ['string' => [],'text' => [],'boolean' => [],'integer' => ['smallint', 'bigint'],'float' => [],],'relation' => ['relation' => 'a '.$wizard.' will help you build the relation',EntityRelation::MANY_TO_ONE => [],EntityRelation::ONE_TO_MANY => [],EntityRelation::MANY_TO_MANY => [],EntityRelation::ONE_TO_ONE => [],],'array_object' => ['array' => ['simple_array'],'json' => [],'object' => [],'binary' => [],'blob' => [],],'date_time' => ['datetime' => ['datetime_immutable'],'datetimetz' => ['datetimetz_immutable'],'date' => ['date_immutable'],'time' => ['time_immutable'],'dateinterval' => [],],];$printSection = static function (array $sectionTypes) use ($io, &$allTypes) {foreach ($sectionTypes as $mainType => $subTypes) {unset($allTypes[$mainType]);$line = sprintf(' * <comment>%s</comment>', $mainType);if (\is_string($subTypes) && $subTypes) {$line .= sprintf(' (%s)', $subTypes);} elseif (\is_array($subTypes) && !empty($subTypes)) {$line .= sprintf(' (or %s)', implode(', ', array_map(static fn ($subType) => sprintf('<comment>%s</comment>', $subType), $subTypes)));foreach ($subTypes as $subType) {unset($allTypes[$subType]);}}$io->writeln($line);}$io->writeln('');};$io->writeln('<info>Main Types</info>');$printSection($typesTable['main']);$io->writeln('<info>Relationships/Associations</info>');$printSection($typesTable['relation']);$io->writeln('<info>Array/Object Types</info>');$printSection($typesTable['array_object']);$io->writeln('<info>Date/Time Types</info>');$printSection($typesTable['date_time']);$io->writeln('<info>Other Types</info>');// empty the values$allTypes = array_map(static fn () => [], $allTypes);$printSection($allTypes);}private function createEntityClassQuestion(string $questionText): Question{$question = new Question($questionText);$question->setValidator([Validator::class, 'notBlank']);$question->setAutocompleterValues($this->doctrineHelper->getEntitiesForAutocomplete());return $question;}private function askRelationDetails(ConsoleStyle $io, string $generatedEntityClass, string $type, string $newFieldName): EntityRelation{// ask the targetEntity$targetEntityClass = null;while (null === $targetEntityClass) {$question = $this->createEntityClassQuestion('What class should this entity be related to?');$answeredEntityClass = $io->askQuestion($question);// find the correct class name - but give priority over looking// in the Entity namespace versus just checking the full class// name to avoid issues with classes like "Directory" that exist// in PHP's core.if (class_exists($this->getEntityNamespace().'\\'.$answeredEntityClass)) {$targetEntityClass = $this->getEntityNamespace().'\\'.$answeredEntityClass;} elseif (class_exists($answeredEntityClass)) {$targetEntityClass = $answeredEntityClass;} else {$io->error(sprintf('Unknown class "%s"', $answeredEntityClass));continue;}}// help the user select the typeif ('relation' === $type) {$type = $this->askRelationType($io, $generatedEntityClass, $targetEntityClass);}$askFieldName = fn (string $targetClass, string $defaultValue) => $io->ask(sprintf('New field name inside %s', Str::getShortClassName($targetClass)),$defaultValue,function ($name) use ($targetClass) {// it's still *possible* to create duplicate properties - by// trying to generate the same property 2 times during the// same make:entity run. property_exists() only knows about// properties that *originally* existed on this class.if (property_exists($targetClass, $name)) {throw new \InvalidArgumentException(sprintf('The "%s" class already has a "%s" property.', $targetClass, $name));}return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());});$askIsNullable = static fn (string $propertyName, string $targetClass) => $io->confirm(sprintf('Is the <comment>%s</comment>.<comment>%s</comment> property allowed to be null (nullable)?',Str::getShortClassName($targetClass),$propertyName));$askOrphanRemoval = static function (string $owningClass, string $inverseClass) use ($io) {$io->text(['Do you want to activate <comment>orphanRemoval</comment> on your relationship?',sprintf('A <comment>%s</comment> is "orphaned" when it is removed from its related <comment>%s</comment>.',Str::getShortClassName($owningClass),Str::getShortClassName($inverseClass)),sprintf('e.g. <comment>$%s->remove%s($%s)</comment>',Str::asLowerCamelCase(Str::getShortClassName($inverseClass)),Str::asCamelCase(Str::getShortClassName($owningClass)),Str::asLowerCamelCase(Str::getShortClassName($owningClass))),'',sprintf('NOTE: If a <comment>%s</comment> may *change* from one <comment>%s</comment> to another, answer "no".',Str::getShortClassName($owningClass),Str::getShortClassName($inverseClass)),]);return $io->confirm(sprintf('Do you want to automatically delete orphaned <comment>%s</comment> objects (orphanRemoval)?', $owningClass), false);};$askInverseSide = function (EntityRelation $relation) use ($io) {if ($this->isClassInVendor($relation->getInverseClass())) {$relation->setMapInverseRelation(false);return;}// recommend an inverse side, except for OneToOne, where it's inefficient$recommendMappingInverse = EntityRelation::ONE_TO_ONE !== $relation->getType();$getterMethodName = 'get'.Str::asCamelCase(Str::getShortClassName($relation->getOwningClass()));if (EntityRelation::ONE_TO_ONE !== $relation->getType()) {// pluralize!$getterMethodName = Str::singularCamelCaseToPluralCamelCase($getterMethodName);}$mapInverse = $io->confirm(sprintf('Do you want to add a new property to <comment>%s</comment> so that you can access/update <comment>%s</comment> objects from it - e.g. <comment>$%s->%s()</comment>?',Str::getShortClassName($relation->getInverseClass()),Str::getShortClassName($relation->getOwningClass()),Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())),$getterMethodName),$recommendMappingInverse);$relation->setMapInverseRelation($mapInverse);};switch ($type) {case EntityRelation::MANY_TO_ONE:$relation = new EntityRelation(EntityRelation::MANY_TO_ONE,$generatedEntityClass,$targetEntityClass);$relation->setOwningProperty($newFieldName);$relation->setIsNullable($askIsNullable($relation->getOwningProperty(),$relation->getOwningClass()));$askInverseSide($relation);if ($relation->getMapInverseRelation()) {$io->comment(sprintf('A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',Str::getShortClassName($relation->getInverseClass()),Str::getShortClassName($relation->getOwningClass())));$relation->setInverseProperty($askFieldName($relation->getInverseClass(),Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))));// orphan removal only applies if the inverse relation is setif (!$relation->isNullable()) {$relation->setOrphanRemoval($askOrphanRemoval($relation->getOwningClass(),$relation->getInverseClass()));}}break;case EntityRelation::ONE_TO_MANY:// we *actually* create a ManyToOne, but populate it differently$relation = new EntityRelation(EntityRelation::MANY_TO_ONE,$targetEntityClass,$generatedEntityClass);$relation->setInverseProperty($newFieldName);$io->comment(sprintf('A new property will also be added to the <comment>%s</comment> class so that you can access and set the related <comment>%s</comment> object from it.',Str::getShortClassName($relation->getOwningClass()),Str::getShortClassName($relation->getInverseClass())));$relation->setOwningProperty($askFieldName($relation->getOwningClass(),Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass()))));$relation->setIsNullable($askIsNullable($relation->getOwningProperty(),$relation->getOwningClass()));if (!$relation->isNullable()) {$relation->setOrphanRemoval($askOrphanRemoval($relation->getOwningClass(),$relation->getInverseClass()));}break;case EntityRelation::MANY_TO_MANY:$relation = new EntityRelation(EntityRelation::MANY_TO_MANY,$generatedEntityClass,$targetEntityClass);$relation->setOwningProperty($newFieldName);$askInverseSide($relation);if ($relation->getMapInverseRelation()) {$io->comment(sprintf('A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',Str::getShortClassName($relation->getInverseClass()),Str::getShortClassName($relation->getOwningClass())));$relation->setInverseProperty($askFieldName($relation->getInverseClass(),Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))));}break;case EntityRelation::ONE_TO_ONE:$relation = new EntityRelation(EntityRelation::ONE_TO_ONE,$generatedEntityClass,$targetEntityClass);$relation->setOwningProperty($newFieldName);$relation->setIsNullable($askIsNullable($relation->getOwningProperty(),$relation->getOwningClass()));$askInverseSide($relation);if ($relation->getMapInverseRelation()) {$io->comment(sprintf('A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> object from it.',Str::getShortClassName($relation->getInverseClass()),Str::getShortClassName($relation->getOwningClass())));$relation->setInverseProperty($askFieldName($relation->getInverseClass(),Str::asLowerCamelCase(Str::getShortClassName($relation->getOwningClass()))));}break;default:throw new \InvalidArgumentException('Invalid type: '.$type);}return $relation;}private function askRelationType(ConsoleStyle $io, string $entityClass, string $targetEntityClass){$io->writeln('What type of relationship is this?');$originalEntityShort = Str::getShortClassName($entityClass);$targetEntityShort = Str::getShortClassName($targetEntityClass);$rows = [];$rows[] = [EntityRelation::MANY_TO_ONE,sprintf("Each <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),];$rows[] = ['', ''];$rows[] = [EntityRelation::ONE_TO_MANY,sprintf("Each <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),];$rows[] = ['', ''];$rows[] = [EntityRelation::MANY_TO_MANY,sprintf("Each <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> can also relate to (can also have) <info>many</info> <comment>%s</comment> objects.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),];$rows[] = ['', ''];$rows[] = [EntityRelation::ONE_TO_ONE,sprintf("Each <comment>%s</comment> relates to (has) exactly <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> also relates to (has) exactly <info>one</info> <comment>%s</comment>.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),];$io->table(['Type','Description',], $rows);$question = new Question(sprintf('Relation type? [%s]',implode(', ', EntityRelation::getValidRelationTypes())));$question->setAutocompleterValues(EntityRelation::getValidRelationTypes());$question->setValidator(function ($type) {if (!\in_array($type, EntityRelation::getValidRelationTypes())) {throw new \InvalidArgumentException(sprintf('Invalid type: use one of: %s', implode(', ', EntityRelation::getValidRelationTypes())));}return $type;});return $io->askQuestion($question);}private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator{$manipulator = new ClassSourceManipulator(sourceCode: $this->fileManager->getFileContents($path),overwrite: $overwrite,);$manipulator->setIo($io);return $manipulator;}private function getPathOfClass(string $class): string{return (new ClassDetails($class))->getPath();}private function isClassInVendor(string $class): bool{$path = $this->getPathOfClass($class);return $this->fileManager->isPathInVendor($path);}private function regenerateEntities(string $classOrNamespace, bool $overwrite, Generator $generator): void{$regenerator = new EntityRegenerator($this->doctrineHelper, $this->fileManager, $generator, $this->entityClassGenerator, $overwrite);$regenerator->regenerateEntities($classOrNamespace);}private function getPropertyNames(string $class): array{if (!class_exists($class)) {return [];}$reflClass = new \ReflectionClass($class);return array_map(static fn (\ReflectionProperty $prop) => $prop->getName(), $reflClass->getProperties());}/** @legacy Drop when Annotations are no longer supported */private function doesEntityUseAttributeMapping(string $className): bool{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 mappedif (empty($otherClassMetadatas)) {return false;}$className = reset($otherClassMetadatas)->getName();}return $this->doctrineHelper->doesClassUsesAttributes($className);}private function getEntityNamespace(): string{return $this->doctrineHelper->getEntityNamespace();}private function getTypesMap(): array{$types = Type::getTypesMap();// remove deprecated json_array if it existsif (\defined(sprintf('%s::JSON_ARRAY', Type::class))) {unset($types[Type::JSON_ARRAY]);}return $types;}}