diff --git a/composer.json b/composer.json index a216eb192..6a26d79a0 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "doctrine/collections": "~1.2", "doctrine/dbal": ">=2.5-dev,<2.6-dev", "doctrine/instantiator": "~1.0.1", - "symfony/console": "2.*" + "symfony/console": "~2.3" }, "require-dev": { "symfony/yaml": "~2.1", diff --git a/lib/Doctrine/ORM/Tools/Console/Command/MappingDescribeCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/MappingDescribeCommand.php new file mode 100644 index 000000000..47a00b996 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/MappingDescribeCommand.php @@ -0,0 +1,298 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Doctrine\Common\Persistence\Mapping\MappingException; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Show information about mapped entities. + * + * @link www.doctrine-project.org + * @since 2.4 + * @author Daniel Leech + */ +final class MappingDescribeCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:mapping:describe') + ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity') + ->setDescription('Display information about mapped objects') + ->setHelp(<<%command.full_name% My\Namespace\Entity\MyEntity + +Or: + + %command.full_name% MyEntity +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /* @var $entityManager \Doctrine\ORM\EntityManagerInterface */ + $entityManager = $this->getHelper('em')->getEntityManager(); + + $this->displayEntity($input->getArgument('entityName'), $entityManager, $output); + + return 0; + } + + /** + * Display all the mapping information for a single Entity. + * + * @param string $entityName Full or partial entity class name + * @param EntityManagerInterface $entityManager + * @param OutputInterface $output + */ + private function displayEntity($entityName, EntityManagerInterface $entityManager, OutputInterface $output) + { + $table = new TableHelper(); + + $table->setHeaders(array('Field', 'Value')); + + $metadata = $this->getClassMetadata($entityName, $entityManager); + + array_map( + array($table, 'addRow'), + array_merge( + array( + $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('Named queries', $metadata->namedQueries), + $this->formatField('Named native queries', $metadata->namedNativeQueries), + $this->formatField('SQL result set mappings', $metadata->sqlResultSetMappings), + $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('Sequence generator definition', $metadata->sequenceGeneratorDefinition), + $this->formatField('Table generator definition', $metadata->tableGeneratorDefinition), + $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), + ), + array($this->formatField('Association mappings:', '')), + $this->formatMappings($metadata->associationMappings), + array($this->formatField('Field mappings:', '')), + $this->formatMappings($metadata->fieldMappings) + ) + ); + + $table->render($output); + } + + /** + * Return all mapped entity class names + * + * @param EntityManagerInterface $entityManager + * + * @return string[] + */ + private function getMappedEntities(EntityManagerInterface $entityManager) + { + $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 + * @param EntityManagerInterface $entityManager + * + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + private function getClassMetadata($entityName, EntityManagerInterface $entityManager) + { + try { + return $entityManager->getClassMetadata($entityName); + } catch (MappingException $e) { + } + + $matches = array_filter( + $this->getMappedEntities($entityManager), + function ($mappedEntity) use ($entityName) { + return 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 ambigous, possible matches: "%s"', + $entityName, implode(', ', $matches) + )); + } + + return $entityManager->getClassMetadata(current($matches)); + } + + /** + * Format the given value for console output + * + * @param mixed $value + * + * @return string + */ + private function formatValue($value) + { + if ('' === $value) { + return ''; + } + + if (null === $value) { + return 'Null'; + } + + if (is_bool($value)) { + return '' . ($value ? 'True' : 'False') . ''; + } + + if (empty($value)) { + return 'Empty'; + } + + if (is_array($value)) { + if (defined('JSON_UNESCAPED_UNICODE') && defined('JSON_UNESCAPED_SLASHES')) { + return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + return json_encode($value); + } + + if (is_object($value)) { + return sprintf('<%s>', get_class($value)); + } + + if (is_scalar($value)) { + return $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 array + */ + private function formatField($label, $value) + { + if (null === $value) { + $value = 'None'; + } + + return array(sprintf('%s', $label), $this->formatValue($value)); + } + + /** + * Format the association mappings + * + * @param array + * + * @return array + */ + private function formatMappings(array $propertyMappings) + { + $output = array(); + + foreach ($propertyMappings as $propertyName => $mapping) { + $output[] = $this->formatField(sprintf(' %s', $propertyName), ''); + + foreach ($mapping as $field => $value) { + $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value)); + } + } + + return $output; + } + + /** + * Format the entity listeners + * + * @param array $entityListeners + * + * @return array + */ + private function formatEntityListeners(array $entityListeners) + { + return $this->formatField( + 'Entity listeners', + array_map( + function ($entityListener) { + return get_class($entityListener); + }, + $entityListeners + ) + ); + } +} diff --git a/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php b/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php index c69e2342b..55ec3791c 100644 --- a/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php +++ b/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php @@ -107,7 +107,8 @@ class ConsoleRunner new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), - new \Doctrine\ORM\Tools\Console\Command\InfoCommand() + new \Doctrine\ORM\Tools\Console\Command\InfoCommand(), + new \Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand(), )); } diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/InfoCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/InfoCommandTest.php new file mode 100644 index 000000000..5594299ac --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/InfoCommandTest.php @@ -0,0 +1,56 @@ +application = new Application(); + $command = new InfoCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($command); + + $this->command = $this->application->find('orm:info'); + $this->tester = new CommandTester($command); + } + + public function testListAllClasses() + { + $this->tester->execute(array( + 'command' => $this->command->getName(), + )); + + $this->assertContains('Doctrine\Tests\Models\Cache\AttractionInfo', $this->tester->getDisplay()); + $this->assertContains('Doctrine\Tests\Models\Cache\City', $this->tester->getDisplay()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/MappingDescribeCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/MappingDescribeCommandTest.php new file mode 100644 index 000000000..017e94aee --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/MappingDescribeCommandTest.php @@ -0,0 +1,87 @@ +application = new Application(); + $command = new MappingDescribeCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($command); + + $this->command = $this->application->find('orm:mapping:describe'); + $this->tester = new CommandTester($command); + } + + public function testShowSpecificFuzzySingle() + { + $this->tester->execute(array( + 'command' => $this->command->getName(), + 'entityName' => 'AttractionInfo', + )); + + $display = $this->tester->getDisplay(); + $this->assertContains('Doctrine\Tests\Models\Cache\AttractionInfo', $display); + $this->assertContains('Root entity name', $display); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage possible matches + */ + public function testShowSpecificFuzzyAmbiguous() + { + $this->tester->execute(array( + 'command' => $this->command->getName(), + 'entityName' => 'Attraction', + )); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Could not find any mapped Entity classes matching "AttractionFooBar" + */ + public function testShowSpecificNotFound() + { + $this->tester->execute(array( + 'command' => $this->command->getName(), + 'entityName' => 'AttractionFooBar' + )); + } +} +