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

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