Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 96 additions & 4 deletions src/Tools/Console/Command/MappingDescribeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\Persistence\Mapping\MappingException;
use InvalidArgumentException;
use JsonException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
Expand Down Expand Up @@ -52,9 +53,17 @@ final class MappingDescribeCommand extends AbstractEntityManagerCommand
protected function configure(): void
{
$this->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')
->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')
->addOption(
'format',
null,
InputOption::VALUE_REQUIRED,
'Output format (text, json)',
MappingDescribeCommandFormat::TEXT->value,
array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()),
)
->setHelp(<<<'EOT'
The %command.full_name% command describes the metadata for the given full or partial entity class name.

Expand All @@ -63,16 +72,25 @@ protected function configure(): void
Or:

<info>%command.full_name%</info> MyEntity

To output the metadata in JSON format, use the <info>--format</info> option:
<info>%command.full_name% My\Namespace\Entity\MyEntity --format=json</info>

To use a specific entity manager (e.g., for multi-DB projects), use the <info>--em</info> option:
<info>%command.full_name% My\Namespace\Entity\MyEntity --em=my_custom_entity_manager</info>

EOT);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);

$format = MappingDescribeCommandFormat::from($input->getOption('format'));

$entityManager = $this->getEntityManager($input);

$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui, $format);

return 0;
}
Expand All @@ -89,6 +107,10 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti

$suggestions->suggestValues(array_values($entities));
}

if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()));
}
}

/**
Expand All @@ -100,9 +122,47 @@ private function displayEntity(
string $entityName,
EntityManagerInterface $entityManager,
SymfonyStyle $ui,
MappingDescribeCommandFormat $format,
): void {
$metadata = $this->getClassMetadata($entityName, $entityManager);

if ($format === MappingDescribeCommandFormat::JSON) {
$ui->text(json_encode(
[
'name' => $metadata->name,
'rootEntityName' => $metadata->rootEntityName,
'customGeneratorDefinition' => $this->formatValueAsJson($metadata->customGeneratorDefinition),
'customRepositoryClassName' => $metadata->customRepositoryClassName,
'isMappedSuperclass' => $metadata->isMappedSuperclass,
'isEmbeddedClass' => $metadata->isEmbeddedClass,
'parentClasses' => $metadata->parentClasses,
'subClasses' => $metadata->subClasses,
'embeddedClasses' => $metadata->embeddedClasses,
'identifier' => $metadata->identifier,
'inheritanceType' => $metadata->inheritanceType,
'discriminatorColumn' => $this->formatValueAsJson($metadata->discriminatorColumn),
'discriminatorValue' => $metadata->discriminatorValue,
'discriminatorMap' => $metadata->discriminatorMap,
'generatorType' => $metadata->generatorType,
'table' => $this->formatValueAsJson($metadata->table),
'isIdentifierComposite' => $metadata->isIdentifierComposite,
'containsForeignIdentifier' => $metadata->containsForeignIdentifier,
'containsEnumIdentifier' => $metadata->containsEnumIdentifier,
'sequenceGeneratorDefinition' => $this->formatValueAsJson($metadata->sequenceGeneratorDefinition),
'changeTrackingPolicy' => $metadata->changeTrackingPolicy,
'isVersioned' => $metadata->isVersioned,
'versionField' => $metadata->versionField,
'isReadOnly' => $metadata->isReadOnly,
'entityListeners' => $metadata->entityListeners,
'associationMappings' => $this->formatMappingsAsJson($metadata->associationMappings),
'fieldMappings' => $this->formatMappingsAsJson($metadata->fieldMappings),
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
));

return;
}

$ui->table(
['Field', 'Value'],
array_merge(
Expand Down Expand Up @@ -240,6 +300,22 @@ private function formatValue(mixed $value): string
throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
}

/** @throws JsonException */
private function formatValueAsJson(mixed $value): mixed
{
if (is_object($value)) {
$value = (array) $value;
}

if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->formatValueAsJson($v);
}
}

return $value;
}

/**
* Add the given label and value to the two column table output
*
Expand Down Expand Up @@ -281,6 +357,22 @@ private function formatMappings(array $propertyMappings): array
return $output;
}

/**
* @param array<string, FieldMapping|AssociationMapping> $propertyMappings
*
* @return array<string, mixed>
*/
private function formatMappingsAsJson(array $propertyMappings): array
{
$output = [];

foreach ($propertyMappings as $propertyName => $mapping) {
$output[$propertyName] = $this->formatValueAsJson((array) $mapping);
}

return $output;
}

/**
* Format the entity listeners
*
Expand Down
11 changes: 11 additions & 0 deletions src/Tools/Console/Command/MappingDescribeCommandFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Tools\Console\Command;

enum MappingDescribeCommandFormat: string
{
case TEXT = 'text';
case JSON = 'json';
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\Console\Tester\CommandCompletionTester;
use Symfony\Component\Console\Tester\CommandTester;

use function json_decode;

/**
* Tests for {@see \Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand}
*/
Expand Down Expand Up @@ -56,6 +58,25 @@ public function testShowSpecificFuzzySingle(): void
self::assertStringContainsString('Root entity name', $display);
}

public function testShowSpecificFuzzySingleJson(): void
{
$this->tester->execute([
'command' => $this->command->getName(),
'entityName' => 'AttractionInfo',
'--format' => 'json',
]);

$display = $this->tester->getDisplay();
$decodedJson = json_decode($display, true);

self::assertJson($display);
self::assertSame(AttractionInfo::class, $decodedJson['name']);
self::assertArrayHasKey('rootEntityName', $decodedJson);
self::assertArrayHasKey('fieldMappings', $decodedJson);
self::assertArrayHasKey('associationMappings', $decodedJson);
self::assertArrayHasKey('id', $decodedJson['fieldMappings']);
}

public function testShowSpecificFuzzyAmbiguous(): void
{
$this->expectException(InvalidArgumentException::class);
Expand Down Expand Up @@ -111,5 +132,10 @@ public static function provideCompletionSuggestions(): iterable
'Doctrine\\\\Tests\\\\Models\\\\Cache\\\\Bar',
],
];

yield 'format option value' => [
['--format='],
['text', 'json'],
];
}
}