Skip to content

Commit 20e9af2

Browse files
committed
Add debug:form type option
1 parent b749204 commit 20e9af2

File tree

11 files changed

+270
-86
lines changed

11 files changed

+270
-86
lines changed

src/Symfony/Component/Form/Command/DebugCommand.php

+81-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
use Symfony\Component\Console\Output\OutputInterface;
2020
use Symfony\Component\Console\Style\SymfonyStyle;
2121
use Symfony\Component\Form\Console\Helper\DescriptorHelper;
22+
use Symfony\Component\Form\Extension\Core\CoreExtension;
2223
use Symfony\Component\Form\FormRegistryInterface;
24+
use Symfony\Component\Form\FormTypeInterface;
2325

2426
/**
2527
* A console command for retrieving information about form types.
@@ -55,6 +57,7 @@ protected function configure()
5557
$this
5658
->setDefinition(array(
5759
new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
60+
new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'),
5861
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
5962
))
6063
->setDescription('Displays form type information')
@@ -70,6 +73,10 @@ protected function configure()
7073
7174
The command lists all defined options that contains the given form type, as well as their parents and type extensions.
7275
76+
<info>php %command.full_name% ChoiceType choice_value</info>
77+
78+
The command displays the definition of the given option name.
79+
7380
<info>php %command.full_name% --format=json</info>
7481
7582
The command lists everything in a machine readable json format.
@@ -87,14 +94,42 @@ protected function execute(InputInterface $input, OutputInterface $output)
8794

8895
if (null === $class = $input->getArgument('class')) {
8996
$object = null;
90-
$options['types'] = $this->types;
97+
$options['core_types'] = $this->getCoreTypes();
98+
$options['service_types'] = $this->types;
9199
$options['extensions'] = $this->extensions;
92100
$options['guessers'] = $this->guessers;
101+
foreach ($options as &$list) {
102+
sort($list);
103+
}
93104
} else {
94105
if (!class_exists($class)) {
95106
$class = $this->getFqcnTypeClass($input, $io, $class);
96107
}
97-
$object = $this->formRegistry->getType($class);
108+
$resolvedType = $this->formRegistry->getType($class);
109+
110+
if ($option = $input->getArgument('option')) {
111+
$object = $resolvedType->getOptionsResolver();
112+
113+
if (!$object->isDefined($option)) {
114+
$message = sprintf('Option "%s" is not defined in "%s".', $option, get_class($resolvedType->getInnerType()));
115+
116+
if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) {
117+
if (1 == count($alternatives)) {
118+
$message .= "\n\nDid you mean this?\n ";
119+
} else {
120+
$message .= "\n\nDid you mean one of these?\n ";
121+
}
122+
$message .= implode("\n ", $alternatives);
123+
}
124+
125+
throw new InvalidArgumentException($message);
126+
}
127+
128+
$options['type'] = $resolvedType->getInnerType();
129+
$options['option'] = $option;
130+
} else {
131+
$object = $resolvedType;
132+
}
98133
}
99134

100135
$helper = new DescriptorHelper();
@@ -105,14 +140,27 @@ protected function execute(InputInterface $input, OutputInterface $output)
105140
private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName)
106141
{
107142
$classes = array();
143+
sort($this->namespaces);
108144
foreach ($this->namespaces as $namespace) {
109145
if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
110146
$classes[] = $fqcn;
111147
}
112148
}
113149

114150
if (0 === $count = count($classes)) {
115-
throw new InvalidArgumentException(sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)));
151+
$message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces));
152+
153+
$allTypes = array_merge($this->getCoreTypes(), $this->types);
154+
if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) {
155+
if (1 == count($alternatives)) {
156+
$message .= "\n\nDid you mean this?\n ";
157+
} else {
158+
$message .= "\n\nDid you mean one of these?\n ";
159+
}
160+
$message .= implode("\n ", $alternatives);
161+
}
162+
163+
throw new InvalidArgumentException($message);
116164
}
117165
if (1 === $count) {
118166
return $classes[0];
@@ -123,4 +171,34 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor
123171

124172
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]);
125173
}
174+
175+
private function getCoreTypes()
176+
{
177+
$coreExtension = new CoreExtension();
178+
$coreExtensionRefObject = new \ReflectionObject($coreExtension);
179+
$loadTypesRefMethod = $coreExtensionRefObject->getMethod('loadTypes');
180+
$loadTypesRefMethod->setAccessible(true);
181+
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
182+
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
183+
sort($coreTypes);
184+
185+
return $coreTypes;
186+
}
187+
188+
private function findAlternatives($name, array $collection)
189+
{
190+
$alternatives = array();
191+
foreach ($collection as $item) {
192+
$lev = levenshtein($name, $item);
193+
if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
194+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
195+
}
196+
}
197+
198+
$threshold = 1e3;
199+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
200+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
201+
202+
return array_keys($alternatives);
203+
}
126204
}

src/Symfony/Component/Form/Console/Descriptor/Descriptor.php

+28-18
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
use Symfony\Component\Console\Descriptor\DescriptorInterface;
1515
use Symfony\Component\Console\Input\ArrayInput;
1616
use Symfony\Component\Console\Output\OutputInterface;
17-
use Symfony\Component\Console\Style\StyleInterface;
17+
use Symfony\Component\Console\Style\OutputStyle;
1818
use Symfony\Component\Console\Style\SymfonyStyle;
19-
use Symfony\Component\Form\Extension\Core\CoreExtension;
20-
use Symfony\Component\Form\FormTypeInterface;
2119
use Symfony\Component\Form\ResolvedFormTypeInterface;
2220
use Symfony\Component\Form\Util\OptionsResolverWrapper;
2321
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -29,7 +27,7 @@
2927
*/
3028
abstract class Descriptor implements DescriptorInterface
3129
{
32-
/** @var StyleInterface */
30+
/** @var OutputStyle */
3331
protected $output;
3432
protected $type;
3533
protected $ownOptions = array();
@@ -45,7 +43,7 @@ abstract class Descriptor implements DescriptorInterface
4543
*/
4644
public function describe(OutputInterface $output, $object, array $options = array())
4745
{
48-
$this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
46+
$this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
4947

5048
switch (true) {
5149
case null === $object:
@@ -54,6 +52,9 @@ public function describe(OutputInterface $output, $object, array $options = arra
5452
case $object instanceof ResolvedFormTypeInterface:
5553
$this->describeResolvedFormType($object, $options);
5654
break;
55+
case $object instanceof OptionsResolver:
56+
$this->describeOption($object, $options);
57+
break;
5758
default:
5859
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
5960
}
@@ -63,19 +64,7 @@ abstract protected function describeDefaults(array $options = array());
6364

6465
abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array());
6566

66-
protected function getCoreTypes()
67-
{
68-
$coreExtension = new CoreExtension();
69-
$coreExtensionRefObject = new \ReflectionObject($coreExtension);
70-
$loadTypesRefMethod = $coreExtensionRefObject->getMethod('loadTypes');
71-
$loadTypesRefMethod->setAccessible(true);
72-
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
73-
74-
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
75-
sort($coreTypes);
76-
77-
return $coreTypes;
78-
}
67+
abstract protected function describeOption(OptionsResolver $optionsResolver, array $options = array());
7968

8069
protected function collectOptions(ResolvedFormTypeInterface $type)
8170
{
@@ -113,6 +102,27 @@ protected function collectOptions(ResolvedFormTypeInterface $type)
113102
$this->extensions = array_keys($this->extensions);
114103
}
115104

105+
protected function getOptionDefinition(OptionsResolver $optionsResolver, $option)
106+
{
107+
$refObject = new \ReflectionObject($optionsResolver);
108+
foreach (array('defaults', 'lazy', 'allowedTypes', 'allowedValues', 'normalizers') as $name) {
109+
$property = $refObject->getProperty($name);
110+
$property->setAccessible(true);
111+
$value = $property->getValue($optionsResolver);
112+
if (array_key_exists($option, $value)) {
113+
$definition[$name] = $value[$option];
114+
}
115+
}
116+
$definition['required'] = $optionsResolver->isRequired($option);
117+
118+
if (isset($definition['lazy'])) {
119+
$definition['defaults'] = 1 === count($definition['lazy']) ? $definition['lazy'][0] : $definition['lazy'];
120+
unset($definition['lazy']);
121+
}
122+
123+
return $definition;
124+
}
125+
116126
private function getParentOptionsResolver(ResolvedFormTypeInterface $type)
117127
{
118128
$this->parents[$class = get_class($type->getInnerType())] = array();

src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php

+23-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Form\Console\Descriptor;
1313

1414
use Symfony\Component\Form\ResolvedFormTypeInterface;
15+
use Symfony\Component\OptionsResolver\OptionsResolver;
1516

1617
/**
1718
* @author Yonel Ceruto <yonelceruto@gmail.com>
@@ -22,8 +23,8 @@ class JsonDescriptor extends Descriptor
2223
{
2324
protected function describeDefaults(array $options = array())
2425
{
25-
$data['builtin_form_types'] = $this->getCoreTypes();
26-
$data['service_form_types'] = array_values(array_diff($options['types'], $data['builtin_form_types']));
26+
$data['builtin_form_types'] = $options['core_types'];
27+
$data['service_form_types'] = array_values(array_diff($options['service_types'], $data['builtin_form_types']));
2728
$data['type_extensions'] = $options['extensions'];
2829
$data['type_guessers'] = $options['guessers'];
2930

@@ -54,6 +55,26 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF
5455
$this->writeData($data, $options);
5556
}
5657

58+
protected function describeOption(OptionsResolver $optionsResolver, array $options = array())
59+
{
60+
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);
61+
62+
$map = array(
63+
'required' => 'required',
64+
'default' => 'defaults',
65+
'allowed_types' => 'allowedTypes',
66+
'allowed_values' => 'allowedValues',
67+
);
68+
foreach ($map as $label => $name) {
69+
if (array_key_exists($name, $definition)) {
70+
$data[$label] = $definition[$name];
71+
}
72+
}
73+
$data['has_normalizer'] = isset($definition['normalizers']);
74+
75+
$this->writeData($data, $options);
76+
}
77+
5778
private function writeData(array $data, array $options)
5879
{
5980
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;

src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
use Symfony\Component\Console\Helper\TableSeparator;
1515
use Symfony\Component\Form\ResolvedFormTypeInterface;
16+
use Symfony\Component\OptionsResolver\OptionsResolver;
17+
use Symfony\Component\VarDumper\Cloner\VarCloner;
18+
use Symfony\Component\VarDumper\Dumper\CliDumper;
1619

1720
/**
1821
* @author Yonel Ceruto <yonelceruto@gmail.com>
@@ -23,7 +26,7 @@ class TextDescriptor extends Descriptor
2326
{
2427
protected function describeDefaults(array $options = array())
2528
{
26-
$coreTypes = $this->getCoreTypes();
29+
$coreTypes = $options['core_types'];
2730

2831
$this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
2932
$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $coreTypes);
@@ -32,7 +35,7 @@ protected function describeDefaults(array $options = array())
3235
}
3336

3437
$this->output->section('Service form types');
35-
$this->output->listing(array_diff($options['types'], $coreTypes));
38+
$this->output->listing(array_diff($options['service_types'], $coreTypes));
3639

3740
$this->output->section('Type extensions');
3841
$this->output->listing($options['extensions']);
@@ -94,6 +97,29 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF
9497
}
9598
}
9699

100+
protected function describeOption(OptionsResolver $optionsResolver, array $options = array())
101+
{
102+
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);
103+
104+
$dump = $this->getDumpFunction();
105+
$map = array(
106+
'Required' => 'required',
107+
'Default' => 'defaults',
108+
'Allowed types' => 'allowedTypes',
109+
'Allowed values' => 'allowedValues',
110+
'Normalizer' => 'normalizers',
111+
);
112+
$rows = array();
113+
foreach ($map as $label => $name) {
114+
$rows[] = array("<info>$label</info>", array_key_exists($name, $definition) ? $dump($definition[$name]) : '-');
115+
$rows[] = new TableSeparator();
116+
}
117+
array_pop($rows);
118+
119+
$this->output->title(sprintf('%s (%s)', get_class($options['type']), $options['option']));
120+
$this->output->table(array(), $rows);
121+
}
122+
97123
private function normalizeAndSortOptionsColumns(array $options)
98124
{
99125
foreach ($options as $group => &$opts) {
@@ -125,4 +151,15 @@ private function normalizeAndSortOptionsColumns(array $options)
125151

126152
return $options;
127153
}
154+
155+
private function getDumpFunction()
156+
{
157+
$cloner = new VarCloner();
158+
$dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
159+
$dumper->setColors($this->output->isDecorated());
160+
161+
return function ($value) use ($dumper, $cloner) {
162+
return rtrim($dumper->dump($cloner->cloneVar($value), true));
163+
};
164+
}
128165
}

src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\Form\Extension\Core\Type\FormType;
1919
use Symfony\Component\Form\FormRegistryInterface;
2020
use Symfony\Component\Form\ResolvedFormTypeInterface;
21+
use Symfony\Component\OptionsResolver\OptionsResolver;
2122

2223
class DebugCommandTest extends TestCase
2324
{
@@ -39,6 +40,15 @@ public function testDebugSingleFormType()
3940
$this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")', $tester->getDisplay());
4041
}
4142

43+
public function testDebugFormTypeOption()
44+
{
45+
$tester = $this->createCommandTester();
46+
$ret = $tester->execute(array('class' => 'FormType', 'option' => 'method'), array('decorated' => false));
47+
48+
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
49+
$this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (method)', $tester->getDisplay());
50+
}
51+
4252
/**
4353
* @expectedException \InvalidArgumentException
4454
*/
@@ -68,6 +78,13 @@ private function createCommandTester()
6878
->method('getTypeExtensions')
6979
->willReturn(array())
7080
;
81+
$optionsResolver = new OptionsResolver();
82+
$optionsResolver->setDefault('method', 'POST');
83+
$resolvedFormType
84+
->expects($this->any())
85+
->method('getOptionsResolver')
86+
->willReturn($optionsResolver)
87+
;
7188

7289
$formRegistry = $this->getMockBuilder(FormRegistryInterface::class)->getMock();
7390
$formRegistry

0 commit comments

Comments
 (0)