Skip to content

Commit 61cda3e

Browse files
committed
feature #24208 [Form] Display option definition from a given form type (yceruto, ogizanagi)
This PR was merged into the 3.4 branch. Discussion ---------- [Form] Display option definition from a given form type | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes (deps=high failure expected) | Fixed tickets | - | License | MIT | Doc PR | - ![debug-form-option](https://user-images.githubusercontent.com/2028198/30569305-12a30738-9ca8-11e7-98b7-6eaf78d3d5a7.png) Show friendly message if typo: ![debug-form-not-found](https://user-images.githubusercontent.com/2028198/30450999-83d58b56-9960-11e7-8705-b60ba33baf48.png) complement of #24185 Commits ------- d6d187d Add & use OptionResolverIntrospector 8bbb5e7 Add debug:form type option
2 parents 5e40274 + d6d187d commit 61cda3e

19 files changed

+734
-98
lines changed

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

+81-4
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'] = array_values(array_diff($this->types, $options['core_types']));
9199
$options['extensions'] = $this->extensions;
92100
$options['guessers'] = $this->guessers;
101+
foreach ($options as $k => $list) {
102+
sort($options[$k]);
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];
@@ -121,6 +169,35 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor
121169
throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes)));
122170
}
123171

124-
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]);
172+
return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]);
173+
}
174+
175+
private function getCoreTypes()
176+
{
177+
$coreExtension = new CoreExtension();
178+
$loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes');
179+
$loadTypesRefMethod->setAccessible(true);
180+
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
181+
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
182+
sort($coreTypes);
183+
184+
return $coreTypes;
185+
}
186+
187+
private function findAlternatives($name, array $collection)
188+
{
189+
$alternatives = array();
190+
foreach ($collection as $item) {
191+
$lev = levenshtein($name, $item);
192+
if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
193+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
194+
}
195+
}
196+
197+
$threshold = 1e3;
198+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
199+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
200+
201+
return array_keys($alternatives);
125202
}
126203
}

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

+35-19
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
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;
21+
use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
22+
use Symfony\Component\OptionsResolver\Exception\NoConfigurationException;
2323
use Symfony\Component\OptionsResolver\OptionsResolver;
2424

2525
/**
@@ -29,7 +29,7 @@
2929
*/
3030
abstract class Descriptor implements DescriptorInterface
3131
{
32-
/** @var StyleInterface */
32+
/** @var OutputStyle */
3333
protected $output;
3434
protected $type;
3535
protected $ownOptions = array();
@@ -45,7 +45,7 @@ abstract class Descriptor implements DescriptorInterface
4545
*/
4646
public function describe(OutputInterface $output, $object, array $options = array())
4747
{
48-
$this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
48+
$this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
4949

5050
switch (true) {
5151
case null === $object:
@@ -54,28 +54,19 @@ public function describe(OutputInterface $output, $object, array $options = arra
5454
case $object instanceof ResolvedFormTypeInterface:
5555
$this->describeResolvedFormType($object, $options);
5656
break;
57+
case $object instanceof OptionsResolver:
58+
$this->describeOption($object, $options);
59+
break;
5760
default:
5861
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
5962
}
6063
}
6164

62-
abstract protected function describeDefaults(array $options = array());
65+
abstract protected function describeDefaults(array $options);
6366

6467
abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array());
6568

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-
}
69+
abstract protected function describeOption(OptionsResolver $optionsResolver, array $options);
7970

8071
protected function collectOptions(ResolvedFormTypeInterface $type)
8172
{
@@ -113,6 +104,31 @@ protected function collectOptions(ResolvedFormTypeInterface $type)
113104
$this->extensions = array_keys($this->extensions);
114105
}
115106

107+
protected function getOptionDefinition(OptionsResolver $optionsResolver, $option)
108+
{
109+
$definition = array('required' => $optionsResolver->isRequired($option));
110+
111+
$introspector = new OptionsResolverIntrospector($optionsResolver);
112+
113+
$map = array(
114+
'default' => 'getDefault',
115+
'lazy' => 'getLazyClosures',
116+
'allowedTypes' => 'getAllowedTypes',
117+
'allowedValues' => 'getAllowedValues',
118+
'normalizer' => 'getNormalizer',
119+
);
120+
121+
foreach ($map as $key => $method) {
122+
try {
123+
$definition[$key] = $introspector->{$method}($option);
124+
} catch (NoConfigurationException $e) {
125+
// noop
126+
}
127+
}
128+
129+
return $definition;
130+
}
131+
116132
private function getParentOptionsResolver(ResolvedFormTypeInterface $type)
117133
{
118134
$this->parents[$class = get_class($type->getInnerType())] = array();

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

+28-3
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>
@@ -20,10 +21,10 @@
2021
*/
2122
class JsonDescriptor extends Descriptor
2223
{
23-
protected function describeDefaults(array $options = array())
24+
protected function describeDefaults(array $options)
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'] = $options['service_types'];
2728
$data['type_extensions'] = $options['extensions'];
2829
$data['type_guessers'] = $options['guessers'];
2930

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

58+
protected function describeOption(OptionsResolver $optionsResolver, array $options)
59+
{
60+
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);
61+
62+
$map = array(
63+
'required' => 'required',
64+
'default' => 'default',
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+
if ('default' === $name) {
73+
$data['is_lazy'] = isset($definition['lazy']);
74+
}
75+
}
76+
}
77+
$data['has_normalizer'] = isset($definition['normalizer']);
78+
79+
$this->writeData($data, $options);
80+
}
81+
5782
private function writeData(array $data, array $options)
5883
{
5984
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;

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

+55-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
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\Caster\Caster;
18+
use Symfony\Component\VarDumper\Cloner\VarCloner;
19+
use Symfony\Component\VarDumper\Dumper\CliDumper;
1620

1721
/**
1822
* @author Yonel Ceruto <yonelceruto@gmail.com>
@@ -21,18 +25,16 @@
2125
*/
2226
class TextDescriptor extends Descriptor
2327
{
24-
protected function describeDefaults(array $options = array())
28+
protected function describeDefaults(array $options)
2529
{
26-
$coreTypes = $this->getCoreTypes();
27-
2830
$this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
29-
$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $coreTypes);
31+
$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $options['core_types']);
3032
for ($i = 0; $i * 5 < count($shortClassNames); ++$i) {
3133
$this->output->writeln(' '.implode(', ', array_slice($shortClassNames, $i * 5, 5)));
3234
}
3335

3436
$this->output->section('Service form types');
35-
$this->output->listing(array_diff($options['types'], $coreTypes));
37+
$this->output->listing($options['service_types']);
3638

3739
$this->output->section('Type extensions');
3840
$this->output->listing($options['extensions']);
@@ -94,6 +96,34 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF
9496
}
9597
}
9698

99+
protected function describeOption(OptionsResolver $optionsResolver, array $options)
100+
{
101+
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);
102+
103+
$dump = $this->getDumpFunction();
104+
$map = array(
105+
'Required' => 'required',
106+
'Default' => 'default',
107+
'Allowed types' => 'allowedTypes',
108+
'Allowed values' => 'allowedValues',
109+
'Normalizer' => 'normalizer',
110+
);
111+
$rows = array();
112+
foreach ($map as $label => $name) {
113+
$value = array_key_exists($name, $definition) ? $dump($definition[$name]) : '-';
114+
if ('default' === $name && isset($definition['lazy'])) {
115+
$value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']);
116+
}
117+
118+
$rows[] = array("<info>$label</info>", $value);
119+
$rows[] = new TableSeparator();
120+
}
121+
array_pop($rows);
122+
123+
$this->output->title(sprintf('%s (%s)', get_class($options['type']), $options['option']));
124+
$this->output->table(array(), $rows);
125+
}
126+
97127
private function normalizeAndSortOptionsColumns(array $options)
98128
{
99129
foreach ($options as $group => &$opts) {
@@ -125,4 +155,24 @@ private function normalizeAndSortOptionsColumns(array $options)
125155

126156
return $options;
127157
}
158+
159+
private function getDumpFunction()
160+
{
161+
$cloner = new VarCloner();
162+
$cloner->addCasters(array('Closure' => function ($c, $a) {
163+
$prefix = Caster::PREFIX_VIRTUAL;
164+
165+
return array(
166+
$prefix.'parameters' => isset($a[$prefix.'parameters']) ? count($a[$prefix.'parameters']->value) : 0,
167+
$prefix.'file' => $a[$prefix.'file'],
168+
$prefix.'line' => $a[$prefix.'line'],
169+
);
170+
}));
171+
$dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
172+
$dumper->setColors($this->output->isDecorated());
173+
174+
return function ($value) use ($dumper, $cloner) {
175+
return rtrim($dumper->dump($cloner->cloneVar($value)->withRefHandles(false), true));
176+
};
177+
}
128178
}

0 commit comments

Comments
 (0)