diff --git a/src/Symfony/Component/Form/Command/DebugCommand.php b/src/Symfony/Component/Form/Command/DebugCommand.php index d8e1f5a0789b..68f3bc96f100 100644 --- a/src/Symfony/Component/Form/Command/DebugCommand.php +++ b/src/Symfony/Component/Form/Command/DebugCommand.php @@ -19,7 +19,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Form\Console\Helper\DescriptorHelper; +use Symfony\Component\Form\Extension\Core\CoreExtension; use Symfony\Component\Form\FormRegistryInterface; +use Symfony\Component\Form\FormTypeInterface; /** * A console command for retrieving information about form types. @@ -55,6 +57,7 @@ protected function configure() $this ->setDefinition(array( new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'), + new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'), )) ->setDescription('Displays form type information') @@ -70,6 +73,10 @@ protected function configure() The command lists all defined options that contains the given form type, as well as their parents and type extensions. + php %command.full_name% ChoiceType choice_value + +The command displays the definition of the given option name. + php %command.full_name% --format=json The command lists everything in a machine readable json format. @@ -87,14 +94,42 @@ protected function execute(InputInterface $input, OutputInterface $output) if (null === $class = $input->getArgument('class')) { $object = null; - $options['types'] = $this->types; + $options['core_types'] = $this->getCoreTypes(); + $options['service_types'] = array_values(array_diff($this->types, $options['core_types'])); $options['extensions'] = $this->extensions; $options['guessers'] = $this->guessers; + foreach ($options as $k => $list) { + sort($options[$k]); + } } else { if (!class_exists($class)) { $class = $this->getFqcnTypeClass($input, $io, $class); } - $object = $this->formRegistry->getType($class); + $resolvedType = $this->formRegistry->getType($class); + + if ($option = $input->getArgument('option')) { + $object = $resolvedType->getOptionsResolver(); + + if (!$object->isDefined($option)) { + $message = sprintf('Option "%s" is not defined in "%s".', $option, get_class($resolvedType->getInnerType())); + + if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new InvalidArgumentException($message); + } + + $options['type'] = $resolvedType->getInnerType(); + $options['option'] = $option; + } else { + $object = $resolvedType; + } } $helper = new DescriptorHelper(); @@ -105,6 +140,7 @@ protected function execute(InputInterface $input, OutputInterface $output) private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName) { $classes = array(); + sort($this->namespaces); foreach ($this->namespaces as $namespace) { if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) { $classes[] = $fqcn; @@ -112,7 +148,19 @@ 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))); + $message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)); + + $allTypes = array_merge($this->getCoreTypes(), $this->types); + if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new InvalidArgumentException($message); } if (1 === $count) { return $classes[0]; @@ -121,6 +169,35 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\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]); + return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); + } + + private function getCoreTypes() + { + $coreExtension = new CoreExtension(); + $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); + $loadTypesRefMethod->setAccessible(true); + $coreTypes = $loadTypesRefMethod->invoke($coreExtension); + $coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes); + sort($coreTypes); + + return $coreTypes; + } + + private function findAlternatives($name, array $collection) + { + $alternatives = array(); + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $threshold = 1e3; + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE); + + return array_keys($alternatives); } } diff --git a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php index c72a19d7993f..6cccd8ead235 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php @@ -14,12 +14,12 @@ 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\OutputStyle; 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\Debug\OptionsResolverIntrospector; +use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -29,7 +29,7 @@ */ abstract class Descriptor implements DescriptorInterface { - /** @var StyleInterface */ + /** @var OutputStyle */ protected $output; protected $type; protected $ownOptions = array(); @@ -45,7 +45,7 @@ abstract class Descriptor implements DescriptorInterface */ public function describe(OutputInterface $output, $object, array $options = array()) { - $this->output = $output instanceof StyleInterface ? $output : new SymfonyStyle(new ArrayInput(array()), $output); + $this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput(array()), $output); switch (true) { case null === $object: @@ -54,28 +54,19 @@ public function describe(OutputInterface $output, $object, array $options = arra case $object instanceof ResolvedFormTypeInterface: $this->describeResolvedFormType($object, $options); break; + case $object instanceof OptionsResolver: + $this->describeOption($object, $options); + break; default: throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); } } - abstract protected function describeDefaults(array $options = array()); + abstract protected function describeDefaults(array $options); 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; - } + abstract protected function describeOption(OptionsResolver $optionsResolver, array $options); protected function collectOptions(ResolvedFormTypeInterface $type) { @@ -113,6 +104,31 @@ protected function collectOptions(ResolvedFormTypeInterface $type) $this->extensions = array_keys($this->extensions); } + protected function getOptionDefinition(OptionsResolver $optionsResolver, $option) + { + $definition = array('required' => $optionsResolver->isRequired($option)); + + $introspector = new OptionsResolverIntrospector($optionsResolver); + + $map = array( + 'default' => 'getDefault', + 'lazy' => 'getLazyClosures', + 'allowedTypes' => 'getAllowedTypes', + 'allowedValues' => 'getAllowedValues', + 'normalizer' => 'getNormalizer', + ); + + foreach ($map as $key => $method) { + try { + $definition[$key] = $introspector->{$method}($option); + } catch (NoConfigurationException $e) { + // noop + } + } + + return $definition; + } + private function getParentOptionsResolver(ResolvedFormTypeInterface $type) { $this->parents[$class = get_class($type->getInnerType())] = array(); diff --git a/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php index 7616d616f144..9d02aba3c12a 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Console\Descriptor; use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * @author Yonel Ceruto @@ -20,10 +21,10 @@ */ class JsonDescriptor extends Descriptor { - protected function describeDefaults(array $options = array()) + protected function describeDefaults(array $options) { - $data['builtin_form_types'] = $this->getCoreTypes(); - $data['service_form_types'] = array_values(array_diff($options['types'], $data['builtin_form_types'])); + $data['builtin_form_types'] = $options['core_types']; + $data['service_form_types'] = $options['service_types']; $data['type_extensions'] = $options['extensions']; $data['type_guessers'] = $options['guessers']; @@ -54,6 +55,30 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF $this->writeData($data, $options); } + protected function describeOption(OptionsResolver $optionsResolver, array $options) + { + $definition = $this->getOptionDefinition($optionsResolver, $options['option']); + + $map = array( + 'required' => 'required', + 'default' => 'default', + 'allowed_types' => 'allowedTypes', + 'allowed_values' => 'allowedValues', + ); + foreach ($map as $label => $name) { + if (array_key_exists($name, $definition)) { + $data[$label] = $definition[$name]; + + if ('default' === $name) { + $data['is_lazy'] = isset($definition['lazy']); + } + } + } + $data['has_normalizer'] = isset($definition['normalizer']); + + $this->writeData($data, $options); + } + private function writeData(array $data, array $options) { $flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0; diff --git a/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php index d5072f1e9a63..6e17e9b859e0 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php @@ -13,6 +13,10 @@ use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; /** * @author Yonel Ceruto @@ -21,18 +25,16 @@ */ class TextDescriptor extends Descriptor { - protected function describeDefaults(array $options = array()) + protected function describeDefaults(array $options) { - $coreTypes = $this->getCoreTypes(); - $this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)'); - $shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $coreTypes); + $shortClassNames = array_map(function ($fqcn) { return array_slice(explode('\\', $fqcn), -1)[0]; }, $options['core_types']); 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->listing($options['service_types']); $this->output->section('Type extensions'); $this->output->listing($options['extensions']); @@ -94,6 +96,34 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF } } + protected function describeOption(OptionsResolver $optionsResolver, array $options) + { + $definition = $this->getOptionDefinition($optionsResolver, $options['option']); + + $dump = $this->getDumpFunction(); + $map = array( + 'Required' => 'required', + 'Default' => 'default', + 'Allowed types' => 'allowedTypes', + 'Allowed values' => 'allowedValues', + 'Normalizer' => 'normalizer', + ); + $rows = array(); + foreach ($map as $label => $name) { + $value = array_key_exists($name, $definition) ? $dump($definition[$name]) : '-'; + if ('default' === $name && isset($definition['lazy'])) { + $value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']); + } + + $rows[] = array("$label", $value); + $rows[] = new TableSeparator(); + } + array_pop($rows); + + $this->output->title(sprintf('%s (%s)', get_class($options['type']), $options['option'])); + $this->output->table(array(), $rows); + } + private function normalizeAndSortOptionsColumns(array $options) { foreach ($options as $group => &$opts) { @@ -125,4 +155,24 @@ private function normalizeAndSortOptionsColumns(array $options) return $options; } + + private function getDumpFunction() + { + $cloner = new VarCloner(); + $cloner->addCasters(array('Closure' => function ($c, $a) { + $prefix = Caster::PREFIX_VIRTUAL; + + return array( + $prefix.'parameters' => isset($a[$prefix.'parameters']) ? count($a[$prefix.'parameters']->value) : 0, + $prefix.'file' => $a[$prefix.'file'], + $prefix.'line' => $a[$prefix.'line'], + ); + })); + $dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); + $dumper->setColors($this->output->isDecorated()); + + return function ($value) use ($dumper, $cloner) { + return rtrim($dumper->dump($cloner->cloneVar($value)->withRefHandles(false), true)); + }; + } } diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index d24af0a4b1bc..f69222c345ed 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -39,6 +39,15 @@ public function testDebugSingleFormType() $this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")', $tester->getDisplay()); } + public function testDebugFormTypeOption() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(array('class' => 'FormType', 'option' => 'method'), array('decorated' => false)); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (method)', $tester->getDisplay()); + } + /** * @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException * @expectedExceptionMessage Could not find type "NonExistentType" @@ -90,7 +99,7 @@ public function testDebugAmbiguousFormTypeInteractive() The type "AmbiguousType" is ambiguous. - Select one of the following form types to display its information: [%A\A\AmbiguousType]: +Select one of the following form types to display its information: [%A\A\AmbiguousType]: [0] %A\A\AmbiguousType [1] %A\B\AmbiguousType %A diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php index 3a5efcefeb68..8849266527d8 100644 --- a/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -15,11 +15,15 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Form\AbstractType; 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\FormInterface; use Symfony\Component\Form\ResolvedFormType; use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Csrf\CsrfTokenManager; abstract class AbstractDescriptorTest extends TestCase @@ -27,8 +31,8 @@ abstract class AbstractDescriptorTest extends TestCase /** @dataProvider getDescribeDefaultsTestData */ public function testDescribeDefaults($object, array $options, $fixtureName) { - $expectedDescription = $this->getExpectedDescription($fixtureName); $describedObject = $this->getObjectDescription($object, $options); + $expectedDescription = $this->getExpectedDescription($fixtureName); if ('json' === $this->getFormat()) { $this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($describedObject), JSON_PRETTY_PRINT)); @@ -40,8 +44,8 @@ public function testDescribeDefaults($object, array $options, $fixtureName) /** @dataProvider getDescribeResolvedFormTypeTestData */ public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, array $options, $fixtureName) { - $expectedDescription = $this->getExpectedDescription($fixtureName); $describedObject = $this->getObjectDescription($type, $options); + $expectedDescription = $this->getExpectedDescription($fixtureName); if ('json' === $this->getFormat()) { $this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($describedObject), JSON_PRETTY_PRINT)); @@ -50,32 +54,64 @@ public function testDescribeResolvedFormType(ResolvedFormTypeInterface $type, ar } } + /** @dataProvider getDescribeOptionTestData */ + public function testDescribeOption(OptionsResolver $optionsResolver, array $options, $fixtureName) + { + $describedObject = $this->getObjectDescription($optionsResolver, $options); + $expectedDescription = $this->getExpectedDescription($fixtureName); + + if ('json' === $this->getFormat()) { + $this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($describedObject), JSON_PRETTY_PRINT)); + } else { + $this->assertStringMatchesFormat(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $describedObject))); + } + } + public function getDescribeDefaultsTestData() { - $options['types'] = array('Symfony\Bridge\Doctrine\Form\Type\EntityType'); + $options['core_types'] = array('Symfony\Component\Form\Extension\Core\Type\FormType'); + $options['service_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'); + $options['decorated'] = false; yield array(null, $options, 'defaults_1'); } public function getDescribeResolvedFormTypeTestData() { - $typeExtensions = array( - new FormTypeCsrfExtension(new CsrfTokenManager()), - ); + $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'); + yield array(new ResolvedFormType(new ChoiceType(), array(), $parent), array('decorated' => false), 'resolved_form_type_1'); + } + + public function getDescribeOptionTestData() + { + $parent = new ResolvedFormType(new FormType()); + $options['decorated'] = false; + + $resolvedType = new ResolvedFormType(new ChoiceType(), array(), $parent); + $options['type'] = $resolvedType->getInnerType(); + $options['option'] = 'choice_translation_domain'; + yield array($resolvedType->getOptionsResolver(), $options, 'default_option_with_normalizer'); + + $resolvedType = new ResolvedFormType(new FooType(), array(), $parent); + $options['type'] = $resolvedType->getInnerType(); + $options['option'] = 'foo'; + yield array($resolvedType->getOptionsResolver(), $options, 'required_option_with_allowed_values'); + + $options['option'] = 'empty_data'; + yield array($resolvedType->getOptionsResolver(), $options, 'overridden_option_with_default_closures'); } abstract protected function getDescriptor(); abstract protected function getFormat(); - private function getObjectDescription($object, array $options = array()) + private function getObjectDescription($object, array $options) { - $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, $options['decorated']); $io = new SymfonyStyle(new ArrayInput(array()), $output); $this->getDescriptor()->describe($io, $object, $options); @@ -93,3 +129,23 @@ private function getFixtureFilename($name) return sprintf('%s/../../Fixtures/Descriptor/%s.%s', __DIR__, $name, $this->getFormat()); } } + +class FooType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setRequired('foo'); + $resolver->setDefault('empty_data', function (Options $options, $value) { + $foo = $options['foo']; + + return function (FormInterface $form) use ($foo) { + return $form->getConfig()->getCompound() ? array($foo) : $foo; + }; + }); + $resolver->setAllowedTypes('foo', 'string'); + $resolver->setAllowedValues('foo', array('bar', 'baz')); + $resolver->setNormalizer('foo', function (Options $options, $value) { + return (string) $value; + }); + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.json new file mode 100644 index 000000000000..0ac903a95475 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.json @@ -0,0 +1,11 @@ +{ + "required": false, + "default": true, + "is_lazy": false, + "allowed_types": [ + "null", + "bool", + "string" + ], + "has_normalizer": true +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt new file mode 100644 index 000000000000..a579a90e53b5 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt @@ -0,0 +1,24 @@ + +Symfony\Component\Form\Extension\Core\Type\ChoiceType (choice_translation_domain) +================================================================================= + + ---------------- --------------------%s + Required false %s + ---------------- --------------------%s + Default true %s + ---------------- --------------------%s + Allowed types [ %s + "null", %s + "bool", %s + "string" %s + ] %s + ---------------- --------------------%s + Allowed values - %s + ---------------- --------------------%s + Normalizer Closure { %s + parameters: 2, %s + file: "%s%eExtension%eCore%eType%eChoiceType.php", + line: "%s to %s" %s + } %s + ---------------- --------------------%s + diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.json index 99858a2f997e..7629e80431eb 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.json @@ -1,39 +1,6 @@ { "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\\ColorType", - "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\\TelType", - "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" + "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType" ], "service_form_types": [ "Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType" diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.txt index 52a579ac43e6..9b3338ec7bd3 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/defaults_1.txt @@ -1,27 +1,21 @@ -Built-in form types (Symfony\Component\Form\Extension\Core\Type) ----------------------------------------------------------------- +Built-in form types (Symfony\Component\Form\Extension\Core\Type) +---------------------------------------------------------------- - BirthdayType, ButtonType, CheckboxType, ChoiceType, CollectionType - ColorType, CountryType, CurrencyType, DateIntervalType, DateTimeType - DateType, EmailType, FileType, FormType, HiddenType - IntegerType, LanguageType, LocaleType, MoneyType, NumberType - PasswordType, PercentType, RadioType, RangeType, RepeatedType - ResetType, SearchType, SubmitType, TelType, TextType - TextareaType, TimeType, TimezoneType, UrlType + FormType -Service form types ------------------- +Service form types +------------------ * Symfony\Bridge\Doctrine\Form\Type\EntityType -Type extensions ---------------- +Type extensions +--------------- * Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension -Type guessers -------------- +Type guessers +------------- * Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.json new file mode 100644 index 000000000000..c41e377acd3d --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.json @@ -0,0 +1,6 @@ +{ + "required": false, + "default": null, + "is_lazy": true, + "has_normalizer": false +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt new file mode 100644 index 000000000000..662d982979bc --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt @@ -0,0 +1,29 @@ + +Symfony\Component\Form\Tests\Console\Descriptor\FooType (empty_data) +==================================================================== + + ---------------- ----------------------%s + Required false %s + ---------------- ----------------------%s + Default Value: null %s + %s + Closure(s): [ %s + Closure { %s + parameters: 1, %s + file: "%s%eExtension%eCore%eType%eFormType.php", + line: "%s to %s" %s + }, %s + Closure { %s + parameters: 2, %s + file: "%s%eTests%eConsole%eDescriptor%eAbstractDescriptorTest.php", + line: "%s to %s" %s + } %s + ] %s + ---------------- ----------------------%s + Allowed types - %s + ---------------- ----------------------%s + Allowed values - %s + ---------------- ----------------------%s + Normalizer - %s + ---------------- ----------------------%s + diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.json new file mode 100644 index 000000000000..126933c6b048 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.json @@ -0,0 +1,11 @@ +{ + "required": true, + "allowed_types": [ + "string" + ], + "allowed_values": [ + "bar", + "baz" + ], + "has_normalizer": true +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt new file mode 100644 index 000000000000..049b692f1dc7 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt @@ -0,0 +1,25 @@ + +Symfony\Component\Form\Tests\Console\Descriptor\FooType (foo) +============================================================= + + ---------------- --------------------%s + Required true %s + ---------------- --------------------%s + Default - %s + ---------------- --------------------%s + Allowed types [ %s + "string" %s + ] %s + ---------------- --------------------%s + Allowed values [ %s + "bar", %s + "baz" %s + ] %s + ---------------- --------------------%s + Normalizer Closure { %s + parameters: 2, %s + file: "%s%eTests%eConsole%eDescriptor%eAbstractDescriptorTest.php", + line: "%s to %s" %s + } %s + ---------------- --------------------%s + 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 index 5f839b85ac6b..2b34ad960d67 100644 --- 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 @@ -1,11 +1,11 @@ -Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") -============================================================================== +Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") +============================================================================== --------------------------- -------------------- ------------------------- ----------------------- -  Options   Overridden options   Parent options   Extension options  + Options Overridden options Parent options Extension options --------------------------- -------------------- ------------------------- ----------------------- - choice_attr FormType FormType FormTypeCsrfExtension + choice_attr FormType FormType FormTypeCsrfExtension choice_label -------------------- ------------------------- ----------------------- choice_loader compound action csrf_field_name choice_name data_class attr csrf_message @@ -28,13 +28,13 @@ upload_max_size_message --------------------------- -------------------- ------------------------- ----------------------- -Parent types ------------- +Parent types +------------ * Symfony\Component\Form\Extension\Core\Type\FormType -Type extensions ---------------- +Type extensions +--------------- * Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index 5f6d15b2c7dd..d19116f37c28 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.4.0 +----- + + * added `OptionsResolverIntrospector` to inspect options definitions inside an `OptionsResolver` instance + 2.6.0 ----- diff --git a/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php new file mode 100644 index 000000000000..60317243e9c4 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Debug; + +use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Maxime Steinhausser + * + * @final + */ +class OptionsResolverIntrospector +{ + private $get; + + public function __construct(OptionsResolver $optionsResolver) + { + $this->get = \Closure::bind(function ($property, $option, $message) { + /** @var OptionsResolver $this */ + if (!$this->isDefined($option)) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist.', $option)); + } + + if (!array_key_exists($option, $this->{$property})) { + throw new NoConfigurationException($message); + } + + return $this->{$property}[$option]; + }, $optionsResolver, $optionsResolver); + } + + /** + * @param string $option + * + * @return mixed + * + * @throws NoConfigurationException on no configured value + */ + public function getDefault($option) + { + return call_user_func($this->get, 'defaults', $option, sprintf('No default value was set for the "%s" option.', $option)); + } + + /** + * @param string $option + * + * @return \Closure[] + * + * @throws NoConfigurationException on no configured closures + */ + public function getLazyClosures($option) + { + return call_user_func($this->get, 'lazy', $option, sprintf('No lazy closures were set for the "%s" option.', $option)); + } + + /** + * @param string $option + * + * @return string[] + * + * @throws NoConfigurationException on no configured types + */ + public function getAllowedTypes($option) + { + return call_user_func($this->get, 'allowedTypes', $option, sprintf('No allowed types were set for the "%s" option.', $option)); + } + + /** + * @param string $option + * + * @return mixed[] + * + * @throws NoConfigurationException on no configured values + */ + public function getAllowedValues($option) + { + return call_user_func($this->get, 'allowedValues', $option, sprintf('No allowed values were set for the "%s" option.', $option)); + } + + /** + * @param string $option + * + * @return \Closure + * + * @throws NoConfigurationException on no configured normalizer + */ + public function getNormalizer($option) + { + return call_user_func($this->get, 'normalizers', $option, sprintf('No normalizer was set for the "%s" option.', $option)); + } +} diff --git a/src/Symfony/Component/OptionsResolver/Exception/NoConfigurationException.php b/src/Symfony/Component/OptionsResolver/Exception/NoConfigurationException.php new file mode 100644 index 000000000000..6693ec14df89 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Exception/NoConfigurationException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; + +/** + * Thrown when trying to introspect an option definition property + * for which no value was configured inside the OptionsResolver instance. + * + * @see OptionsResolverIntrospector + * + * @author Maxime Steinhausser + */ +class NoConfigurationException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php new file mode 100644 index 000000000000..7c4753ab5f6b --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests\Debug; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class OptionsResolverIntrospectorTest extends TestCase +{ + public function testGetDefault() + { + $resolver = new OptionsResolver(); + $resolver->setDefault($option = 'foo', 'bar'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getDefault($option)); + } + + public function testGetDefaultNull() + { + $resolver = new OptionsResolver(); + $resolver->setDefault($option = 'foo', null); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertNull($debug->getDefault($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No default value was set for the "foo" option. + */ + public function testGetDefaultThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getDefault($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetDefaultThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getDefault('foo')); + } + + public function testGetLazyClosures() + { + $resolver = new OptionsResolver(); + $closures = array(); + $resolver->setDefault($option = 'foo', $closures[] = function (Options $options) {}); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame($closures, $debug->getLazyClosures($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No lazy closures were set for the "foo" option. + */ + public function testGetLazyClosuresThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getLazyClosures($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetLazyClosuresThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getLazyClosures('foo')); + } + + public function testGetAllowedTypes() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + $resolver->setAllowedTypes($option = 'foo', $allowedTypes = array('string', 'bool')); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame($allowedTypes, $debug->getAllowedTypes($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No allowed types were set for the "foo" option. + */ + public function testGetAllowedTypesThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getAllowedTypes($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetAllowedTypesThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getAllowedTypes('foo')); + } + + public function testGetAllowedValues() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + $resolver->setAllowedValues($option = 'foo', $allowedValues = array('bar', 'baz')); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame($allowedValues, $debug->getAllowedValues($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No allowed values were set for the "foo" option. + */ + public function testGetAllowedValuesThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getAllowedValues($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetAllowedValuesThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getAllowedValues('foo')); + } + + public function testGetNormalizer() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + $resolver->setNormalizer($option = 'foo', $normalizer = function () {}); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame($normalizer, $debug->getNormalizer($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No normalizer was set for the "foo" option. + */ + public function testGetNormalizerThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined($option = 'foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getNormalizer($option)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetNormalizerThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame('bar', $debug->getNormalizer('foo')); + } +}