Licitator 1.0
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

801 lines
34 KiB

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