Skip to content

[Form] Display general forms information on debug:form #24185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Display form defaults on debug:form
  • Loading branch information
yceruto authored and ogizanagi committed Sep 15, 2017
commit 12d1a7f81064c3669368e3b2453be28769c2085f
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
<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 -->
<argument type="collection" /> <!-- All services form types are stored here by FormPass -->
<argument type="collection" /> <!-- All type extensions are stored here by FormPass -->
<argument type="collection" /> <!-- All type guessers are stored here by FormPass -->
<tag name="console.command" command="debug:form" />
</service>
</services>
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

3.4.0
-----

* added `DebugCommand`

3.3.0
-----

Expand Down
40 changes: 30 additions & 10 deletions src/Symfony/Component/Form/Command/DebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -31,13 +32,19 @@ class DebugCommand extends Command

private $formRegistry;
private $namespaces;
private $types;
private $extensions;
private $guessers;

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

$this->formRegistry = $formRegistry;
$this->namespaces = $namespaces;
$this->types = $types;
$this->extensions = $extensions;
$this->guessers = $guessers;
}

/**
Expand All @@ -47,18 +54,25 @@ protected function configure()
{
$this
->setDefinition(array(
new InputArgument('class', InputArgument::REQUIRED, 'The form type class'),
new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
))
->setDescription('Displays form type information')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays information about a form type.
The <info>%command.name%</info> command displays information about form types.

Either the fully-qualified class name or the short class name can be used:
<info>php %command.full_name%</info>

The command lists all built-in types, services types, type extensions and guessers currently available.

<info>php %command.full_name% Symfony\Component\Form\Extension\Core\Type\ChoiceType</info>
<info>php %command.full_name% ChoiceType</info>

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% --format=json</info>

The command lists everything in a machine readable json format.
EOF
)
;
Expand All @@ -71,12 +85,18 @@ 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);
if (null === $class = $input->getArgument('class')) {
$object = null;
$options['types'] = $this->types;
$options['extensions'] = $this->extensions;
$options['guessers'] = $this->guessers;
} else {
if (!class_exists($class)) {
$class = $this->getFqcnTypeClass($input, $io, $class);
}
$object = $this->formRegistry->getType($class);
}

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

$helper = new DescriptorHelper();
$options['format'] = $input->getOption('format');
$helper->describe($io, $object, $options);
Expand All @@ -92,13 +112,13 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor
}

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)));
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)));
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]);
Expand Down
29 changes: 25 additions & 4 deletions src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
namespace Symfony\Component\Form\Console\Descriptor;

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\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\OptionsResolver;
Expand All @@ -25,9 +29,7 @@
*/
abstract class Descriptor implements DescriptorInterface
{
/**
* @var SymfonyStyle
*/
/** @var StyleInterface */
protected $output;
protected $type;
protected $ownOptions = array();
Expand All @@ -43,9 +45,12 @@ abstract class Descriptor implements DescriptorInterface
*/
public function describe(OutputInterface $output, $object, array $options = array())
{
$this->output = $output;
$this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output);

switch (true) {
case null === $object:
$this->describeDefaults($options);
break;
case $object instanceof ResolvedFormTypeInterface:
$this->describeResolvedFormType($object, $options);
break;
Expand All @@ -54,8 +59,24 @@ public function describe(OutputInterface $output, $object, array $options = arra
}
}

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

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;
}

protected function collectOptions(ResolvedFormTypeInterface $type)
{
$this->parents = array();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
*/
class JsonDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
{
$data['builtin_form_types'] = $this->getCoreTypes();
$data['service_form_types'] = array_values(array_diff($options['types'], $data['builtin_form_types']));
$data['type_extensions'] = $options['extensions'];
$data['type_guessers'] = $options['guessers'];

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

protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
{
$this->collectOptions($resolvedFormType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@
*/
class TextDescriptor extends Descriptor
{
protected function describeDefaults(array $options = array())
{
$coreTypes = $this->getCoreTypes();

$this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
Copy link
Contributor

@ro0NL ro0NL Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not bound to OutputInterface but StyleInterface. Are we being pragmatic here? Should we?

$output = $this->output instanceof StyleInterface ? $this->ouput : new SymfonyStyle($this->output);

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we are subject to DescriptorInterface signature.

Copy link
Member Author

@yceruto yceruto Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks!

$shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $coreTypes);
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->section('Type extensions');
$this->output->listing($options['extensions']);

$this->output->section('Type guessers');
$this->output->listing($options['guessers']);
}

protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
{
$this->collectOptions($resolvedFormType);
Expand Down
17 changes: 17 additions & 0 deletions src/Symfony/Component/Form/DependencyInjection/FormPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private function processFormTypes(ContainerBuilder $container)
if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(1, array_keys($namespaces));
$commandDefinition->setArgument(2, array_keys($servicesMap));
}

return ServiceLocatorTagPass::register($container, $servicesMap);
Expand All @@ -85,6 +86,7 @@ private function processFormTypes(ContainerBuilder $container)
private function processFormTypeExtensions(ContainerBuilder $container)
{
$typeExtensions = array();
$typeExtensionsClasses = array();
foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) {
$serviceId = (string) $reference;
$serviceDefinition = $container->getDefinition($serviceId);
Expand All @@ -97,20 +99,35 @@ private function processFormTypeExtensions(ContainerBuilder $container)
}

$typeExtensions[$extendedType][] = new Reference($serviceId);
$typeExtensionsClasses[] = $serviceDefinition->getClass();
}

foreach ($typeExtensions as $extendedType => $extensions) {
$typeExtensions[$extendedType] = new IteratorArgument($extensions);
}

if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(3, $typeExtensionsClasses);
}

return $typeExtensions;
}

private function processFormTypeGuessers(ContainerBuilder $container)
{
$guessers = array();
$guessersClasses = array();
foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag, true) as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);

$serviceDefinition = $container->getDefinition($serviceId);
$guessersClasses[] = $serviceDefinition->getClass();
}

if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(4, $guessersClasses);
}

return new IteratorArgument($guessers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@

class DebugCommandTest extends TestCase
{
public function testDebugDefaults()
{
$tester = $this->createCommandTester();
$ret = $tester->execute(array(), array('decorated' => false));

$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertContains('Built-in form types', $tester->getDisplay());
}

public function testDebugSingleFormType()
{
$tester = $this->createCommandTester();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@

abstract class AbstractDescriptorTest extends TestCase
{
/** @dataProvider getDescribeDefaultsTestData */
public function testDescribeDefaults($object, array $options, $fixtureName)
{
$expectedDescription = $this->getExpectedDescription($fixtureName);
$describedObject = $this->getObjectDescription($object, $options);

if ('json' === $this->getFormat()) {
$this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($describedObject), JSON_PRETTY_PRINT));
} else {
$this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $describedObject)));
}
}

/** @dataProvider getDescribeResolvedFormTypeTestData */
public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, array $options, $fixtureName)
{
Expand All @@ -37,6 +50,15 @@ public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, ar
}
}

public function getDescribeDefaultsTestData()
{
$options['types'] = array('Symfony\Bridge\Doctrine\Form\Type\EntityType');
$options['extensions'] = array('Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension');
$options['guessers'] = array('Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser');

yield array(null, $options, 'defaults_1');
}

public function getDescribeResolvedFormTypeTestData()
{
$typeExtensions = array(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"builtin_form_types": [
"Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\ResetType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType",
"Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType"
],
"service_form_types": [
"Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType"
],
"type_extensions": [
"Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension"
],
"type_guessers": [
"Symfony\\Component\\Form\\Extension\\Validator\\ValidatorTypeGuesser"
]
}
Loading