Skip to content

Commit

Permalink
feature #23694 [Form] Add debug:form command (yceruto)
Browse files Browse the repository at this point in the history
This PR was merged into the 3.4 branch.

Discussion
----------

[Form] Add debug:form command

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #23688
| License       | MIT
| Doc PR        | -

![debug-form](https://user-images.githubusercontent.com/2028198/29007125-c3508cd6-7aca-11e7-91e2-c2b509847db5.png)

A short class name (e.g. `DateType`) can be passed as `class` argument too (the command will try to resolve its FQCN if it's in known form type namespaces).

Commits
-------

4f040d7 Add debug:form command
  • Loading branch information
ogizanagi committed Aug 30, 2017
2 parents b3d31e9 + 4f040d7 commit dd3276c
Show file tree
Hide file tree
Showing 18 changed files with 917 additions and 5 deletions.
Expand Up @@ -220,6 +220,8 @@ public function load(array $configs, ContainerBuilder $container)
if (!class_exists('Symfony\Component\Validator\Validation')) {
throw new LogicException('The Validator component is required to use the Form component.');
}
} else {
$container->removeDefinition('Symfony\Component\Form\Command\DebugCommand');
}

$this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader);
Expand Down
Expand Up @@ -96,5 +96,11 @@
<service id="Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand">
<tag name="console.command" command="lint:yaml" />
</service>

<service id="Symfony\Component\Form\Command\DebugCommand">
<argument type="service" id="form.registry" />
<argument type="collection" /> <!-- All form types namespaces are stored here by FormPass -->
<tag name="console.command" command="debug:form" />
</service>
</services>
</container>
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -40,7 +40,7 @@
"symfony/dom-crawler": "~2.8|~3.0|~4.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/security": "~2.8|~3.0|~4.0",
"symfony/form": "~3.3|~4.0",
"symfony/form": "~3.4|~4.0",
"symfony/expression-language": "~2.8|~3.0|~4.0",
"symfony/process": "~2.8|~3.0|~4.0",
"symfony/security-core": "~3.2|~4.0",
Expand Down
96 changes: 96 additions & 0 deletions src/Symfony/Component/Form/Command/DebugCommand.php
@@ -0,0 +1,96 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Form\Console\Helper\DescriptorHelper;
use Symfony\Component\Form\FormRegistryInterface;

/**
* A console command for retrieving information about form types.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class DebugCommand extends Command
{
protected static $defaultName = 'debug:form';

private $formRegistry;
private $namespaces;

public function __construct(FormRegistryInterface $formRegistry, array $namespaces = array('Symfony\Component\Form\Extension\Core\Type'))
{
parent::__construct();

$this->formRegistry = $formRegistry;
$this->namespaces = $namespaces;
}

/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDefinition(array(
new InputArgument('class', InputArgument::REQUIRED, 'The form type class'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
))
->setDescription('Displays form type information')
;
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);

if (!class_exists($class = $input->getArgument('class'))) {
$class = $this->getFqcnTypeClass($input, $io, $class);
}

$object = $this->formRegistry->getType($class);

$helper = new DescriptorHelper();
$options['format'] = $input->getOption('format');
$helper->describe($io, $object, $options);
}

private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName)
{
$classes = array();
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)));
}
if (1 === $count) {
return $classes[0];
}
if (!$input->isInteractive()) {
throw new \InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\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]);
}
}
122 changes: 122 additions & 0 deletions src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
@@ -0,0 +1,122 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Console\Descriptor;

use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\Form\Util\OptionsResolverWrapper;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
abstract class Descriptor implements DescriptorInterface
{
/**
* @var SymfonyStyle
*/
protected $output;
protected $type;
protected $ownOptions = array();
protected $overriddenOptions = array();
protected $parentOptions = array();
protected $extensionOptions = array();
protected $requiredOptions = array();
protected $parents = array();
protected $extensions = array();

/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = array())
{
$this->output = $output;

switch (true) {
case $object instanceof ResolvedFormTypeInterface:
$this->describeResolvedFormType($object, $options);
break;
default:
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
}
}

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

protected function collectOptions(ResolvedFormTypeInterface $type)
{
$this->parents = array();
$this->extensions = array();

if (null !== $type->getParent()) {
$optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
} else {
$optionsResolver = new OptionsResolver();
}

$type->getInnerType()->configureOptions($ownOptionsResolver = new OptionsResolverWrapper());
$this->ownOptions = array_diff($ownOptionsResolver->getDefinedOptions(), $optionsResolver->getDefinedOptions());
$overriddenOptions = array_intersect(array_merge($ownOptionsResolver->getDefinedOptions(), $ownOptionsResolver->getUndefinedOptions()), $optionsResolver->getDefinedOptions());

$this->parentOptions = array();
foreach ($this->parents as $class => $parentOptions) {
$this->overriddenOptions[$class] = array_intersect($overriddenOptions, $parentOptions);
$this->parentOptions[$class] = array_diff($parentOptions, $overriddenOptions);
}

$type->getInnerType()->configureOptions($optionsResolver);
$this->collectTypeExtensionsOptions($type, $optionsResolver);
$this->extensionOptions = array();
foreach ($this->extensions as $class => $extensionOptions) {
$this->overriddenOptions[$class] = array_intersect($overriddenOptions, $extensionOptions);
$this->extensionOptions[$class] = array_diff($extensionOptions, $overriddenOptions);
}

$this->overriddenOptions = array_filter($this->overriddenOptions);
$this->requiredOptions = $optionsResolver->getRequiredOptions();

$this->parents = array_keys($this->parents);
$this->extensions = array_keys($this->extensions);
}

private function getParentOptionsResolver(ResolvedFormTypeInterface $type)
{
$this->parents[$class = get_class($type->getInnerType())] = array();

if (null !== $type->getParent()) {
$optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
} else {
$optionsResolver = new OptionsResolver();
}

$inheritedOptions = $optionsResolver->getDefinedOptions();
$type->getInnerType()->configureOptions($optionsResolver);
$this->parents[$class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);

$this->collectTypeExtensionsOptions($type, $optionsResolver);

return $optionsResolver;
}

private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver)
{
foreach ($type->getTypeExtensions() as $extension) {
$inheritedOptions = $optionsResolver->getDefinedOptions();
$extension->configureOptions($optionsResolver);
$this->extensions[get_class($extension)] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
}
}
}
68 changes: 68 additions & 0 deletions src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php
@@ -0,0 +1,68 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Console\Descriptor;

use Symfony\Component\Form\ResolvedFormTypeInterface;

/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class JsonDescriptor extends Descriptor
{
protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
{
$this->collectOptions($resolvedFormType);

$formOptions = array(
'own' => $this->ownOptions,
'overridden' => $this->overriddenOptions,
'parent' => $this->parentOptions,
'extension' => $this->extensionOptions,
'required' => $this->requiredOptions,
);
$this->sortOptions($formOptions);

$data = array(
'class' => get_class($resolvedFormType->getInnerType()),
'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(),
'options' => $formOptions,
'parent_types' => $this->parents,
'type_extensions' => $this->extensions,
);

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

private function writeData(array $data, array $options)
{
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;
$this->output->write(json_encode($data, $flags | JSON_PRETTY_PRINT)."\n");
}

private function sortOptions(array &$options)
{
foreach ($options as &$opts) {
$sorted = false;
foreach ($opts as &$opt) {
if (is_array($opt)) {
sort($opt);
$sorted = true;
}
}
if (!$sorted) {
sort($opts);
}
}
}
}

0 comments on commit dd3276c

Please sign in to comment.