Skip to content

Commit

Permalink
feature #24208 [Form] Display option definition from a given form typ…
Browse files Browse the repository at this point in the history
…e (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
  • Loading branch information
fabpot committed Oct 9, 2017
2 parents 5e40274 + d6d187d commit 61cda3e
Show file tree
Hide file tree
Showing 19 changed files with 734 additions and 98 deletions.
85 changes: 81 additions & 4 deletions src/Symfony/Component/Form/Command/DebugCommand.php
Expand Up @@ -19,7 +19,9 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Form\Console\Helper\DescriptorHelper;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Component\Form\FormRegistryInterface;
use Symfony\Component\Form\FormTypeInterface;

/**
* A console command for retrieving information about form types.
Expand Down Expand Up @@ -55,6 +57,7 @@ protected function configure()
$this
->setDefinition(array(
new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
))
->setDescription('Displays form type information')
Expand All @@ -70,6 +73,10 @@ protected function configure()
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% ChoiceType choice_value</info>
The command displays the definition of the given option name.
<info>php %command.full_name% --format=json</info>
The command lists everything in a machine readable json format.
Expand All @@ -87,14 +94,42 @@ protected function execute(InputInterface $input, OutputInterface $output)

if (null === $class = $input->getArgument('class')) {
$object = null;
$options['types'] = $this->types;
$options['core_types'] = $this->getCoreTypes();
$options['service_types'] = array_values(array_diff($this->types, $options['core_types']));
$options['extensions'] = $this->extensions;
$options['guessers'] = $this->guessers;
foreach ($options as $k => $list) {
sort($options[$k]);
}
} else {
if (!class_exists($class)) {
$class = $this->getFqcnTypeClass($input, $io, $class);
}
$object = $this->formRegistry->getType($class);
$resolvedType = $this->formRegistry->getType($class);

if ($option = $input->getArgument('option')) {
$object = $resolvedType->getOptionsResolver();

if (!$object->isDefined($option)) {
$message = sprintf('Option "%s" is not defined in "%s".', $option, get_class($resolvedType->getInnerType()));

if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) {
if (1 == count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
}
$message .= implode("\n ", $alternatives);
}

throw new InvalidArgumentException($message);
}

$options['type'] = $resolvedType->getInnerType();
$options['option'] = $option;
} else {
$object = $resolvedType;
}
}

$helper = new DescriptorHelper();
Expand All @@ -105,14 +140,27 @@ protected function execute(InputInterface $input, OutputInterface $output)
private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName)
{
$classes = array();
sort($this->namespaces);
foreach ($this->namespaces as $namespace) {
if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
$classes[] = $fqcn;
}
}

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)));
$message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces));

$allTypes = array_merge($this->getCoreTypes(), $this->types);
if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) {
if (1 == count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
}
$message .= implode("\n ", $alternatives);
}

throw new InvalidArgumentException($message);
}
if (1 === $count) {
return $classes[0];
Expand All @@ -121,6 +169,35 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor
throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\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]);
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]);
}

private function getCoreTypes()
{
$coreExtension = new CoreExtension();
$loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes');
$loadTypesRefMethod->setAccessible(true);
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
sort($coreTypes);

return $coreTypes;
}

private function findAlternatives($name, array $collection)
{
$alternatives = array();
foreach ($collection as $item) {
$lev = levenshtein($name, $item);
if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
}
}

$threshold = 1e3;
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);

return array_keys($alternatives);
}
}
54 changes: 35 additions & 19 deletions src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
Expand Up @@ -14,12 +14,12 @@
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\OutputStyle;
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\Debug\OptionsResolverIntrospector;
use Symfony\Component\OptionsResolver\Exception\NoConfigurationException;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand All @@ -29,7 +29,7 @@
*/
abstract class Descriptor implements DescriptorInterface
{
/** @var StyleInterface */
/** @var OutputStyle */
protected $output;
protected $type;
protected $ownOptions = array();
Expand All @@ -45,7 +45,7 @@ abstract class Descriptor implements DescriptorInterface
*/
public function describe(OutputInterface $output, $object, array $options = array())
{
$this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output);
$this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput(array()), $output);

switch (true) {
case null === $object:
Expand All @@ -54,28 +54,19 @@ public function describe(OutputInterface $output, $object, array $options = arra
case $object instanceof ResolvedFormTypeInterface:
$this->describeResolvedFormType($object, $options);
break;
case $object instanceof OptionsResolver:
$this->describeOption($object, $options);
break;
default:
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
}
}

abstract protected function describeDefaults(array $options = array());
abstract protected function describeDefaults(array $options);

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

protected function collectOptions(ResolvedFormTypeInterface $type)
{
Expand Down Expand Up @@ -113,6 +104,31 @@ protected function collectOptions(ResolvedFormTypeInterface $type)
$this->extensions = array_keys($this->extensions);
}

protected function getOptionDefinition(OptionsResolver $optionsResolver, $option)
{
$definition = array('required' => $optionsResolver->isRequired($option));

$introspector = new OptionsResolverIntrospector($optionsResolver);

$map = array(
'default' => 'getDefault',
'lazy' => 'getLazyClosures',
'allowedTypes' => 'getAllowedTypes',
'allowedValues' => 'getAllowedValues',
'normalizer' => 'getNormalizer',
);

foreach ($map as $key => $method) {
try {
$definition[$key] = $introspector->{$method}($option);
} catch (NoConfigurationException $e) {
// noop
}
}

return $definition;
}

private function getParentOptionsResolver(ResolvedFormTypeInterface $type)
{
$this->parents[$class = get_class($type->getInnerType())] = array();
Expand Down
31 changes: 28 additions & 3 deletions src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Console\Descriptor;

use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
Expand All @@ -20,10 +21,10 @@
*/
class JsonDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
protected function describeDefaults(array $options)
{
$data['builtin_form_types'] = $this->getCoreTypes();
$data['service_form_types'] = array_values(array_diff($options['types'], $data['builtin_form_types']));
$data['builtin_form_types'] = $options['core_types'];
$data['service_form_types'] = $options['service_types'];
$data['type_extensions'] = $options['extensions'];
$data['type_guessers'] = $options['guessers'];

Expand Down Expand Up @@ -54,6 +55,30 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF
$this->writeData($data, $options);
}

protected function describeOption(OptionsResolver $optionsResolver, array $options)
{
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);

$map = array(
'required' => 'required',
'default' => 'default',
'allowed_types' => 'allowedTypes',
'allowed_values' => 'allowedValues',
);
foreach ($map as $label => $name) {
if (array_key_exists($name, $definition)) {
$data[$label] = $definition[$name];

if ('default' === $name) {
$data['is_lazy'] = isset($definition['lazy']);
}
}
}
$data['has_normalizer'] = isset($definition['normalizer']);

$this->writeData($data, $options);
}

private function writeData(array $data, array $options)
{
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;
Expand Down
60 changes: 55 additions & 5 deletions src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php
Expand Up @@ -13,6 +13,10 @@

use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;

/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
Expand All @@ -21,18 +25,16 @@
*/
class TextDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
protected function describeDefaults(array $options)
{
$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);
$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $options['core_types']);
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->listing($options['service_types']);

$this->output->section('Type extensions');
$this->output->listing($options['extensions']);
Expand Down Expand Up @@ -94,6 +96,34 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF
}
}

protected function describeOption(OptionsResolver $optionsResolver, array $options)
{
$definition = $this->getOptionDefinition($optionsResolver, $options['option']);

$dump = $this->getDumpFunction();
$map = array(
'Required' => 'required',
'Default' => 'default',
'Allowed types' => 'allowedTypes',
'Allowed values' => 'allowedValues',
'Normalizer' => 'normalizer',
);
$rows = array();
foreach ($map as $label => $name) {
$value = array_key_exists($name, $definition) ? $dump($definition[$name]) : '-';
if ('default' === $name && isset($definition['lazy'])) {
$value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']);
}

$rows[] = array("<info>$label</info>", $value);
$rows[] = new TableSeparator();
}
array_pop($rows);

$this->output->title(sprintf('%s (%s)', get_class($options['type']), $options['option']));
$this->output->table(array(), $rows);
}

private function normalizeAndSortOptionsColumns(array $options)
{
foreach ($options as $group => &$opts) {
Expand Down Expand Up @@ -125,4 +155,24 @@ private function normalizeAndSortOptionsColumns(array $options)

return $options;
}

private function getDumpFunction()
{
$cloner = new VarCloner();
$cloner->addCasters(array('Closure' => function ($c, $a) {
$prefix = Caster::PREFIX_VIRTUAL;

return array(
$prefix.'parameters' => isset($a[$prefix.'parameters']) ? count($a[$prefix.'parameters']->value) : 0,
$prefix.'file' => $a[$prefix.'file'],
$prefix.'line' => $a[$prefix.'line'],
);
}));
$dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
$dumper->setColors($this->output->isDecorated());

return function ($value) use ($dumper, $cloner) {
return rtrim($dumper->dump($cloner->cloneVar($value)->withRefHandles(false), true));
};
}
}

0 comments on commit 61cda3e

Please sign in to comment.