Skip to content

Commit bf17bcb

Browse files
committed
Add debug:form command
1 parent d14359a commit bf17bcb

File tree

15 files changed

+908
-4
lines changed

15 files changed

+908
-4
lines changed

Command/DebugCommand.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputArgument;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Console\Style\SymfonyStyle;
20+
use Symfony\Component\Form\Console\Helper\DescriptorHelper;
21+
use Symfony\Component\Form\FormRegistryInterface;
22+
23+
/**
24+
* A console command for retrieving information about form types.
25+
*
26+
* @author Yonel Ceruto <[email protected]>
27+
*/
28+
class DebugCommand extends Command
29+
{
30+
protected static $defaultName = 'debug:form';
31+
32+
private $formRegistry;
33+
private $namespaces;
34+
35+
public function __construct(FormRegistryInterface $formRegistry, array $namespaces = array('Symfony\Component\Form\Extension\Core\Type'))
36+
{
37+
parent::__construct();
38+
39+
$this->formRegistry = $formRegistry;
40+
$this->namespaces = $namespaces;
41+
}
42+
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
protected function configure()
47+
{
48+
$this
49+
->setDefinition(array(
50+
new InputArgument('class', InputArgument::REQUIRED, 'The form type class'),
51+
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
52+
))
53+
->setDescription('Displays form type information')
54+
;
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
protected function execute(InputInterface $input, OutputInterface $output)
61+
{
62+
$io = new SymfonyStyle($input, $output);
63+
64+
if (!class_exists($class = $input->getArgument('class'))) {
65+
$class = $this->getFqcnTypeClass($input, $io, $class);
66+
}
67+
68+
$object = $this->formRegistry->getType($class);
69+
70+
$helper = new DescriptorHelper();
71+
$options['format'] = $input->getOption('format');
72+
$helper->describe($io, $object, $options);
73+
}
74+
75+
private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName)
76+
{
77+
$classes = array();
78+
foreach ($this->namespaces as $namespace) {
79+
if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
80+
$classes[] = $fqcn;
81+
}
82+
}
83+
84+
if (0 === $count = count($classes)) {
85+
throw new \InvalidArgumentException(sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)));
86+
}
87+
if (1 === $count) {
88+
return $classes[0];
89+
}
90+
if (!$input->isInteractive()) {
91+
throw new \InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes)));
92+
}
93+
94+
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]);
95+
}
96+
}

Console/Descriptor/Descriptor.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Console\Descriptor;
13+
14+
use Symfony\Component\Console\Descriptor\DescriptorInterface;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
use Symfony\Component\Console\Style\SymfonyStyle;
17+
use Symfony\Component\Form\ResolvedFormTypeInterface;
18+
use Symfony\Component\Form\Util\OptionsResolverWrapper;
19+
use Symfony\Component\OptionsResolver\OptionsResolver;
20+
21+
/**
22+
* @author Yonel Ceruto <[email protected]>
23+
*
24+
* @internal
25+
*/
26+
abstract class Descriptor implements DescriptorInterface
27+
{
28+
/**
29+
* @var SymfonyStyle
30+
*/
31+
protected $output;
32+
protected $type;
33+
protected $ownOptions = array();
34+
protected $overriddenOptions = array();
35+
protected $parentOptions = array();
36+
protected $extensionOptions = array();
37+
protected $requiredOptions = array();
38+
protected $parents = array();
39+
protected $extensions = array();
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function describe(OutputInterface $output, $object, array $options = array())
45+
{
46+
$this->output = $output;
47+
48+
switch (true) {
49+
case $object instanceof ResolvedFormTypeInterface:
50+
$this->describeResolvedFormType($object, $options);
51+
break;
52+
default:
53+
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
54+
}
55+
}
56+
57+
abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array());
58+
59+
protected function collectOptions(ResolvedFormTypeInterface $type)
60+
{
61+
$this->parents = array();
62+
$this->extensions = array();
63+
64+
if (null !== $type->getParent()) {
65+
$optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
66+
} else {
67+
$optionsResolver = new OptionsResolver();
68+
}
69+
70+
$type->getInnerType()->configureOptions($ownOptionsResolver = new OptionsResolverWrapper());
71+
$this->ownOptions = array_diff($ownOptionsResolver->getDefinedOptions(), $optionsResolver->getDefinedOptions());
72+
$overriddenOptions = array_intersect(array_merge($ownOptionsResolver->getDefinedOptions(), $ownOptionsResolver->getUndefinedOptions()), $optionsResolver->getDefinedOptions());
73+
74+
$this->parentOptions = array();
75+
foreach ($this->parents as $class => $parentOptions) {
76+
$this->overriddenOptions[$class] = array_intersect($overriddenOptions, $parentOptions);
77+
$this->parentOptions[$class] = array_diff($parentOptions, $overriddenOptions);
78+
}
79+
80+
$type->getInnerType()->configureOptions($optionsResolver);
81+
$this->collectTypeExtensionsOptions($type, $optionsResolver);
82+
$this->extensionOptions = array();
83+
foreach ($this->extensions as $class => $extensionOptions) {
84+
$this->overriddenOptions[$class] = array_intersect($overriddenOptions, $extensionOptions);
85+
$this->extensionOptions[$class] = array_diff($extensionOptions, $overriddenOptions);
86+
}
87+
88+
$this->overriddenOptions = array_filter($this->overriddenOptions);
89+
$this->requiredOptions = $optionsResolver->getRequiredOptions();
90+
91+
$this->parents = array_keys($this->parents);
92+
$this->extensions = array_keys($this->extensions);
93+
}
94+
95+
private function getParentOptionsResolver(ResolvedFormTypeInterface $type)
96+
{
97+
$this->parents[$class = get_class($type->getInnerType())] = array();
98+
99+
if (null !== $type->getParent()) {
100+
$optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
101+
} else {
102+
$optionsResolver = new OptionsResolver();
103+
}
104+
105+
$inheritedOptions = $optionsResolver->getDefinedOptions();
106+
$type->getInnerType()->configureOptions($optionsResolver);
107+
$this->parents[$class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
108+
109+
$this->collectTypeExtensionsOptions($type, $optionsResolver);
110+
111+
return $optionsResolver;
112+
}
113+
114+
private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver)
115+
{
116+
foreach ($type->getTypeExtensions() as $extension) {
117+
$inheritedOptions = $optionsResolver->getDefinedOptions();
118+
$extension->configureOptions($optionsResolver);
119+
$this->extensions[get_class($extension)] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
120+
}
121+
}
122+
}

Console/Descriptor/JsonDescriptor.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Console\Descriptor;
13+
14+
use Symfony\Component\Form\ResolvedFormTypeInterface;
15+
16+
/**
17+
* @author Yonel Ceruto <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
class JsonDescriptor extends Descriptor
22+
{
23+
protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
24+
{
25+
$this->collectOptions($resolvedFormType);
26+
27+
$formOptions = array(
28+
'own' => $this->ownOptions,
29+
'overridden' => $this->overriddenOptions,
30+
'parent' => $this->parentOptions,
31+
'extension' => $this->extensionOptions,
32+
'required' => $this->requiredOptions,
33+
);
34+
$this->sortOptions($formOptions);
35+
36+
$data = array(
37+
'class' => get_class($resolvedFormType->getInnerType()),
38+
'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(),
39+
'options' => $formOptions,
40+
'parent_types' => $this->parents,
41+
'type_extensions' => $this->extensions,
42+
);
43+
44+
$this->writeData($data, $options);
45+
}
46+
47+
private function writeData(array $data, array $options)
48+
{
49+
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;
50+
$this->output->write(json_encode($data, $flags | JSON_PRETTY_PRINT)."\n");
51+
}
52+
53+
private function sortOptions(array &$options)
54+
{
55+
foreach ($options as &$opts) {
56+
$sorted = false;
57+
foreach ($opts as &$opt) {
58+
if (is_array($opt)) {
59+
sort($opt);
60+
$sorted = true;
61+
}
62+
}
63+
if (!$sorted) {
64+
sort($opts);
65+
}
66+
}
67+
}
68+
}

Console/Descriptor/TextDescriptor.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Console\Descriptor;
13+
14+
use Symfony\Component\Console\Helper\TableSeparator;
15+
use Symfony\Component\Form\ResolvedFormTypeInterface;
16+
17+
/**
18+
* @author Yonel Ceruto <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
class TextDescriptor extends Descriptor
23+
{
24+
protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
25+
{
26+
$this->collectOptions($resolvedFormType);
27+
28+
$formOptions = $this->normalizeAndSortOptionsColumns(array_filter(array(
29+
'own' => $this->ownOptions,
30+
'overridden' => $this->overriddenOptions,
31+
'parent' => $this->parentOptions,
32+
'extension' => $this->extensionOptions,
33+
)));
34+
35+
// setting headers and column order
36+
$tableHeaders = array_intersect_key(array(
37+
'own' => 'Options',
38+
'overridden' => 'Overridden options',
39+
'parent' => 'Parent options',
40+
'extension' => 'Extension options',
41+
), $formOptions);
42+
43+
$tableRows = array();
44+
$count = count(max($formOptions));
45+
for ($i = 0; $i < $count; ++$i) {
46+
$cells = array();
47+
foreach (array_keys($tableHeaders) as $group) {
48+
if (isset($formOptions[$group][$i])) {
49+
$option = $formOptions[$group][$i];
50+
51+
if (is_string($option) && in_array($option, $this->requiredOptions)) {
52+
$option .= ' <info>(required)</info>';
53+
}
54+
55+
$cells[] = $option;
56+
} else {
57+
$cells[] = null;
58+
}
59+
}
60+
$tableRows[] = $cells;
61+
}
62+
63+
$this->output->title(sprintf('%s (Block prefix: "%s")', get_class($resolvedFormType->getInnerType()), $resolvedFormType->getInnerType()->getBlockPrefix()));
64+
$this->output->table($tableHeaders, $tableRows);
65+
66+
if ($this->parents) {
67+
$this->output->section('Parent types');
68+
$this->output->listing($this->parents);
69+
}
70+
71+
if ($this->extensions) {
72+
$this->output->section('Type extensions');
73+
$this->output->listing($this->extensions);
74+
}
75+
}
76+
77+
private function normalizeAndSortOptionsColumns(array $options)
78+
{
79+
foreach ($options as $group => &$opts) {
80+
$sorted = false;
81+
foreach ($opts as $class => $opt) {
82+
if (!is_array($opt) || 0 === count($opt)) {
83+
continue;
84+
}
85+
86+
unset($opts[$class]);
87+
88+
if (!$sorted) {
89+
$opts = array();
90+
} else {
91+
$opts[] = null;
92+
}
93+
$opts[] = sprintf('<info>%s</info>', (new \ReflectionClass($class))->getShortName());
94+
$opts[] = new TableSeparator();
95+
96+
sort($opt);
97+
$sorted = true;
98+
$opts = array_merge($opts, $opt);
99+
}
100+
101+
if (!$sorted) {
102+
sort($opts);
103+
}
104+
}
105+
106+
return $options;
107+
}
108+
}

0 commit comments

Comments
 (0)