setName('orm:mapping:describe')
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
->setDescription('Display information about mapped objects')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->setHelp(<<<'EOT'
The %command.full_name% command describes the metadata for the given full or partial entity class name.
%command.full_name% My\Namespace\Entity\MyEntity
Or:
%command.full_name% MyEntity
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$entityManager = $this->getEntityManager($input);
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
return 0;
}
/**
* Display all the mapping information for a single Entity.
*
* @param string $entityName Full or partial entity class name
*/
private function displayEntity(
string $entityName,
EntityManagerInterface $entityManager,
SymfonyStyle $ui,
): void {
$metadata = $this->getClassMetadata($entityName, $entityManager);
$ui->table(
['Field', 'Value'],
array_merge(
[
$this->formatField('Name', $metadata->name),
$this->formatField('Root entity name', $metadata->rootEntityName),
$this->formatField('Custom generator definition', $metadata->customGeneratorDefinition),
$this->formatField('Custom repository class', $metadata->customRepositoryClassName),
$this->formatField('Mapped super class?', $metadata->isMappedSuperclass),
$this->formatField('Embedded class?', $metadata->isEmbeddedClass),
$this->formatField('Parent classes', $metadata->parentClasses),
$this->formatField('Sub classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->subClasses),
$this->formatField('Identifier', $metadata->identifier),
$this->formatField('Inheritance type', $metadata->inheritanceType),
$this->formatField('Discriminator column', $metadata->discriminatorColumn),
$this->formatField('Discriminator value', $metadata->discriminatorValue),
$this->formatField('Discriminator map', $metadata->discriminatorMap),
$this->formatField('Generator type', $metadata->generatorType),
$this->formatField('Table', $metadata->table),
$this->formatField('Composite identifier?', $metadata->isIdentifierComposite),
$this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier),
$this->formatField('Enum identifier?', $metadata->containsEnumIdentifier),
$this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition),
$this->formatField('Change tracking policy', $metadata->changeTrackingPolicy),
$this->formatField('Versioned?', $metadata->isVersioned),
$this->formatField('Version field', $metadata->versionField),
$this->formatField('Read only?', $metadata->isReadOnly),
$this->formatEntityListeners($metadata->entityListeners),
],
[$this->formatField('Association mappings:', '')],
$this->formatMappings($metadata->associationMappings),
[$this->formatField('Field mappings:', '')],
$this->formatMappings($metadata->fieldMappings),
),
);
}
/**
* Return all mapped entity class names
*
* @return string[]
* @psalm-return class-string[]
*/
private function getMappedEntities(EntityManagerInterface $entityManager): array
{
$entityClassNames = $entityManager->getConfiguration()
->getMetadataDriverImpl()
->getAllClassNames();
if (! $entityClassNames) {
throw new InvalidArgumentException(
'You do not have any mapped Doctrine ORM entities according to the current configuration. ' .
'If you have entities or mapping files you should check your mapping configuration for errors.',
);
}
return $entityClassNames;
}
/**
* Return the class metadata for the given entity
* name
*
* @param string $entityName Full or partial entity name
*/
private function getClassMetadata(
string $entityName,
EntityManagerInterface $entityManager,
): ClassMetadata {
try {
return $entityManager->getClassMetadata($entityName);
} catch (MappingException) {
}
$matches = array_filter(
$this->getMappedEntities($entityManager),
static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity)
);
if (! $matches) {
throw new InvalidArgumentException(sprintf(
'Could not find any mapped Entity classes matching "%s"',
$entityName,
));
}
if (count($matches) > 1) {
throw new InvalidArgumentException(sprintf(
'Entity name "%s" is ambiguous, possible matches: "%s"',
$entityName,
implode(', ', $matches),
));
}
return $entityManager->getClassMetadata(current($matches));
}
/**
* Format the given value for console output
*/
private function formatValue(mixed $value): string
{
if ($value === '') {
return '';
}
if ($value === null) {
return 'Null';
}
if (is_bool($value)) {
return '' . ($value ? 'True' : 'False') . '';
}
if (empty($value)) {
return 'Empty';
}
if (is_array($value)) {
return json_encode(
$value,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
);
}
if (is_object($value)) {
return sprintf('<%s>', get_debug_type($value));
}
if (is_scalar($value)) {
return (string) $value;
}
throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
}
/**
* Add the given label and value to the two column table output
*
* @param string $label Label for the value
* @param mixed $value A Value to show
*
* @return string[]
* @psalm-return array{0: string, 1: string}
*/
private function formatField(string $label, mixed $value): array
{
if ($value === null) {
$value = 'None';
}
return [sprintf('%s', $label), $this->formatValue($value)];
}
/**
* Format the association mappings
*
* @psalm-param array $propertyMappings
*
* @return string[][]
* @psalm-return list
*/
private function formatMappings(array $propertyMappings): array
{
$output = [];
foreach ($propertyMappings as $propertyName => $mapping) {
$output[] = $this->formatField(sprintf(' %s', $propertyName), '');
foreach ((array) $mapping as $field => $value) {
$output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value));
}
}
return $output;
}
/**
* Format the entity listeners
*
* @psalm-param list