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

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