feature #24185 [Form] Display general forms information on debug:form (yceruto)

This PR was merged into the 3.4 branch.

Discussion
----------

[Form] Display general forms information on debug:form

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

![debug-form-defaults](https://user-images.githubusercontent.com/2028198/30436620-998103ca-993a-11e7-9873-31f042327374.png)

When we run `bin/console debug:form` (without argument) all possible Form Component information is displayed.

Commits
-------

12d1a7f810 Display form defaults on debug:form
This commit is contained in:
Maxime Steinhausser 2017-09-15 18:33:15 +02:00
commit b7492045f3
11 changed files with 213 additions and 14 deletions

View File

@ -100,6 +100,9 @@
<service id="Symfony\Component\Form\Command\DebugCommand">
<argument type="service" id="form.registry" />
<argument type="collection" /> <!-- All form types namespaces are stored here by FormPass -->
<argument type="collection" /> <!-- All services form types are stored here by FormPass -->
<argument type="collection" /> <!-- All type extensions are stored here by FormPass -->
<argument type="collection" /> <!-- All type guessers are stored here by FormPass -->
<tag name="console.command" command="debug:form" />
</service>
</services>

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
3.4.0
-----
* added `DebugCommand`
3.3.0
-----

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -31,13 +32,19 @@ class DebugCommand extends Command
private $formRegistry;
private $namespaces;
private $types;
private $extensions;
private $guessers;
public function __construct(FormRegistryInterface $formRegistry, array $namespaces = array('Symfony\Component\Form\Extension\Core\Type'))
public function __construct(FormRegistryInterface $formRegistry, array $namespaces = array('Symfony\Component\Form\Extension\Core\Type'), array $types = array(), array $extensions = array(), array $guessers = array())
{
parent::__construct();
$this->formRegistry = $formRegistry;
$this->namespaces = $namespaces;
$this->types = $types;
$this->extensions = $extensions;
$this->guessers = $guessers;
}
/**
@ -47,18 +54,25 @@ class DebugCommand extends Command
{
$this
->setDefinition(array(
new InputArgument('class', InputArgument::REQUIRED, 'The form type class'),
new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
))
->setDescription('Displays form type information')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays information about a form type.
The <info>%command.name%</info> command displays information about form types.
Either the fully-qualified class name or the short class name can be used:
<info>php %command.full_name%</info>
The command lists all built-in types, services types, type extensions and guessers currently available.
<info>php %command.full_name% Symfony\Component\Form\Extension\Core\Type\ChoiceType</info>
<info>php %command.full_name% ChoiceType</info>
The command lists all defined options that contains the given form type, as well as their parents and type extensions.
<info>php %command.full_name% --format=json</info>
The command lists everything in a machine readable json format.
EOF
)
;
@ -71,12 +85,18 @@ EOF
{
$io = new SymfonyStyle($input, $output);
if (!class_exists($class = $input->getArgument('class'))) {
$class = $this->getFqcnTypeClass($input, $io, $class);
if (null === $class = $input->getArgument('class')) {
$object = null;
$options['types'] = $this->types;
$options['extensions'] = $this->extensions;
$options['guessers'] = $this->guessers;
} else {
if (!class_exists($class)) {
$class = $this->getFqcnTypeClass($input, $io, $class);
}
$object = $this->formRegistry->getType($class);
}
$object = $this->formRegistry->getType($class);
$helper = new DescriptorHelper();
$options['format'] = $input->getOption('format');
$helper->describe($io, $object, $options);
@ -92,13 +112,13 @@ EOF
}
if (0 === $count = count($classes)) {
throw new \InvalidArgumentException(sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)));
throw new InvalidArgumentException(sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)));
}
if (1 === $count) {
return $classes[0];
}
if (!$input->isInteractive()) {
throw new \InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes)));
throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes)));
}
return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\n Select one of the following form types to display its information:", $shortClassName), $classes, $classes[0]);

View File

@ -12,8 +12,12 @@
namespace Symfony\Component\Form\Console\Descriptor;
use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\Form\Util\OptionsResolverWrapper;
use Symfony\Component\OptionsResolver\OptionsResolver;
@ -25,9 +29,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
abstract class Descriptor implements DescriptorInterface
{
/**
* @var SymfonyStyle
*/
/** @var StyleInterface */
protected $output;
protected $type;
protected $ownOptions = array();
@ -43,9 +45,12 @@ abstract class Descriptor implements DescriptorInterface
*/
public function describe(OutputInterface $output, $object, array $options = array())
{
$this->output = $output;
$this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
switch (true) {
case null === $object:
$this->describeDefaults($options);
break;
case $object instanceof ResolvedFormTypeInterface:
$this->describeResolvedFormType($object, $options);
break;
@ -54,8 +59,24 @@ abstract class Descriptor implements DescriptorInterface
}
}
abstract protected function describeDefaults(array $options = array());
abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array());
protected function getCoreTypes()
{
$coreExtension = new CoreExtension();
$coreExtensionRefObject = new \ReflectionObject($coreExtension);
$loadTypesRefMethod = $coreExtensionRefObject->getMethod('loadTypes');
$loadTypesRefMethod->setAccessible(true);
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
sort($coreTypes);
return $coreTypes;
}
protected function collectOptions(ResolvedFormTypeInterface $type)
{
$this->parents = array();

View File

@ -20,6 +20,16 @@ use Symfony\Component\Form\ResolvedFormTypeInterface;
*/
class JsonDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
{
$data['builtin_form_types'] = $this->getCoreTypes();
$data['service_form_types'] = array_values(array_diff($options['types'], $data['builtin_form_types']));
$data['type_extensions'] = $options['extensions'];
$data['type_guessers'] = $options['guessers'];
$this->writeData($data, $options);
}
protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
{
$this->collectOptions($resolvedFormType);

View File

@ -21,6 +21,26 @@ use Symfony\Component\Form\ResolvedFormTypeInterface;
*/
class TextDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
{
$coreTypes = $this->getCoreTypes();
$this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $coreTypes);
for ($i = 0; $i * 5 < count($shortClassNames); ++$i) {
$this->output->writeln(' '.implode(', ', array_slice($shortClassNames, $i * 5, 5)));
}
$this->output->section('Service form types');
$this->output->listing(array_diff($options['types'], $coreTypes));
$this->output->section('Type extensions');
$this->output->listing($options['extensions']);
$this->output->section('Type guessers');
$this->output->listing($options['guessers']);
}
protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
{
$this->collectOptions($resolvedFormType);

View File

@ -77,6 +77,7 @@ class FormPass implements CompilerPassInterface
if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(1, array_keys($namespaces));
$commandDefinition->setArgument(2, array_keys($servicesMap));
}
return ServiceLocatorTagPass::register($container, $servicesMap);
@ -85,6 +86,7 @@ class FormPass implements CompilerPassInterface
private function processFormTypeExtensions(ContainerBuilder $container)
{
$typeExtensions = array();
$typeExtensionsClasses = array();
foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) {
$serviceId = (string) $reference;
$serviceDefinition = $container->getDefinition($serviceId);
@ -97,20 +99,35 @@ class FormPass implements CompilerPassInterface
}
$typeExtensions[$extendedType][] = new Reference($serviceId);
$typeExtensionsClasses[] = $serviceDefinition->getClass();
}
foreach ($typeExtensions as $extendedType => $extensions) {
$typeExtensions[$extendedType] = new IteratorArgument($extensions);
}
if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(3, $typeExtensionsClasses);
}
return $typeExtensions;
}
private function processFormTypeGuessers(ContainerBuilder $container)
{
$guessers = array();
$guessersClasses = array();
foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag, true) as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);
$serviceDefinition = $container->getDefinition($serviceId);
$guessersClasses[] = $serviceDefinition->getClass();
}
if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(4, $guessersClasses);
}
return new IteratorArgument($guessers);

View File

@ -21,6 +21,15 @@ use Symfony\Component\Form\ResolvedFormTypeInterface;
class DebugCommandTest extends TestCase
{
public function testDebugDefaults()
{
$tester = $this->createCommandTester();
$ret = $tester->execute(array(), array('decorated' => false));
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertContains('Built-in form types', $tester->getDisplay());
}
public function testDebugSingleFormType()
{
$tester = $this->createCommandTester();

View File

@ -24,6 +24,19 @@ use Symfony\Component\Security\Csrf\CsrfTokenManager;
abstract class AbstractDescriptorTest extends TestCase
{
/** @dataProvider getDescribeDefaultsTestData */
public function testDescribeDefaults($object, array $options, $fixtureName)
{
$expectedDescription = $this->getExpectedDescription($fixtureName);
$describedObject = $this->getObjectDescription($object, $options);
if ('json' === $this->getFormat()) {
$this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($describedObject), JSON_PRETTY_PRINT));
} else {
$this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $describedObject)));
}
}
/** @dataProvider getDescribeResolvedFormTypeTestData */
public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, array $options, $fixtureName)
{
@ -37,6 +50,15 @@ abstract class AbstractDescriptorTest extends TestCase
}
}
public function getDescribeDefaultsTestData()
{
$options['types'] = array('Symfony\Bridge\Doctrine\Form\Type\EntityType');
$options['extensions'] = array('Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension');
$options['guessers'] = array('Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser');
yield array(null, $options, 'defaults_1');
}
public function getDescribeResolvedFormTypeTestData()
{
$typeExtensions = array(

View File

@ -0,0 +1,45 @@
{
"builtin_form_types": [
"Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ResetType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType"
],
"service_form_types": [
"Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType"
],
"type_extensions": [
"Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension"
],
"type_guessers": [
"Symfony\\Component\\Form\\Extension\\Validator\\ValidatorTypeGuesser"
]
}

View File

@ -0,0 +1,27 @@
Built-in form types (Symfony\Component\Form\Extension\Core\Type)
----------------------------------------------------------------
BirthdayType, ButtonType, CheckboxType, ChoiceType, CollectionType
CountryType, CurrencyType, DateIntervalType, DateTimeType, DateType
EmailType, FileType, FormType, HiddenType, IntegerType
LanguageType, LocaleType, MoneyType, NumberType, PasswordType
PercentType, RadioType, RangeType, RepeatedType, ResetType
SearchType, SubmitType, TextType, TextareaType, TimeType
TimezoneType, UrlType
Service form types
------------------
* Symfony\Bridge\Doctrine\Form\Type\EntityType
Type extensions
---------------
* Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension
Type guessers
-------------
* Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser