From 4f040d78fe17be03100c86ad032d2bbdafae3abe Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Thu, 27 Jul 2017 09:45:21 -0400 Subject: [PATCH] Add debug:form command --- .../FrameworkExtension.php | 2 + .../Resources/config/console.xml | 6 + .../Bundle/FrameworkBundle/composer.json | 2 +- .../Component/Form/Command/DebugCommand.php | 96 ++++++++++++++ .../Form/Console/Descriptor/Descriptor.php | 122 ++++++++++++++++++ .../Console/Descriptor/JsonDescriptor.php | 68 ++++++++++ .../Console/Descriptor/TextDescriptor.php | 108 ++++++++++++++++ .../Form/Console/Helper/DescriptorHelper.php | 32 +++++ .../Form/DependencyInjection/FormPass.php | 14 +- .../Form/Tests/Command/DebugCommandTest.php | 76 +++++++++++ .../Descriptor/AbstractDescriptorTest.php | 73 +++++++++++ .../Console/Descriptor/JsonDescriptorTest.php | 37 ++++++ .../Console/Descriptor/TextDescriptorTest.php | 37 ++++++ .../DependencyInjection/FormPassTest.php | 45 +++++++ .../Descriptor/resolved_form_type_1.json | 68 ++++++++++ .../Descriptor/resolved_form_type_1.txt | 40 ++++++ .../Form/Util/OptionsResolverWrapper.php | 91 +++++++++++++ src/Symfony/Component/Form/composer.json | 5 +- 18 files changed, 917 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/Form/Command/DebugCommand.php create mode 100644 src/Symfony/Component/Form/Console/Descriptor/Descriptor.php create mode 100644 src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php create mode 100644 src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php create mode 100644 src/Symfony/Component/Form/Console/Helper/DescriptorHelper.php create mode 100644 src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php create mode 100644 src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt create mode 100644 src/Symfony/Component/Form/Util/OptionsResolverWrapper.php 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 @@ + +Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") +============================================================================== + + --------------------------- -------------------- ------------------------- ----------------------- +  Options   Overridden options   Parent options   Extension options  + --------------------------- -------------------- ------------------------- ----------------------- + choice_attr FormType FormType FormTypeCsrfExtension + 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 + --------------------------- -------------------- ------------------------- ----------------------- + +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/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",