diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index b386d4c84493..f54b74abb605 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -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);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
index 4bdaa85fee4c..80bc32d3ff22 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
@@ -96,5 +96,11 @@
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 7ef0dc842b40..d70292ff7bf6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -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",
diff --git a/src/Symfony/Component/Form/Command/DebugCommand.php b/src/Symfony/Component/Form/Command/DebugCommand.php
new file mode 100644
index 000000000000..3cb4904ab3a7
--- /dev/null
+++ b/src/Symfony/Component/Form/Command/DebugCommand.php
@@ -0,0 +1,96 @@
+
+ *
+ * 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
+ */
+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]);
+ }
+}
diff --git a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
new file mode 100644
index 000000000000..af77f1993124
--- /dev/null
+++ b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php
@@ -0,0 +1,122 @@
+
+ *
+ * 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
+ *
+ * @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);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php
new file mode 100644
index 000000000000..638ea7a5ff71
--- /dev/null
+++ b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php
@@ -0,0 +1,68 @@
+
+ *
+ * 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
+ *
+ * @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);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php
new file mode 100644
index 000000000000..b12a22bb9e26
--- /dev/null
+++ b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php
@@ -0,0 +1,108 @@
+
+ *
+ * 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\Helper\TableSeparator;
+use Symfony\Component\Form\ResolvedFormTypeInterface;
+
+/**
+ * @author Yonel Ceruto
+ *
+ * @internal
+ */
+class TextDescriptor extends Descriptor
+{
+ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = array())
+ {
+ $this->collectOptions($resolvedFormType);
+
+ $formOptions = $this->normalizeAndSortOptionsColumns(array_filter(array(
+ 'own' => $this->ownOptions,
+ 'overridden' => $this->overriddenOptions,
+ 'parent' => $this->parentOptions,
+ 'extension' => $this->extensionOptions,
+ )));
+
+ // setting headers and column order
+ $tableHeaders = array_intersect_key(array(
+ 'own' => 'Options',
+ 'overridden' => 'Overridden options',
+ 'parent' => 'Parent options',
+ 'extension' => 'Extension options',
+ ), $formOptions);
+
+ $tableRows = array();
+ $count = count(max($formOptions));
+ for ($i = 0; $i < $count; ++$i) {
+ $cells = array();
+ foreach (array_keys($tableHeaders) as $group) {
+ if (isset($formOptions[$group][$i])) {
+ $option = $formOptions[$group][$i];
+
+ if (is_string($option) && in_array($option, $this->requiredOptions)) {
+ $option .= ' (required)';
+ }
+
+ $cells[] = $option;
+ } else {
+ $cells[] = null;
+ }
+ }
+ $tableRows[] = $cells;
+ }
+
+ $this->output->title(sprintf('%s (Block prefix: "%s")', get_class($resolvedFormType->getInnerType()), $resolvedFormType->getInnerType()->getBlockPrefix()));
+ $this->output->table($tableHeaders, $tableRows);
+
+ if ($this->parents) {
+ $this->output->section('Parent types');
+ $this->output->listing($this->parents);
+ }
+
+ if ($this->extensions) {
+ $this->output->section('Type extensions');
+ $this->output->listing($this->extensions);
+ }
+ }
+
+ private function normalizeAndSortOptionsColumns(array $options)
+ {
+ foreach ($options as $group => &$opts) {
+ $sorted = false;
+ foreach ($opts as $class => $opt) {
+ if (!is_array($opt) || 0 === count($opt)) {
+ continue;
+ }
+
+ unset($opts[$class]);
+
+ if (!$sorted) {
+ $opts = array();
+ } else {
+ $opts[] = null;
+ }
+ $opts[] = sprintf('%s', (new \ReflectionClass($class))->getShortName());
+ $opts[] = new TableSeparator();
+
+ sort($opt);
+ $sorted = true;
+ $opts = array_merge($opts, $opt);
+ }
+
+ if (!$sorted) {
+ sort($opts);
+ }
+ }
+
+ return $options;
+ }
+}
diff --git a/src/Symfony/Component/Form/Console/Helper/DescriptorHelper.php b/src/Symfony/Component/Form/Console/Helper/DescriptorHelper.php
new file mode 100644
index 000000000000..e850324c0171
--- /dev/null
+++ b/src/Symfony/Component/Form/Console/Helper/DescriptorHelper.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Console\Helper;
+
+use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
+use Symfony\Component\Form\Console\Descriptor\JsonDescriptor;
+use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
+
+/**
+ * @author Yonel Ceruto
+ *
+ * @internal
+ */
+class DescriptorHelper extends BaseDescriptorHelper
+{
+ public function __construct()
+ {
+ $this
+ ->register('txt', new TextDescriptor())
+ ->register('json', new JsonDescriptor())
+ ;
+ }
+}
diff --git a/src/Symfony/Component/Form/DependencyInjection/FormPass.php b/src/Symfony/Component/Form/DependencyInjection/FormPass.php
index 88574558b7b7..ff1ac8af6065 100644
--- a/src/Symfony/Component/Form/DependencyInjection/FormPass.php
+++ b/src/Symfony/Component/Form/DependencyInjection/FormPass.php
@@ -18,6 +18,7 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\Form\Command\DebugCommand;
/**
* Adds all services with the tags "form.type", "form.type_extension" and
@@ -33,13 +34,15 @@ class FormPass implements CompilerPassInterface
private $formTypeTag;
private $formTypeExtensionTag;
private $formTypeGuesserTag;
+ private $formDebugCommandService;
- public function __construct($formExtensionService = 'form.extension', $formTypeTag = 'form.type', $formTypeExtensionTag = 'form.type_extension', $formTypeGuesserTag = 'form.type_guesser')
+ public function __construct($formExtensionService = 'form.extension', $formTypeTag = 'form.type', $formTypeExtensionTag = 'form.type_extension', $formTypeGuesserTag = 'form.type_guesser', $formDebugCommandService = DebugCommand::class)
{
$this->formExtensionService = $formExtensionService;
$this->formTypeTag = $formTypeTag;
$this->formTypeExtensionTag = $formTypeExtensionTag;
$this->formTypeGuesserTag = $formTypeGuesserTag;
+ $this->formDebugCommandService = $formDebugCommandService;
}
public function process(ContainerBuilder $container)
@@ -61,12 +64,19 @@ private function processFormTypes(ContainerBuilder $container)
{
// Get service locator argument
$servicesMap = array();
+ $namespaces = array('Symfony\Component\Form\Extension\Core\Type' => true);
// Builds an array with fully-qualified type class names as keys and service IDs as values
foreach ($container->findTaggedServiceIds($this->formTypeTag, true) as $serviceId => $tag) {
// Add form type service to the service locator
$serviceDefinition = $container->getDefinition($serviceId);
- $servicesMap[$serviceDefinition->getClass()] = new Reference($serviceId);
+ $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId);
+ $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true;
+ }
+
+ if ($container->hasDefinition($this->formDebugCommandService)) {
+ $commandDefinition = $container->getDefinition($this->formDebugCommandService);
+ $commandDefinition->setArgument(1, array_keys($namespaces));
}
return ServiceLocatorTagPass::register($container, $servicesMap);
diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php
new file mode 100644
index 000000000000..c2083ea12ce4
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Command;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Form\Command\DebugCommand;
+use Symfony\Component\Form\Extension\Core\Type\FormType;
+use Symfony\Component\Form\FormRegistryInterface;
+use Symfony\Component\Form\ResolvedFormTypeInterface;
+
+class DebugCommandTest extends TestCase
+{
+ public function testDebugSingleFormType()
+ {
+ $tester = $this->createCommandTester();
+ $ret = $tester->execute(array('class' => 'FormType'), array('decorated' => false));
+
+ $this->assertEquals(0, $ret, 'Returns 0 in case of success');
+ $this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")', $tester->getDisplay());
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testDebugInvalidFormType()
+ {
+ $this->createCommandTester()->execute(array('class' => 'test'));
+ }
+
+ /**
+ * @return CommandTester
+ */
+ private function createCommandTester()
+ {
+ $resolvedFormType = $this->getMockBuilder(ResolvedFormTypeInterface::class)->getMock();
+ $resolvedFormType
+ ->expects($this->any())
+ ->method('getParent')
+ ->willReturn(null)
+ ;
+ $resolvedFormType
+ ->expects($this->any())
+ ->method('getInnerType')
+ ->willReturn(new FormType())
+ ;
+ $resolvedFormType
+ ->expects($this->any())
+ ->method('getTypeExtensions')
+ ->willReturn(array())
+ ;
+
+ $formRegistry = $this->getMockBuilder(FormRegistryInterface::class)->getMock();
+ $formRegistry
+ ->expects($this->any())
+ ->method('getType')
+ ->will($this->returnValue($resolvedFormType))
+ ;
+
+ $command = new DebugCommand($formRegistry);
+ $application = new Application();
+ $application->add($command);
+
+ return new CommandTester($application->find('debug:form'));
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php
new file mode 100644
index 000000000000..c760e5ecf972
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Console\Descriptor;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Output\BufferedOutput;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\Form\Extension\Core\Type\FormType;
+use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension;
+use Symfony\Component\Form\ResolvedFormType;
+use Symfony\Component\Form\ResolvedFormTypeInterface;
+use Symfony\Component\Security\Csrf\CsrfTokenManager;
+
+abstract class AbstractDescriptorTest extends TestCase
+{
+ /** @dataProvider getDescribeResolvedFormTypeTestData */
+ public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, array $options, $fixtureName)
+ {
+ $expectedDescription = $this->getExpectedDescription($fixtureName);
+ $describedObject = $this->getObjectDescription($type, $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)));
+ }
+ }
+
+ public function getDescribeResolvedFormTypeTestData()
+ {
+ $typeExtensions = array(
+ new FormTypeCsrfExtension(new CsrfTokenManager()),
+ );
+ $parent = new ResolvedFormType(new FormType(), $typeExtensions);
+
+ yield array(new ResolvedFormType(new ChoiceType(), array(), $parent), array(), 'resolved_form_type_1');
+ }
+
+ abstract protected function getDescriptor();
+
+ abstract protected function getFormat();
+
+ private function getObjectDescription($object, array $options = array())
+ {
+ $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true);
+ $io = new SymfonyStyle(new ArrayInput(array()), $output);
+
+ $this->getDescriptor()->describe($io, $object, $options);
+
+ return $output->fetch();
+ }
+
+ private function getExpectedDescription($name)
+ {
+ return file_get_contents($this->getFixtureFilename($name));
+ }
+
+ private function getFixtureFilename($name)
+ {
+ return sprintf('%s/../../Fixtures/Descriptor/%s.%s', __DIR__, $name, $this->getFormat());
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php
new file mode 100644
index 000000000000..fb339f6b475e
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Console\Descriptor;
+
+use Symfony\Component\Form\Console\Descriptor\JsonDescriptor;
+
+class JsonDescriptorTest extends AbstractDescriptorTest
+{
+ protected function setUp()
+ {
+ putenv('COLUMNS=121');
+ }
+
+ protected function tearDown()
+ {
+ putenv('COLUMNS');
+ }
+
+ protected function getDescriptor()
+ {
+ return new JsonDescriptor();
+ }
+
+ protected function getFormat()
+ {
+ return 'json';
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php
new file mode 100644
index 000000000000..053f7e451234
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Console\Descriptor;
+
+use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
+
+class TextDescriptorTest extends AbstractDescriptorTest
+{
+ protected function setUp()
+ {
+ putenv('COLUMNS=121');
+ }
+
+ protected function tearDown()
+ {
+ putenv('COLUMNS');
+ }
+
+ protected function getDescriptor()
+ {
+ return new TextDescriptor();
+ }
+
+ protected function getFormat()
+ {
+ return 'txt';
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php
index acfed570bce9..4e9971ea8ccf 100644
--- a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php
+++ b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php
@@ -14,12 +14,14 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
+use Symfony\Component\Form\Command\DebugCommand;
use Symfony\Component\Form\DependencyInjection\FormPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormRegistryInterface;
/**
* @author Bernhard Schussek
@@ -35,6 +37,15 @@ public function testDoNothingIfFormExtensionNotLoaded()
$this->assertFalse($container->hasDefinition('form.extension'));
}
+ public function testDoNothingIfDebugCommandNotLoaded()
+ {
+ $container = $this->createContainerBuilder();
+
+ $container->compile();
+
+ $this->assertFalse($container->hasDefinition(DebugCommand::class));
+ }
+
public function testAddTaggedTypes()
{
$container = $this->createContainerBuilder();
@@ -56,6 +67,28 @@ public function testAddTaggedTypes()
);
}
+ public function testAddTaggedTypesToDebugCommand()
+ {
+ $container = $this->createContainerBuilder();
+
+ $container->setDefinition('form.extension', $this->createExtensionDefinition());
+ $container->setDefinition(DebugCommand::class, $this->createDebugCommandDefinition());
+ $container->register('my.type1', __CLASS__.'_Type1')->addTag('form.type');
+ $container->register('my.type2', __CLASS__.'_Type2')->addTag('form.type');
+
+ $container->compile();
+
+ $cmdDefinition = $container->getDefinition(DebugCommand::class);
+
+ $this->assertEquals(
+ array(
+ 'Symfony\Component\Form\Extension\Core\Type',
+ __NAMESPACE__,
+ ),
+ $cmdDefinition->getArgument(1)
+ );
+ }
+
/**
* @dataProvider addTaggedTypeExtensionsDataProvider
*/
@@ -225,6 +258,18 @@ private function createExtensionDefinition()
return $definition;
}
+ private function createDebugCommandDefinition()
+ {
+ $definition = new Definition('Symfony\Component\Form\Command\DebugCommand');
+ $definition->setArguments(array(
+ $formRegistry = $this->getMockBuilder(FormRegistryInterface::class)->getMock(),
+ array(),
+ array('Symfony\Component\Form\Extension\Core\Type'),
+ ));
+
+ return $definition;
+ }
+
private function createContainerBuilder()
{
$container = new ContainerBuilder();
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json
new file mode 100644
index 000000000000..b0c083b5b71d
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json
@@ -0,0 +1,68 @@
+{
+ "class": "Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType",
+ "block_prefix": "choice",
+ "options": {
+ "own": [
+ "choice_attr",
+ "choice_label",
+ "choice_loader",
+ "choice_name",
+ "choice_translation_domain",
+ "choice_value",
+ "choices",
+ "choices_as_values",
+ "expanded",
+ "group_by",
+ "multiple",
+ "placeholder",
+ "preferred_choices"
+ ],
+ "overridden": {
+ "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType": [
+ "compound",
+ "data_class",
+ "empty_data",
+ "error_bubbling"
+ ]
+ },
+ "parent": {
+ "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType": [
+ "action",
+ "attr",
+ "auto_initialize",
+ "block_name",
+ "by_reference",
+ "data",
+ "disabled",
+ "inherit_data",
+ "label",
+ "label_attr",
+ "label_format",
+ "mapped",
+ "method",
+ "post_max_size_message",
+ "property_path",
+ "required",
+ "translation_domain",
+ "trim",
+ "upload_max_size_message"
+ ]
+ },
+ "extension": {
+ "Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension": [
+ "csrf_field_name",
+ "csrf_message",
+ "csrf_protection",
+ "csrf_token_id",
+ "csrf_token_manager"
+ ]
+ },
+ "required": []
+ },
+ "parent_types": [
+ "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType"
+ ],
+ "type_extensions": [
+ "Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension"
+ ]
+}
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt
new file mode 100644
index 000000000000..5f839b85ac6b
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt
@@ -0,0 +1,40 @@
+
+[33mSymfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice")[39m
+[33m==============================================================================[39m
+
+ --------------------------- -------------------- ------------------------- -----------------------
+ [32m Options [39m [32m Overridden options [39m [32m Parent options [39m [32m Extension options [39m
+ --------------------------- -------------------- ------------------------- -----------------------
+ choice_attr [32mFormType[39m [32mFormType[39m [32mFormTypeCsrfExtension[39m
+ choice_label -------------------- ------------------------- -----------------------
+ choice_loader compound action csrf_field_name
+ choice_name data_class attr csrf_message
+ choice_translation_domain empty_data auto_initialize csrf_protection
+ choice_value error_bubbling block_name csrf_token_id
+ choices by_reference csrf_token_manager
+ choices_as_values data
+ expanded disabled
+ group_by inherit_data
+ multiple label
+ placeholder label_attr
+ preferred_choices label_format
+ mapped
+ method
+ post_max_size_message
+ property_path
+ required
+ translation_domain
+ trim
+ upload_max_size_message
+ --------------------------- -------------------- ------------------------- -----------------------
+
+[33mParent types[39m
+[33m------------[39m
+
+ * Symfony\Component\Form\Extension\Core\Type\FormType
+
+[33mType extensions[39m
+[33m---------------[39m
+
+ * Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension
+
diff --git a/src/Symfony/Component/Form/Util/OptionsResolverWrapper.php b/src/Symfony/Component/Form/Util/OptionsResolverWrapper.php
new file mode 100644
index 000000000000..94a4fc111abc
--- /dev/null
+++ b/src/Symfony/Component/Form/Util/OptionsResolverWrapper.php
@@ -0,0 +1,91 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Util;
+
+use Symfony\Component\OptionsResolver\Exception\AccessException;
+use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * @author Yonel Ceruto
+ *
+ * @internal
+ */
+class OptionsResolverWrapper extends OptionsResolver
+{
+ private $undefined = array();
+
+ public function setNormalizer($option, \Closure $normalizer)
+ {
+ try {
+ parent::setNormalizer($option, $normalizer);
+ } catch (UndefinedOptionsException $e) {
+ $this->undefined[$option] = true;
+ }
+
+ return $this;
+ }
+
+ public function setAllowedValues($option, $allowedValues)
+ {
+ try {
+ parent::setAllowedValues($option, $allowedValues);
+ } catch (UndefinedOptionsException $e) {
+ $this->undefined[$option] = true;
+ }
+
+ return $this;
+ }
+
+ public function addAllowedValues($option, $allowedValues)
+ {
+ try {
+ parent::addAllowedValues($option, $allowedValues);
+ } catch (UndefinedOptionsException $e) {
+ $this->undefined[$option] = true;
+ }
+
+ return $this;
+ }
+
+ public function setAllowedTypes($option, $allowedTypes)
+ {
+ try {
+ parent::setAllowedTypes($option, $allowedTypes);
+ } catch (UndefinedOptionsException $e) {
+ $this->undefined[$option] = true;
+ }
+
+ return $this;
+ }
+
+ public function addAllowedTypes($option, $allowedTypes)
+ {
+ try {
+ parent::addAllowedTypes($option, $allowedTypes);
+ } catch (UndefinedOptionsException $e) {
+ $this->undefined[$option] = true;
+ }
+
+ return $this;
+ }
+
+ public function resolve(array $options = array())
+ {
+ throw new AccessException('Resolve options is not supported.');
+ }
+
+ public function getUndefinedOptions()
+ {
+ return array_keys($this->undefined);
+ }
+}
diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json
index 0bc08d66a7c8..77fc3e565ffa 100644
--- a/src/Symfony/Component/Form/composer.json
+++ b/src/Symfony/Component/Form/composer.json
@@ -19,7 +19,7 @@
"php": "^5.5.9|>=7.0.8",
"symfony/event-dispatcher": "~2.8|~3.0|~4.0",
"symfony/intl": "^2.8.18|^3.2.5|~4.0",
- "symfony/options-resolver": "~2.8|~3.0|~4.0",
+ "symfony/options-resolver": "~3.4|~4.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/property-access": "~2.8|~3.0|~4.0"
},
@@ -32,7 +32,8 @@
"symfony/http-kernel": "^3.3.5|~4.0",
"symfony/security-csrf": "~2.8|~3.0|~4.0",
"symfony/translation": "~2.8|~3.0|~4.0",
- "symfony/var-dumper": "~3.3|~4.0"
+ "symfony/var-dumper": "~3.3|~4.0",
+ "symfony/console": "~3.4|~4.0"
},
"conflict": {
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",