Skip to content

Commit 3c05652

Browse files
Merge branch '3.4'
* 3.4: Clarify the exceptions are going to be rendered just after [DI] Remove colon from env placeholders fixed CS [Yaml] initialize inline line numbers [Workflow] Added tests for the is_valid() guard expression [Workflow] Added guard 'is_valid()' method support Add & use OptionResolverIntrospector Add debug:form type option
2 parents 7b933ab + 61cda3e commit 3c05652

32 files changed

+868
-182
lines changed

src/Symfony/Bundle/FrameworkBundle/Console/Application.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ private function renderRegistrationErrors(InputInterface $input, OutputInterface
190190
$output = $output->getErrorOutput();
191191
}
192192

193-
(new SymfonyStyle($input, $output))->warning('Some commands could not be registered.');
193+
(new SymfonyStyle($input, $output))->warning('Some commands could not be registered:');
194194

195195
foreach ($this->registrationErrors as $error) {
196196
$this->doRenderException($error, $output);

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
/**
1919
* @author Christian Flothmann <[email protected]>
20+
* @author Grégoire Pineau <[email protected]>
2021
*/
2122
class WorkflowGuardListenerPass implements CompilerPassInterface
2223
{
@@ -31,20 +32,17 @@ public function process(ContainerBuilder $container)
3132

3233
$container->getParameterBag()->remove('workflow.has_guard_listeners');
3334

34-
if (!$container->has('security.token_storage')) {
35-
throw new LogicException('The "security.token_storage" service is needed to be able to use the workflow guard listener.');
36-
}
37-
38-
if (!$container->has('security.authorization_checker')) {
39-
throw new LogicException('The "security.authorization_checker" service is needed to be able to use the workflow guard listener.');
40-
}
41-
42-
if (!$container->has('security.authentication.trust_resolver')) {
43-
throw new LogicException('The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener.');
44-
}
45-
46-
if (!$container->has('security.role_hierarchy')) {
47-
throw new LogicException('The "security.role_hierarchy" service is needed to be able to use the workflow guard listener.');
35+
$servicesNeeded = array(
36+
'security.token_storage',
37+
'security.authorization_checker',
38+
'security.authentication.trust_resolver',
39+
'security.role_hierarchy',
40+
);
41+
42+
foreach ($servicesNeeded as $service) {
43+
if (!$container->has($service)) {
44+
throw new LogicException(sprintf('The "%s" service is needed to be able to use the workflow guard listener.', $service));
45+
}
4846
}
4947
}
5048
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
601601
new Reference('security.authorization_checker'),
602602
new Reference('security.authentication.trust_resolver'),
603603
new Reference('security.role_hierarchy'),
604+
new Reference('validator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
604605
));
605606

606607
$container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard);

src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public function testRunOnlyWarnsOnUnregistrableCommand()
160160
$output = $tester->getDisplay();
161161

162162
$this->assertSame(0, $tester->getStatusCode());
163-
$this->assertContains('Some commands could not be registered.', $output);
163+
$this->assertContains('Some commands could not be registered:', $output);
164164
$this->assertContains('throwing', $output);
165165
$this->assertContains('fine', $output);
166166
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
17-
use Symfony\Component\DependencyInjection\Exception\LogicException;
1817
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
1918
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2019
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
2120
use Symfony\Component\Security\Core\Role\RoleHierarchy;
22-
use Symfony\Component\Workflow\EventListener\GuardListener;
21+
use Symfony\Component\Validator\Validator\ValidatorInterface;
2322

2423
class WorkflowGuardListenerPassTest extends TestCase
2524
{
@@ -29,53 +28,37 @@ class WorkflowGuardListenerPassTest extends TestCase
2928
protected function setUp()
3029
{
3130
$this->container = new ContainerBuilder();
32-
$this->container->register('foo.listener.guard', GuardListener::class);
33-
$this->container->register('bar.listener.guard', GuardListener::class);
3431
$this->compilerPass = new WorkflowGuardListenerPass();
3532
}
3633

37-
public function testListenersAreNotRemovedIfParameterIsNotSet()
34+
public function testNoExeptionIfParameterIsNotSet()
3835
{
3936
$this->compilerPass->process($this->container);
4037

41-
$this->assertTrue($this->container->hasDefinition('foo.listener.guard'));
42-
$this->assertTrue($this->container->hasDefinition('bar.listener.guard'));
43-
}
44-
45-
public function testParameterIsRemovedWhenThePassIsProcessed()
46-
{
47-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
48-
49-
try {
50-
$this->compilerPass->process($this->container);
51-
} catch (LogicException $e) {
52-
// Here, we are not interested in the exception handling. This is tested further down.
53-
}
54-
5538
$this->assertFalse($this->container->hasParameter('workflow.has_guard_listeners'));
5639
}
5740

58-
public function testListenersAreNotRemovedIfAllDependenciesArePresent()
41+
public function testNoExeptionIfAllDependenciesArePresent()
5942
{
60-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
43+
$this->container->setParameter('workflow.has_guard_listeners', true);
6144
$this->container->register('security.token_storage', TokenStorageInterface::class);
6245
$this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class);
6346
$this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class);
6447
$this->container->register('security.role_hierarchy', RoleHierarchy::class);
48+
$this->container->register('validator', ValidatorInterface::class);
6549

6650
$this->compilerPass->process($this->container);
6751

68-
$this->assertTrue($this->container->hasDefinition('foo.listener.guard'));
69-
$this->assertTrue($this->container->hasDefinition('bar.listener.guard'));
52+
$this->assertFalse($this->container->hasParameter('workflow.has_guard_listeners'));
7053
}
7154

7255
/**
7356
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
7457
* @expectedExceptionMessage The "security.token_storage" service is needed to be able to use the workflow guard listener.
7558
*/
76-
public function testListenersAreRemovedIfTheTokenStorageServiceIsNotPresent()
59+
public function testExceptionIfTheTokenStorageServiceIsNotPresent()
7760
{
78-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
61+
$this->container->setParameter('workflow.has_guard_listeners', true);
7962
$this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class);
8063
$this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class);
8164
$this->container->register('security.role_hierarchy', RoleHierarchy::class);
@@ -87,9 +70,9 @@ public function testListenersAreRemovedIfTheTokenStorageServiceIsNotPresent()
8770
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
8871
* @expectedExceptionMessage The "security.authorization_checker" service is needed to be able to use the workflow guard listener.
8972
*/
90-
public function testListenersAreRemovedIfTheAuthorizationCheckerServiceIsNotPresent()
73+
public function testExceptionIfTheAuthorizationCheckerServiceIsNotPresent()
9174
{
92-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
75+
$this->container->setParameter('workflow.has_guard_listeners', true);
9376
$this->container->register('security.token_storage', TokenStorageInterface::class);
9477
$this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class);
9578
$this->container->register('security.role_hierarchy', RoleHierarchy::class);
@@ -101,9 +84,9 @@ public function testListenersAreRemovedIfTheAuthorizationCheckerServiceIsNotPres
10184
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
10285
* @expectedExceptionMessage The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener.
10386
*/
104-
public function testListenersAreRemovedIfTheAuthenticationTrustResolverServiceIsNotPresent()
87+
public function testExceptionIfTheAuthenticationTrustResolverServiceIsNotPresent()
10588
{
106-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
89+
$this->container->setParameter('workflow.has_guard_listeners', true);
10790
$this->container->register('security.token_storage', TokenStorageInterface::class);
10891
$this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class);
10992
$this->container->register('security.role_hierarchy', RoleHierarchy::class);
@@ -115,9 +98,9 @@ public function testListenersAreRemovedIfTheAuthenticationTrustResolverServiceIs
11598
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
11699
* @expectedExceptionMessage The "security.role_hierarchy" service is needed to be able to use the workflow guard listener.
117100
*/
118-
public function testListenersAreRemovedIfTheRoleHierarchyServiceIsNotPresent()
101+
public function testExceptionIfTheRoleHierarchyServiceIsNotPresent()
119102
{
120-
$this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard'));
103+
$this->container->setParameter('workflow.has_guard_listeners', true);
121104
$this->container->register('security.token_storage', TokenStorageInterface::class);
122105
$this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class);
123106
$this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class);

src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function get($name)
4848
}
4949

5050
$uniqueName = md5($name.uniqid(mt_rand(), true));
51-
$placeholder = sprintf('env_%s_%s', $env, $uniqueName);
51+
$placeholder = sprintf('env_%s_%s', str_replace(':', '_', $env), $uniqueName);
5252
$this->envPlaceholders[$env][$placeholder] = $placeholder;
5353

5454
return $placeholder;

src/Symfony/Component/Form/Command/DebugCommand.php

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
use Symfony\Component\Console\Output\OutputInterface;
2020
use Symfony\Component\Console\Style\SymfonyStyle;
2121
use Symfony\Component\Form\Console\Helper\DescriptorHelper;
22+
use Symfony\Component\Form\Extension\Core\CoreExtension;
2223
use Symfony\Component\Form\FormRegistryInterface;
24+
use Symfony\Component\Form\FormTypeInterface;
2325

2426
/**
2527
* A console command for retrieving information about form types.
@@ -55,6 +57,7 @@ protected function configure()
5557
$this
5658
->setDefinition(array(
5759
new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
60+
new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'),
5861
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt or json)', 'txt'),
5962
))
6063
->setDescription('Displays form type information')
@@ -70,6 +73,10 @@ protected function configure()
7073
7174
The command lists all defined options that contains the given form type, as well as their parents and type extensions.
7275
76+
<info>php %command.full_name% ChoiceType choice_value</info>
77+
78+
The command displays the definition of the given option name.
79+
7380
<info>php %command.full_name% --format=json</info>
7481
7582
The command lists everything in a machine readable json format.
@@ -87,14 +94,42 @@ protected function execute(InputInterface $input, OutputInterface $output)
8794

8895
if (null === $class = $input->getArgument('class')) {
8996
$object = null;
90-
$options['types'] = $this->types;
97+
$options['core_types'] = $this->getCoreTypes();
98+
$options['service_types'] = array_values(array_diff($this->types, $options['core_types']));
9199
$options['extensions'] = $this->extensions;
92100
$options['guessers'] = $this->guessers;
101+
foreach ($options as $k => $list) {
102+
sort($options[$k]);
103+
}
93104
} else {
94105
if (!class_exists($class)) {
95106
$class = $this->getFqcnTypeClass($input, $io, $class);
96107
}
97-
$object = $this->formRegistry->getType($class);
108+
$resolvedType = $this->formRegistry->getType($class);
109+
110+
if ($option = $input->getArgument('option')) {
111+
$object = $resolvedType->getOptionsResolver();
112+
113+
if (!$object->isDefined($option)) {
114+
$message = sprintf('Option "%s" is not defined in "%s".', $option, get_class($resolvedType->getInnerType()));
115+
116+
if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) {
117+
if (1 == count($alternatives)) {
118+
$message .= "\n\nDid you mean this?\n ";
119+
} else {
120+
$message .= "\n\nDid you mean one of these?\n ";
121+
}
122+
$message .= implode("\n ", $alternatives);
123+
}
124+
125+
throw new InvalidArgumentException($message);
126+
}
127+
128+
$options['type'] = $resolvedType->getInnerType();
129+
$options['option'] = $option;
130+
} else {
131+
$object = $resolvedType;
132+
}
98133
}
99134

100135
$helper = new DescriptorHelper();
@@ -105,14 +140,27 @@ protected function execute(InputInterface $input, OutputInterface $output)
105140
private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shortClassName)
106141
{
107142
$classes = array();
143+
sort($this->namespaces);
108144
foreach ($this->namespaces as $namespace) {
109145
if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
110146
$classes[] = $fqcn;
111147
}
112148
}
113149

114150
if (0 === $count = count($classes)) {
115-
throw new InvalidArgumentException(sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)));
151+
$message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces));
152+
153+
$allTypes = array_merge($this->getCoreTypes(), $this->types);
154+
if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) {
155+
if (1 == count($alternatives)) {
156+
$message .= "\n\nDid you mean this?\n ";
157+
} else {
158+
$message .= "\n\nDid you mean one of these?\n ";
159+
}
160+
$message .= implode("\n ", $alternatives);
161+
}
162+
163+
throw new InvalidArgumentException($message);
116164
}
117165
if (1 === $count) {
118166
return $classes[0];
@@ -121,6 +169,35 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, $shor
121169
throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes)));
122170
}
123171

124-
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]);
172+
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]);
173+
}
174+
175+
private function getCoreTypes()
176+
{
177+
$coreExtension = new CoreExtension();
178+
$loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes');
179+
$loadTypesRefMethod->setAccessible(true);
180+
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
181+
$coreTypes = array_map(function (FormTypeInterface $type) { return get_class($type); }, $coreTypes);
182+
sort($coreTypes);
183+
184+
return $coreTypes;
185+
}
186+
187+
private function findAlternatives($name, array $collection)
188+
{
189+
$alternatives = array();
190+
foreach ($collection as $item) {
191+
$lev = levenshtein($name, $item);
192+
if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
193+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
194+
}
195+
}
196+
197+
$threshold = 1e3;
198+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
199+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
200+
201+
return array_keys($alternatives);
125202
}
126203
}

0 commit comments

Comments
 (0)