vendor/symfony/maker-bundle/src/Maker/MakeEntity.php line 318

  1. <?php
  2. /*
  3.  * This file is part of the Symfony MakerBundle package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bundle\MakerBundle\Maker;
  11. use ApiPlatform\Core\Annotation\ApiResource as LegacyApiResource;
  12. use ApiPlatform\Metadata\ApiResource;
  13. use Doctrine\DBAL\Types\Type;
  14. use Symfony\Bundle\MakerBundle\ConsoleStyle;
  15. use Symfony\Bundle\MakerBundle\DependencyBuilder;
  16. use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
  17. use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
  18. use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator;
  19. use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation;
  20. use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
  21. use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
  22. use Symfony\Bundle\MakerBundle\FileManager;
  23. use Symfony\Bundle\MakerBundle\Generator;
  24. use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
  25. use Symfony\Bundle\MakerBundle\InputConfiguration;
  26. use Symfony\Bundle\MakerBundle\Str;
  27. use Symfony\Bundle\MakerBundle\Util\ClassDetails;
  28. use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
  29. use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil;
  30. use Symfony\Bundle\MakerBundle\Validator;
  31. use Symfony\Component\Console\Command\Command;
  32. use Symfony\Component\Console\Input\InputArgument;
  33. use Symfony\Component\Console\Input\InputInterface;
  34. use Symfony\Component\Console\Input\InputOption;
  35. use Symfony\Component\Console\Question\ConfirmationQuestion;
  36. use Symfony\Component\Console\Question\Question;
  37. use Symfony\UX\Turbo\Attribute\Broadcast;
  38. /**
  39.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  40.  * @author Ryan Weaver <weaverryan@gmail.com>
  41.  * @author Kévin Dunglas <dunglas@gmail.com>
  42.  */
  43. final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface
  44. {
  45.     private Generator $generator;
  46.     private EntityClassGenerator $entityClassGenerator;
  47.     private PhpCompatUtil $phpCompatUtil;
  48.     public function __construct(
  49.         private FileManager $fileManager,
  50.         private DoctrineHelper $doctrineHelper,
  51.         string $projectDirectory null,
  52.         Generator $generator null,
  53.         EntityClassGenerator $entityClassGenerator null,
  54.         PhpCompatUtil $phpCompatUtil null,
  55.     ) {
  56.         if (null !== $projectDirectory) {
  57.             @trigger_error('The $projectDirectory constructor argument is no longer used since 1.41.0'\E_USER_DEPRECATED);
  58.         }
  59.         if (null === $generator) {
  60.             @trigger_error(sprintf('Passing a "%s" instance as 4th argument is mandatory since version 1.5.'Generator::class), \E_USER_DEPRECATED);
  61.             $this->generator = new Generator($fileManager'App\\');
  62.         } else {
  63.             $this->generator $generator;
  64.         }
  65.         if (null === $entityClassGenerator) {
  66.             @trigger_error(sprintf('Passing a "%s" instance as 5th argument is mandatory since version 1.15.1'EntityClassGenerator::class), \E_USER_DEPRECATED);
  67.             $this->entityClassGenerator = new EntityClassGenerator($generator$this->doctrineHelper);
  68.         } else {
  69.             $this->entityClassGenerator $entityClassGenerator;
  70.         }
  71.         if (null === $phpCompatUtil) {
  72.             @trigger_error(sprintf('Passing a "%s" instance as 6th argument is mandatory since version 1.41.0'PhpCompatUtil::class), \E_USER_DEPRECATED);
  73.             $this->phpCompatUtil = new PhpCompatUtil($this->fileManager);
  74.         } else {
  75.             $this->phpCompatUtil $phpCompatUtil;
  76.         }
  77.     }
  78.     public static function getCommandName(): string
  79.     {
  80.         return 'make:entity';
  81.     }
  82.     public static function getCommandDescription(): string
  83.     {
  84.         return 'Creates or updates a Doctrine entity class, and optionally an API Platform resource';
  85.     }
  86.     public function configureCommand(Command $commandInputConfiguration $inputConfig): void
  87.     {
  88.         $command
  89.             ->addArgument('name'InputArgument::OPTIONALsprintf('Class name of the entity to create or update (e.g. <fg=yellow>%s</>)'Str::asClassName(Str::getRandomTerm())))
  90.             ->addOption('api-resource''a'InputOption::VALUE_NONE'Mark this class as an API Platform resource (expose a CRUD API for it)')
  91.             ->addOption('broadcast''b'InputOption::VALUE_NONE'Add the ability to broadcast entity updates using Symfony UX Turbo?')
  92.             ->addOption('regenerate'nullInputOption::VALUE_NONE'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields')
  93.             ->addOption('overwrite'nullInputOption::VALUE_NONE'Overwrite any existing getter/setter methods')
  94.             ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt'))
  95.         ;
  96.         $inputConfig->setArgumentAsNonInteractive('name');
  97.     }
  98.     public function interact(InputInterface $inputConsoleStyle $ioCommand $command): void
  99.     {
  100.         if ($input->getArgument('name')) {
  101.             return;
  102.         }
  103.         if ($input->getOption('regenerate')) {
  104.             $io->block([
  105.                 'This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.',
  106.                 'To overwrite any existing methods, re-run this command with the --overwrite flag',
  107.             ], null'fg=yellow');
  108.             $classOrNamespace $io->ask('Enter a class or namespace to regenerate'$this->getEntityNamespace(), [Validator::class, 'notBlank']);
  109.             $input->setArgument('name'$classOrNamespace);
  110.             return;
  111.         }
  112.         $argument $command->getDefinition()->getArgument('name');
  113.         $question $this->createEntityClassQuestion($argument->getDescription());
  114.         $entityClassName $io->askQuestion($question);
  115.         $input->setArgument('name'$entityClassName);
  116.         if (
  117.             !$input->getOption('api-resource') &&
  118.             (class_exists(ApiResource::class) || class_exists(LegacyApiResource::class)) &&
  119.             !class_exists($this->generator->createClassNameDetails($entityClassName'Entity\\')->getFullName())
  120.         ) {
  121.             $description $command->getDefinition()->getOption('api-resource')->getDescription();
  122.             $question = new ConfirmationQuestion($descriptionfalse);
  123.             $isApiResource $io->askQuestion($question);
  124.             $input->setOption('api-resource'$isApiResource);
  125.         }
  126.         if (
  127.             !$input->getOption('broadcast') &&
  128.             class_exists(Broadcast::class) &&
  129.             !class_exists($this->generator->createClassNameDetails($entityClassName'Entity\\')->getFullName())
  130.         ) {
  131.             $description $command->getDefinition()->getOption('broadcast')->getDescription();
  132.             $question = new ConfirmationQuestion($descriptionfalse);
  133.             $isBroadcast $io->askQuestion($question);
  134.             $input->setOption('broadcast'$isBroadcast);
  135.         }
  136.     }
  137.     public function generate(InputInterface $inputConsoleStyle $ioGenerator $generator): void
  138.     {
  139.         $overwrite $input->getOption('overwrite');
  140.         // the regenerate option has entirely custom behavior
  141.         if ($input->getOption('regenerate')) {
  142.             $this->regenerateEntities($input->getArgument('name'), $overwrite$generator);
  143.             $this->writeSuccessMessage($io);
  144.             return;
  145.         }
  146.         $entityClassDetails $generator->createClassNameDetails(
  147.             $input->getArgument('name'),
  148.             'Entity\\'
  149.         );
  150.         $classExists class_exists($entityClassDetails->getFullName());
  151.         if (!$classExists) {
  152.             $broadcast $input->getOption('broadcast');
  153.             $entityPath $this->entityClassGenerator->generateEntityClass(
  154.                 $entityClassDetails,
  155.                 $input->getOption('api-resource'),
  156.                 false,
  157.                 true,
  158.                 $broadcast
  159.             );
  160.             if ($broadcast) {
  161.                 $shortName $entityClassDetails->getShortName();
  162.                 $generator->generateTemplate(
  163.                     sprintf('broadcast/%s.stream.html.twig'$shortName),
  164.                     'doctrine/broadcast_twig_template.tpl.php',
  165.                     [
  166.                         'class_name' => Str::asSnakeCase($shortName),
  167.                         'class_name_plural' => Str::asSnakeCase(Str::singularCamelCaseToPluralCamelCase($shortName)),
  168.                     ]
  169.                 );
  170.             }
  171.             $generator->writeChanges();
  172.         }
  173.         if (!$this->doesEntityUseAttributeMapping($entityClassDetails->getFullName())) {
  174.             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()));
  175.         }
  176.         if ($classExists) {
  177.             $entityPath $this->getPathOfClass($entityClassDetails->getFullName());
  178.             $io->text([
  179.                 'Your entity already exists! So let\'s add some new fields!',
  180.             ]);
  181.         } else {
  182.             $io->text([
  183.                 '',
  184.                 'Entity generated! Now let\'s add some fields!',
  185.                 'You can always add more fields later manually or by re-running this command.',
  186.             ]);
  187.         }
  188.         $currentFields $this->getPropertyNames($entityClassDetails->getFullName());
  189.         $manipulator $this->createClassManipulator($entityPath$io$overwrite);
  190.         $isFirstField true;
  191.         while (true) {
  192.             $newField $this->askForNextField($io$currentFields$entityClassDetails->getFullName(), $isFirstField);
  193.             $isFirstField false;
  194.             if (null === $newField) {
  195.                 break;
  196.             }
  197.             $fileManagerOperations = [];
  198.             $fileManagerOperations[$entityPath] = $manipulator;
  199.             if (\is_array($newField)) {
  200.                 $annotationOptions $newField;
  201.                 unset($annotationOptions['fieldName']);
  202.                 $manipulator->addEntityField($newField['fieldName'], $annotationOptions);
  203.                 $currentFields[] = $newField['fieldName'];
  204.             } elseif ($newField instanceof EntityRelation) {
  205.                 // both overridden below for OneToMany
  206.                 $newFieldName $newField->getOwningProperty();
  207.                 if ($newField->isSelfReferencing()) {
  208.                     $otherManipulatorFilename $entityPath;
  209.                     $otherManipulator $manipulator;
  210.                 } else {
  211.                     $otherManipulatorFilename $this->getPathOfClass($newField->getInverseClass());
  212.                     $otherManipulator $this->createClassManipulator($otherManipulatorFilename$io$overwrite);
  213.                 }
  214.                 switch ($newField->getType()) {
  215.                     case EntityRelation::MANY_TO_ONE:
  216.                         if ($newField->getOwningClass() === $entityClassDetails->getFullName()) {
  217.                             // THIS class will receive the ManyToOne
  218.                             $manipulator->addManyToOneRelation($newField->getOwningRelation());
  219.                             if ($newField->getMapInverseRelation()) {
  220.                                 $otherManipulator->addOneToManyRelation($newField->getInverseRelation());
  221.                             }
  222.                         } else {
  223.                             // the new field being added to THIS entity is the inverse
  224.                             $newFieldName $newField->getInverseProperty();
  225.                             $otherManipulatorFilename $this->getPathOfClass($newField->getOwningClass());
  226.                             $otherManipulator $this->createClassManipulator($otherManipulatorFilename$io$overwrite);
  227.                             // The *other* class will receive the ManyToOne
  228.                             $otherManipulator->addManyToOneRelation($newField->getOwningRelation());
  229.                             if (!$newField->getMapInverseRelation()) {
  230.                                 throw new \Exception('Somehow a OneToMany relationship is being created, but the inverse side will not be mapped?');
  231.                             }
  232.                             $manipulator->addOneToManyRelation($newField->getInverseRelation());
  233.                         }
  234.                         break;
  235.                     case EntityRelation::MANY_TO_MANY:
  236.                         $manipulator->addManyToManyRelation($newField->getOwningRelation());
  237.                         if ($newField->getMapInverseRelation()) {
  238.                             $otherManipulator->addManyToManyRelation($newField->getInverseRelation());
  239.                         }
  240.                         break;
  241.                     case EntityRelation::ONE_TO_ONE:
  242.                         $manipulator->addOneToOneRelation($newField->getOwningRelation());
  243.                         if ($newField->getMapInverseRelation()) {
  244.                             $otherManipulator->addOneToOneRelation($newField->getInverseRelation());
  245.                         }
  246.                         break;
  247.                     default:
  248.                         throw new \Exception('Invalid relation type');
  249.                 }
  250.                 // save the inverse side if it's being mapped
  251.                 if ($newField->getMapInverseRelation()) {
  252.                     $fileManagerOperations[$otherManipulatorFilename] = $otherManipulator;
  253.                 }
  254.                 $currentFields[] = $newFieldName;
  255.             } else {
  256.                 throw new \Exception('Invalid value');
  257.             }
  258.             foreach ($fileManagerOperations as $path => $manipulatorOrMessage) {
  259.                 if (\is_string($manipulatorOrMessage)) {
  260.                     $io->comment($manipulatorOrMessage);
  261.                 } else {
  262.                     $this->fileManager->dumpFile($path$manipulatorOrMessage->getSourceCode());
  263.                 }
  264.             }
  265.         }
  266.         $this->writeSuccessMessage($io);
  267.         $io->text([
  268.             'Next: When you\'re ready, create a migration with <info>php bin/console make:migration</info>',
  269.             '',
  270.         ]);
  271.     }
  272.     public function configureDependencies(DependencyBuilder $dependenciesInputInterface $input null): void
  273.     {
  274.         if (null !== $input && $input->getOption('api-resource')) {
  275.             if (class_exists(ApiResource::class)) {
  276.                 $dependencies->addClassDependency(
  277.                     ApiResource::class,
  278.                     'api'
  279.                 );
  280.             } else {
  281.                 $dependencies->addClassDependency(
  282.                     LegacyApiResource::class,
  283.                     'api'
  284.                 );
  285.             }
  286.         }
  287.         if (null !== $input && $input->getOption('broadcast')) {
  288.             $dependencies->addClassDependency(
  289.                 Broadcast::class,
  290.                 'ux-turbo-mercure'
  291.             );
  292.         }
  293.         ORMDependencyBuilder::buildDependencies($dependencies);
  294.     }
  295.     private function askForNextField(ConsoleStyle $io, array $fieldsstring $entityClassbool $isFirstField): EntityRelation|array|null
  296.     {
  297.         $io->writeln('');
  298.         if ($isFirstField) {
  299.             $questionText 'New property name (press <return> to stop adding fields)';
  300.         } else {
  301.             $questionText 'Add another property? Enter the property name (or press <return> to stop adding fields)';
  302.         }
  303.         $fieldName $io->ask($questionTextnull, function ($name) use ($fields) {
  304.             // allow it to be empty
  305.             if (!$name) {
  306.                 return $name;
  307.             }
  308.             if (\in_array($name$fields)) {
  309.                 throw new \InvalidArgumentException(sprintf('The "%s" property already exists.'$name));
  310.             }
  311.             return Validator::validateDoctrineFieldName($name$this->doctrineHelper->getRegistry());
  312.         });
  313.         if (!$fieldName) {
  314.             return null;
  315.         }
  316.         $defaultType 'string';
  317.         // try to guess the type by the field name prefix/suffix
  318.         // convert to snake case for simplicity
  319.         $snakeCasedField Str::asSnakeCase($fieldName);
  320.         if ('_at' === $suffix substr($snakeCasedField, -3)) {
  321.             $defaultType 'datetime_immutable';
  322.         } elseif ('_id' === $suffix) {
  323.             $defaultType 'integer';
  324.         } elseif (str_starts_with($snakeCasedField'is_')) {
  325.             $defaultType 'boolean';
  326.         } elseif (str_starts_with($snakeCasedField'has_')) {
  327.             $defaultType 'boolean';
  328.         } elseif ('uuid' === $snakeCasedField) {
  329.             $defaultType Type::hasType('uuid') ? 'uuid' 'guid';
  330.         } elseif ('guid' === $snakeCasedField) {
  331.             $defaultType 'guid';
  332.         }
  333.         $type null;
  334.         $types $this->getTypesMap();
  335.         $allValidTypes array_merge(
  336.             array_keys($types),
  337.             EntityRelation::getValidRelationTypes(),
  338.             ['relation']
  339.         );
  340.         while (null === $type) {
  341.             $question = new Question('Field type (enter <comment>?</comment> to see all types)'$defaultType);
  342.             $question->setAutocompleterValues($allValidTypes);
  343.             $type $io->askQuestion($question);
  344.             if ('?' === $type) {
  345.                 $this->printAvailableTypes($io);
  346.                 $io->writeln('');
  347.                 $type null;
  348.             } elseif (!\in_array($type$allValidTypes)) {
  349.                 $this->printAvailableTypes($io);
  350.                 $io->error(sprintf('Invalid type "%s".'$type));
  351.                 $io->writeln('');
  352.                 $type null;
  353.             }
  354.         }
  355.         if ('relation' === $type || \in_array($typeEntityRelation::getValidRelationTypes())) {
  356.             return $this->askRelationDetails($io$entityClass$type$fieldName);
  357.         }
  358.         // this is a normal field
  359.         $data = ['fieldName' => $fieldName'type' => $type];
  360.         if ('string' === $type) {
  361.             // default to 255, avoid the question
  362.             $data['length'] = $io->ask('Field length'255, [Validator::class, 'validateLength']);
  363.         } elseif ('decimal' === $type) {
  364.             // 10 is the default value given in \Doctrine\DBAL\Schema\Column::$_precision
  365.             $data['precision'] = $io->ask('Precision (total number of digits stored: 100.00 would be 5)'10, [Validator::class, 'validatePrecision']);
  366.             // 0 is the default value given in \Doctrine\DBAL\Schema\Column::$_scale
  367.             $data['scale'] = $io->ask('Scale (number of decimals to store: 100.00 would be 2)'0, [Validator::class, 'validateScale']);
  368.         }
  369.         if ($io->confirm('Can this field be null in the database (nullable)'false)) {
  370.             $data['nullable'] = true;
  371.         }
  372.         return $data;
  373.     }
  374.     private function printAvailableTypes(ConsoleStyle $io): void
  375.     {
  376.         $allTypes $this->getTypesMap();
  377.         if ('Hyper' === getenv('TERM_PROGRAM')) {
  378.             $wizard 'wizard ðŸ§™';
  379.         } else {
  380.             $wizard '\\' === \DIRECTORY_SEPARATOR 'wizard' 'wizard ðŸ§™';
  381.         }
  382.         $typesTable = [
  383.             'main' => [
  384.                 'string' => [],
  385.                 'text' => [],
  386.                 'boolean' => [],
  387.                 'integer' => ['smallint''bigint'],
  388.                 'float' => [],
  389.             ],
  390.             'relation' => [
  391.                 'relation' => 'a '.$wizard.' will help you build the relation',
  392.                 EntityRelation::MANY_TO_ONE => [],
  393.                 EntityRelation::ONE_TO_MANY => [],
  394.                 EntityRelation::MANY_TO_MANY => [],
  395.                 EntityRelation::ONE_TO_ONE => [],
  396.             ],
  397.             'array_object' => [
  398.                 'array' => ['simple_array'],
  399.                 'json' => [],
  400.                 'object' => [],
  401.                 'binary' => [],
  402.                 'blob' => [],
  403.             ],
  404.             'date_time' => [
  405.                 'datetime' => ['datetime_immutable'],
  406.                 'datetimetz' => ['datetimetz_immutable'],
  407.                 'date' => ['date_immutable'],
  408.                 'time' => ['time_immutable'],
  409.                 'dateinterval' => [],
  410.             ],
  411.         ];
  412.         $printSection = static function (array $sectionTypes) use ($io, &$allTypes) {
  413.             foreach ($sectionTypes as $mainType => $subTypes) {
  414.                 unset($allTypes[$mainType]);
  415.                 $line sprintf('  * <comment>%s</comment>'$mainType);
  416.                 if (\is_string($subTypes) && $subTypes) {
  417.                     $line .= sprintf(' (%s)'$subTypes);
  418.                 } elseif (\is_array($subTypes) && !empty($subTypes)) {
  419.                     $line .= sprintf(' (or %s)'implode(', 'array_map(
  420.                         static fn ($subType) => sprintf('<comment>%s</comment>'$subType), $subTypes))
  421.                     );
  422.                     foreach ($subTypes as $subType) {
  423.                         unset($allTypes[$subType]);
  424.                     }
  425.                 }
  426.                 $io->writeln($line);
  427.             }
  428.             $io->writeln('');
  429.         };
  430.         $io->writeln('<info>Main Types</info>');
  431.         $printSection($typesTable['main']);
  432.         $io->writeln('<info>Relationships/Associations</info>');
  433.         $printSection($typesTable['relation']);
  434.         $io->writeln('<info>Array/Object Types</info>');
  435.         $printSection($typesTable['array_object']);
  436.         $io->writeln('<info>Date/Time Types</info>');
  437.         $printSection($typesTable['date_time']);
  438.         $io->writeln('<info>Other Types</info>');
  439.         // empty the values
  440.         $allTypes array_map(static fn () => [], $allTypes);
  441.         $printSection($allTypes);
  442.     }
  443.     private function createEntityClassQuestion(string $questionText): Question
  444.     {
  445.         $question = new Question($questionText);
  446.         $question->setValidator([Validator::class, 'notBlank']);
  447.         $question->setAutocompleterValues($this->doctrineHelper->getEntitiesForAutocomplete());
  448.         return $question;
  449.     }
  450.     private function askRelationDetails(ConsoleStyle $iostring $generatedEntityClassstring $typestring $newFieldName): EntityRelation
  451.     {
  452.         // ask the targetEntity
  453.         $targetEntityClass null;
  454.         while (null === $targetEntityClass) {
  455.             $question $this->createEntityClassQuestion('What class should this entity be related to?');
  456.             $answeredEntityClass $io->askQuestion($question);
  457.             // find the correct class name - but give priority over looking
  458.             // in the Entity namespace versus just checking the full class
  459.             // name to avoid issues with classes like "Directory" that exist
  460.             // in PHP's core.
  461.             if (class_exists($this->getEntityNamespace().'\\'.$answeredEntityClass)) {
  462.                 $targetEntityClass $this->getEntityNamespace().'\\'.$answeredEntityClass;
  463.             } elseif (class_exists($answeredEntityClass)) {
  464.                 $targetEntityClass $answeredEntityClass;
  465.             } else {
  466.                 $io->error(sprintf('Unknown class "%s"'$answeredEntityClass));
  467.                 continue;
  468.             }
  469.         }
  470.         // help the user select the type
  471.         if ('relation' === $type) {
  472.             $type $this->askRelationType($io$generatedEntityClass$targetEntityClass);
  473.         }
  474.         $askFieldName = fn (string $targetClassstring $defaultValue) => $io->ask(
  475.             sprintf('New field name inside %s'Str::getShortClassName($targetClass)),
  476.             $defaultValue,
  477.             function ($name) use ($targetClass) {
  478.                 // it's still *possible* to create duplicate properties - by
  479.                 // trying to generate the same property 2 times during the
  480.                 // same make:entity run. property_exists() only knows about
  481.                 // properties that *originally* existed on this class.
  482.                 if (property_exists($targetClass$name)) {
  483.                     throw new \InvalidArgumentException(sprintf('The "%s" class already has a "%s" property.'$targetClass$name));
  484.                 }
  485.                 return Validator::validateDoctrineFieldName($name$this->doctrineHelper->getRegistry());
  486.             }
  487.         );
  488.         $askIsNullable = static fn (string $propertyNamestring $targetClass) => $io->confirm(sprintf(
  489.             'Is the <comment>%s</comment>.<comment>%s</comment> property allowed to be null (nullable)?',
  490.             Str::getShortClassName($targetClass),
  491.             $propertyName
  492.         ));
  493.         $askOrphanRemoval = static function (string $owningClassstring $inverseClass) use ($io) {
  494.             $io->text([
  495.                 'Do you want to activate <comment>orphanRemoval</comment> on your relationship?',
  496.                 sprintf(
  497.                     'A <comment>%s</comment> is "orphaned" when it is removed from its related <comment>%s</comment>.',
  498.                     Str::getShortClassName($owningClass),
  499.                     Str::getShortClassName($inverseClass)
  500.                 ),
  501.                 sprintf(
  502.                     'e.g. <comment>$%s->remove%s($%s)</comment>',
  503.                     Str::asLowerCamelCase(Str::getShortClassName($inverseClass)),
  504.                     Str::asCamelCase(Str::getShortClassName($owningClass)),
  505.                     Str::asLowerCamelCase(Str::getShortClassName($owningClass))
  506.                 ),
  507.                 '',
  508.                 sprintf(
  509.                     'NOTE: If a <comment>%s</comment> may *change* from one <comment>%s</comment> to another, answer "no".',
  510.                     Str::getShortClassName($owningClass),
  511.                     Str::getShortClassName($inverseClass)
  512.                 ),
  513.             ]);
  514.             return $io->confirm(sprintf('Do you want to automatically delete orphaned <comment>%s</comment> objects (orphanRemoval)?'$owningClass), false);
  515.         };
  516.         $askInverseSide = function (EntityRelation $relation) use ($io) {
  517.             if ($this->isClassInVendor($relation->getInverseClass())) {
  518.                 $relation->setMapInverseRelation(false);
  519.                 return;
  520.             }
  521.             // recommend an inverse side, except for OneToOne, where it's inefficient
  522.             $recommendMappingInverse EntityRelation::ONE_TO_ONE !== $relation->getType();
  523.             $getterMethodName 'get'.Str::asCamelCase(Str::getShortClassName($relation->getOwningClass()));
  524.             if (EntityRelation::ONE_TO_ONE !== $relation->getType()) {
  525.                 // pluralize!
  526.                 $getterMethodName Str::singularCamelCaseToPluralCamelCase($getterMethodName);
  527.             }
  528.             $mapInverse $io->confirm(
  529.                 sprintf(
  530.                     '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>?',
  531.                     Str::getShortClassName($relation->getInverseClass()),
  532.                     Str::getShortClassName($relation->getOwningClass()),
  533.                     Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())),
  534.                     $getterMethodName
  535.                 ),
  536.                 $recommendMappingInverse
  537.             );
  538.             $relation->setMapInverseRelation($mapInverse);
  539.         };
  540.         switch ($type) {
  541.             case EntityRelation::MANY_TO_ONE:
  542.                 $relation = new EntityRelation(
  543.                     EntityRelation::MANY_TO_ONE,
  544.                     $generatedEntityClass,
  545.                     $targetEntityClass
  546.                 );
  547.                 $relation->setOwningProperty($newFieldName);
  548.                 $relation->setIsNullable($askIsNullable(
  549.                     $relation->getOwningProperty(),
  550.                     $relation->getOwningClass()
  551.                 ));
  552.                 $askInverseSide($relation);
  553.                 if ($relation->getMapInverseRelation()) {
  554.                     $io->comment(sprintf(
  555.                         '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.',
  556.                         Str::getShortClassName($relation->getInverseClass()),
  557.                         Str::getShortClassName($relation->getOwningClass())
  558.                     ));
  559.                     $relation->setInverseProperty($askFieldName(
  560.                         $relation->getInverseClass(),
  561.                         Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
  562.                     ));
  563.                     // orphan removal only applies if the inverse relation is set
  564.                     if (!$relation->isNullable()) {
  565.                         $relation->setOrphanRemoval($askOrphanRemoval(
  566.                             $relation->getOwningClass(),
  567.                             $relation->getInverseClass()
  568.                         ));
  569.                     }
  570.                 }
  571.                 break;
  572.             case EntityRelation::ONE_TO_MANY:
  573.                 // we *actually* create a ManyToOne, but populate it differently
  574.                 $relation = new EntityRelation(
  575.                     EntityRelation::MANY_TO_ONE,
  576.                     $targetEntityClass,
  577.                     $generatedEntityClass
  578.                 );
  579.                 $relation->setInverseProperty($newFieldName);
  580.                 $io->comment(sprintf(
  581.                     '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.',
  582.                     Str::getShortClassName($relation->getOwningClass()),
  583.                     Str::getShortClassName($relation->getInverseClass())
  584.                 ));
  585.                 $relation->setOwningProperty($askFieldName(
  586.                     $relation->getOwningClass(),
  587.                     Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass()))
  588.                 ));
  589.                 $relation->setIsNullable($askIsNullable(
  590.                     $relation->getOwningProperty(),
  591.                     $relation->getOwningClass()
  592.                 ));
  593.                 if (!$relation->isNullable()) {
  594.                     $relation->setOrphanRemoval($askOrphanRemoval(
  595.                         $relation->getOwningClass(),
  596.                         $relation->getInverseClass()
  597.                     ));
  598.                 }
  599.                 break;
  600.             case EntityRelation::MANY_TO_MANY:
  601.                 $relation = new EntityRelation(
  602.                     EntityRelation::MANY_TO_MANY,
  603.                     $generatedEntityClass,
  604.                     $targetEntityClass
  605.                 );
  606.                 $relation->setOwningProperty($newFieldName);
  607.                 $askInverseSide($relation);
  608.                 if ($relation->getMapInverseRelation()) {
  609.                     $io->comment(sprintf(
  610.                         '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.',
  611.                         Str::getShortClassName($relation->getInverseClass()),
  612.                         Str::getShortClassName($relation->getOwningClass())
  613.                     ));
  614.                     $relation->setInverseProperty($askFieldName(
  615.                         $relation->getInverseClass(),
  616.                         Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
  617.                     ));
  618.                 }
  619.                 break;
  620.             case EntityRelation::ONE_TO_ONE:
  621.                 $relation = new EntityRelation(
  622.                     EntityRelation::ONE_TO_ONE,
  623.                     $generatedEntityClass,
  624.                     $targetEntityClass
  625.                 );
  626.                 $relation->setOwningProperty($newFieldName);
  627.                 $relation->setIsNullable($askIsNullable(
  628.                     $relation->getOwningProperty(),
  629.                     $relation->getOwningClass()
  630.                 ));
  631.                 $askInverseSide($relation);
  632.                 if ($relation->getMapInverseRelation()) {
  633.                     $io->comment(sprintf(
  634.                         '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.',
  635.                         Str::getShortClassName($relation->getInverseClass()),
  636.                         Str::getShortClassName($relation->getOwningClass())
  637.                     ));
  638.                     $relation->setInverseProperty($askFieldName(
  639.                         $relation->getInverseClass(),
  640.                         Str::asLowerCamelCase(Str::getShortClassName($relation->getOwningClass()))
  641.                     ));
  642.                 }
  643.                 break;
  644.             default:
  645.                 throw new \InvalidArgumentException('Invalid type: '.$type);
  646.         }
  647.         return $relation;
  648.     }
  649.     private function askRelationType(ConsoleStyle $iostring $entityClassstring $targetEntityClass)
  650.     {
  651.         $io->writeln('What type of relationship is this?');
  652.         $originalEntityShort Str::getShortClassName($entityClass);
  653.         $targetEntityShort Str::getShortClassName($targetEntityClass);
  654.         $rows = [];
  655.         $rows[] = [
  656.             EntityRelation::MANY_TO_ONE,
  657.             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),
  658.         ];
  659.         $rows[] = [''''];
  660.         $rows[] = [
  661.             EntityRelation::ONE_TO_MANY,
  662.             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),
  663.         ];
  664.         $rows[] = [''''];
  665.         $rows[] = [
  666.             EntityRelation::MANY_TO_MANY,
  667.             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),
  668.         ];
  669.         $rows[] = [''''];
  670.         $rows[] = [
  671.             EntityRelation::ONE_TO_ONE,
  672.             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),
  673.         ];
  674.         $io->table([
  675.             'Type',
  676.             'Description',
  677.         ], $rows);
  678.         $question = new Question(sprintf(
  679.             'Relation type? [%s]',
  680.             implode(', 'EntityRelation::getValidRelationTypes())
  681.         ));
  682.         $question->setAutocompleterValues(EntityRelation::getValidRelationTypes());
  683.         $question->setValidator(function ($type) {
  684.             if (!\in_array($typeEntityRelation::getValidRelationTypes())) {
  685.                 throw new \InvalidArgumentException(sprintf('Invalid type: use one of: %s'implode(', 'EntityRelation::getValidRelationTypes())));
  686.             }
  687.             return $type;
  688.         });
  689.         return $io->askQuestion($question);
  690.     }
  691.     private function createClassManipulator(string $pathConsoleStyle $iobool $overwrite): ClassSourceManipulator
  692.     {
  693.         $manipulator = new ClassSourceManipulator(
  694.             sourceCode$this->fileManager->getFileContents($path),
  695.             overwrite$overwrite,
  696.         );
  697.         $manipulator->setIo($io);
  698.         return $manipulator;
  699.     }
  700.     private function getPathOfClass(string $class): string
  701.     {
  702.         return (new ClassDetails($class))->getPath();
  703.     }
  704.     private function isClassInVendor(string $class): bool
  705.     {
  706.         $path $this->getPathOfClass($class);
  707.         return $this->fileManager->isPathInVendor($path);
  708.     }
  709.     private function regenerateEntities(string $classOrNamespacebool $overwriteGenerator $generator): void
  710.     {
  711.         $regenerator = new EntityRegenerator($this->doctrineHelper$this->fileManager$generator$this->entityClassGenerator$overwrite);
  712.         $regenerator->regenerateEntities($classOrNamespace);
  713.     }
  714.     private function getPropertyNames(string $class): array
  715.     {
  716.         if (!class_exists($class)) {
  717.             return [];
  718.         }
  719.         $reflClass = new \ReflectionClass($class);
  720.         return array_map(static fn (\ReflectionProperty $prop) => $prop->getName(), $reflClass->getProperties());
  721.     }
  722.     /** @legacy Drop when Annotations are no longer supported */
  723.     private function doesEntityUseAttributeMapping(string $className): bool
  724.     {
  725.         if (!class_exists($className)) {
  726.             $otherClassMetadatas $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\'true);
  727.             // if we have no metadata, we should assume this is the first class being mapped
  728.             if (empty($otherClassMetadatas)) {
  729.                 return false;
  730.             }
  731.             $className reset($otherClassMetadatas)->getName();
  732.         }
  733.         return $this->doctrineHelper->doesClassUsesAttributes($className);
  734.     }
  735.     private function getEntityNamespace(): string
  736.     {
  737.         return $this->doctrineHelper->getEntityNamespace();
  738.     }
  739.     private function getTypesMap(): array
  740.     {
  741.         $types Type::getTypesMap();
  742.         // remove deprecated json_array if it exists
  743.         if (\defined(sprintf('%s::JSON_ARRAY'Type::class))) {
  744.             unset($types[Type::JSON_ARRAY]);
  745.         }
  746.         return $types;
  747.     }
  748. }