Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 61cda3e

Browse filesBrowse 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
Copy full SHA for 61cda3e

19 files changed

+734
-98
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Command/DebugCommand.php
+81-4Lines changed: 81 additions & 4 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
+35-19Lines changed: 35 additions & 19 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php
+28-3Lines changed: 28 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php
+55-5Lines changed: 55 additions & 5 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.