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