Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 76ec3b8

Browse filesBrowse files
committed
[DI] Service constructor arguments validating
1 parent 61aa8fd commit 76ec3b8
Copy full SHA for 76ec3b8

File tree

Expand file treeCollapse file tree

10 files changed

+277
-5
lines changed
Filter options
Expand file treeCollapse file tree

10 files changed

+277
-5
lines changed

‎src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public function __construct()
5656
new RegisterServiceSubscribersPass(),
5757
new ResolveParameterPlaceHoldersPass(false, false),
5858
new ResolveFactoryClassPass(),
59+
new ValidateConstructorArgumentsPass(),
5960
new ResolveNamedArgumentsPass(),
6061
new AutowireRequiredMethodsPass(),
6162
new AutowireRequiredPropertiesPass(),
+95Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\ServiceLocator;
16+
use Symfony\Component\Validator\Constraint;
17+
use Symfony\Component\Validator\Constraints\Composite;
18+
use Symfony\Component\Validator\Exception\ValidationFailedException;
19+
use Symfony\Component\Validator\Mapping\Loader\AbstractLoader;
20+
use Symfony\Component\Validator\Validation;
21+
22+
/**
23+
* Validates service arguments using Validator component
24+
*/
25+
final class ValidateConstructorArgumentsPass extends AbstractRecursivePass
26+
{
27+
/** @var bool */
28+
private $throwExceptionOnValidationFailure;
29+
30+
public function __construct(bool $throwExceptionOnValidationFailure = true)
31+
{
32+
$this->throwExceptionOnValidationFailure = $throwExceptionOnValidationFailure;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
protected function processValue($value, bool $isRoot = false)
39+
{
40+
if (!$value instanceof Definition || $value->hasErrors()) {
41+
return parent::processValue($value, $isRoot);
42+
}
43+
44+
if (ServiceLocator::class === $value->getClass()) {
45+
return parent::processValue($value, $isRoot);
46+
}
47+
48+
if (count($value->getConstraints()) > 0) {
49+
$this->validate($value);
50+
}
51+
52+
return parent::processValue($value, $isRoot);
53+
}
54+
55+
private function validate(Definition $value): void
56+
{
57+
$serviceConstraints = $value->getConstraints();
58+
foreach ($serviceConstraints as $argumentName => $argumentConstraints) {
59+
$argumentValue = $value->getArgument($argumentName);
60+
61+
$validatorConstraints = $this->getValidatorConstraints($argumentConstraints);
62+
$validator = Validation::createCallable(null, ...$validatorConstraints);
63+
try {
64+
$validator($argumentValue);
65+
} catch (ValidationFailedException $e) {
66+
if ($this->throwExceptionOnValidationFailure) {
67+
throw $e;
68+
}
69+
70+
$value->addError($e);
71+
}
72+
}
73+
}
74+
75+
/**
76+
* @param mixed[] $rawConstraints Constraints definition, parsed from config file
77+
*
78+
* @return Constraint[]
79+
*/
80+
private function getValidatorConstraints(array $rawConstraints): array
81+
{
82+
$constraintsList = [];
83+
foreach ($rawConstraints as $constraintName => $constraintValue) {
84+
$validatorConstraintClass = AbstractLoader::DEFAULT_NAMESPACE . $constraintName;
85+
86+
if (\is_subclass_of($validatorConstraintClass, Composite::class)) {
87+
$constraintValue = $this->getValidatorConstraints($constraintValue);
88+
}
89+
90+
$constraintsList[] = new $validatorConstraintClass($constraintValue);
91+
}
92+
93+
return $constraintsList;
94+
}
95+
}

‎src/Symfony/Component/DependencyInjection/Definition.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Definition.php
+58Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Definition
4444
private $errors = [];
4545

4646
protected $arguments = [];
47+
protected $constraints = [];
4748

4849
private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.';
4950

@@ -324,6 +325,63 @@ public function getArgument($index)
324325
return $this->arguments[$index];
325326
}
326327

328+
/**
329+
* Sets constraints to validate arguments
330+
*
331+
* @param mixed[] $constraints
332+
*
333+
* @return $this
334+
*/
335+
public function setConstraints(array $constraints)
336+
{
337+
$this->constraints = $constraints;
338+
339+
return $this;
340+
}
341+
342+
/**
343+
* Gets constraints list to validate arguments
344+
*
345+
* @return mixed[] The array of constraints
346+
*/
347+
public function getConstraints()
348+
{
349+
return $this->constraints;
350+
}
351+
352+
/**
353+
* Sets specific constraints for argument
354+
*
355+
* @param int|string $key
356+
* @param mixed $value
357+
*
358+
* @return $this
359+
*/
360+
public function setConstraint($key, $value)
361+
{
362+
$this->constraints[$key] = $value;
363+
364+
return $this;
365+
}
366+
367+
/**
368+
* Gets constraints to validate argument
369+
*
370+
* @param int|string $index
371+
*
372+
* @return mixed The arguments constraints
373+
*
374+
* @throws OutOfBoundsException When the constraint does not exist
375+
*/
376+
public function getConstraint($index)
377+
{
378+
if (!\array_key_exists($index, $this->constraints)) {
379+
throw new OutOfBoundsException(sprintf('The constraint "%s" doesn\'t exist.', $index));
380+
}
381+
382+
return $this->constraints[$index];
383+
}
384+
327385
/**
328386
* Sets the methods to call after service initialization.
329387
*

‎src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ private function addService(string $id, Definition $definition): string
125125
$code .= sprintf(" arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0));
126126
}
127127

128+
if ($definition->getConstraints()) {
129+
$code .= sprintf(" constraints: %s\n", $this->dumper->dump($this->dumpValue($definition->getConstraints()), 0));
130+
}
131+
128132
if ($definition->getProperties()) {
129133
$code .= sprintf(" properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0));
130134
}

‎src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class YamlFileLoader extends FileLoader
6363
'autowire' => 'autowire',
6464
'autoconfigure' => 'autoconfigure',
6565
'bind' => 'bind',
66+
'constraints' => 'constraints',
6667
];
6768

6869
private static $prototypeKeywords = [
@@ -500,6 +501,10 @@ private function parseDefinition(string $id, $service, string $file, array $defa
500501
$definition->setArguments($this->resolveServices($service['arguments'], $file));
501502
}
502503

504+
if (isset($service['constraints'])) {
505+
$definition->setConstraints($this->resolveServices($service['constraints'], $file));
506+
}
507+
503508
if (isset($service['properties'])) {
504509
$definition->setProperties($this->resolveServices($service['properties'], $file));
505510
}
+109Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Compiler\ValidateConstructorArgumentsPass;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\Validator\Constraints\Ip;
18+
use Symfony\Component\Validator\Exception\ValidationFailedException;
19+
20+
class ValidateConstructorArgumentsPassTest extends TestCase
21+
{
22+
public function testValidationSuccess(): void
23+
{
24+
$container = new ContainerBuilder();
25+
$definition = $container->register('service', \stdClass::class);
26+
$definition
27+
->setArguments([
28+
'$int' => 1,
29+
'$array' => [1, 2, 3],
30+
'$email' => 'test@email.com',
31+
'$datetime' => '2020-12-31 23:59:59',
32+
'$ipAddresses' => ['8.8.4.4', '8.8.8.8'],
33+
'$noConstraints' => 'no constraints for this argument',
34+
])
35+
->setConstraints([
36+
'$int' => ['EqualTo' => 1],
37+
'$array' => [
38+
'Count' => [
39+
'min' => 1,
40+
'max' => 5,
41+
],
42+
],
43+
'$email' => ['Email' => null],
44+
'$datetime' => [
45+
'NotBlank' => null,
46+
'DateTime' => null,
47+
],
48+
'$ipAddresses' => [
49+
'All' => [
50+
'NotBlank' => null,
51+
'Ip' => ['version' => Ip::V4_ONLY_PUBLIC],
52+
],
53+
],
54+
]);
55+
56+
$pass = new ValidateConstructorArgumentsPass(false);
57+
$pass->process($container);
58+
59+
$this->assertCount(0, $definition->getErrors());
60+
}
61+
62+
public function testValidationFailedWithThrowExceptionOnFailure(): void
63+
{
64+
$this->expectException(ValidationFailedException::class);
65+
$this->expectExceptionMessage('Provided string does not look like JSON. (code 0789c8ad-2d2b-49a4-8356-e2ce63998504)');
66+
67+
$container = new ContainerBuilder();
68+
$definition = $container->register('service', \stdClass::class);
69+
$definition
70+
->setArguments([
71+
'$json' => 'wrong json',
72+
])
73+
->setConstraints([
74+
'$json' => [
75+
'Json' => ['message' => 'Provided string does not look like JSON.'],
76+
],
77+
]);
78+
79+
$pass = new ValidateConstructorArgumentsPass();
80+
$pass->process($container);
81+
}
82+
83+
public function testValidationFailedWithDoNotThrowExceptionOnFailure(): void
84+
{
85+
$container = new ContainerBuilder();
86+
$definition = $container->register('service', \stdClass::class);
87+
$definition
88+
->setArguments([
89+
'$choice' => 'foo',
90+
])
91+
->setConstraints([
92+
'$choice' => [
93+
'Choice' => [
94+
'choices' => ['bar', 'baz'],
95+
'message' => 'Choice should be one of: bar, baz.',
96+
],
97+
],
98+
]);
99+
100+
$pass = new ValidateConstructorArgumentsPass(false);
101+
$pass->process($container);
102+
103+
$this->assertCount(1, $definition->getErrors());
104+
$this->assertMatchesRegularExpression(
105+
'/Choice should be one of: bar, baz. \(code 8e179f1b-97aa-4560-a02f-2a8b42e49df7\)/',
106+
$definition->getErrors()[0]
107+
);
108+
}
109+
}

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function isCompiled(): bool
4242
public function getRemovedIds(): array
4343
{
4444
return [
45-
'.service_locator.mtT6G8y' => true,
45+
'.service_locator.yG6Rg7I' => true,
4646
'Psr\\Container\\ContainerInterface' => true,
4747
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
4848
'foo' => true,

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function isCompiled(): bool
4444
public function getRemovedIds(): array
4545
{
4646
return [
47-
'.service_locator.PWbaRiJ' => true,
47+
'.service_locator.k59fPaB' => true,
4848
'Psr\\Container\\ContainerInterface' => true,
4949
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
5050
];

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function isCompiled(): bool
4545
public function getRemovedIds(): array
4646
{
4747
return [
48-
'.service_locator.ZP1tNYN' => true,
48+
'.service_locator.wX0ALtJ' => true,
4949
'Psr\\Container\\ContainerInterface' => true,
5050
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
5151
'foo2' => true,

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public function isCompiled(): bool
4444
public function getRemovedIds(): array
4545
{
4646
return [
47-
'.service_locator.DlIAmAe' => true,
48-
'.service_locator.DlIAmAe.foo_service' => true,
47+
'.service_locator.u.4vYl9' => true,
48+
'.service_locator.u.4vYl9.foo_service' => true,
4949
'Psr\\Container\\ContainerInterface' => true,
5050
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
5151
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.