Skip to content

Service constructor arguments validating #39678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function __construct()
new RegisterServiceSubscribersPass(),
new ResolveParameterPlaceHoldersPass(false, false),
new ResolveFactoryClassPass(),
new ValidateConstructorArgumentsPass(),
new ResolveNamedArgumentsPass(),
new AutowireRequiredMethodsPass(),
new AutowireRequiredPropertiesPass(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Mapping\Loader\AbstractLoader;
use Symfony\Component\Validator\Validation;

/**
* Validates service arguments using Validator component.
*/
final class ValidateConstructorArgumentsPass extends AbstractRecursivePass
{
/** @var bool */
private $throwExceptionOnValidationFailure;

public function __construct(bool $throwExceptionOnValidationFailure = true)
{
$this->throwExceptionOnValidationFailure = $throwExceptionOnValidationFailure;
}

/**
* {@inheritdoc}
*/
protected function processValue($value, bool $isRoot = false)
{
if (!$value instanceof Definition || $value->hasErrors()) {
return parent::processValue($value, $isRoot);
}

if (ServiceLocator::class === $value->getClass()) {
return parent::processValue($value, $isRoot);
}

if (\count($value->getConstraints()) > 0) {
$this->validate($value);
}

return parent::processValue($value, $isRoot);
}

private function validate(Definition $value): void
{
$serviceConstraints = $value->getConstraints();
foreach ($serviceConstraints as $argumentName => $argumentConstraints) {
$argumentValue = $value->getArgument($argumentName);

$validatorConstraints = $this->getValidatorConstraints($argumentConstraints);
$validator = Validation::createCallable(null, ...$validatorConstraints);
try {
$validator($argumentValue);
} catch (ValidationFailedException $e) {
if ($this->throwExceptionOnValidationFailure) {
throw $e;
}

$value->addError($e);
}
}
}

/**
* @param mixed[] $rawConstraints Constraints definition, parsed from config file
*
* @return Constraint[]
*/
private function getValidatorConstraints(array $rawConstraints): array
{
$constraintsList = [];
foreach ($rawConstraints as $constraintName => $constraintValue) {
$validatorConstraintClass = AbstractLoader::DEFAULT_NAMESPACE.$constraintName;

if (is_subclass_of($validatorConstraintClass, Composite::class)) {
$constraintValue = $this->getValidatorConstraints($constraintValue);
}

$constraintsList[] = new $validatorConstraintClass($constraintValue);
}

return $constraintsList;
}
}
58 changes: 58 additions & 0 deletions src/Symfony/Component/DependencyInjection/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Definition
private $errors = [];

protected $arguments = [];
protected $constraints = [];

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

Expand Down Expand Up @@ -324,6 +325,63 @@ public function getArgument($index)
return $this->arguments[$index];
}

/**
* Sets constraints to validate arguments.
*
* @param mixed[] $constraints
*
* @return $this
*/
public function setConstraints(array $constraints)
{
$this->constraints = $constraints;

return $this;
}

/**
* Gets constraints list to validate arguments.
*
* @return mixed[] The array of constraints
*/
public function getConstraints()
{
return $this->constraints;
}

/**
* Sets specific constraints for argument.
*
* @param int|string $key
* @param mixed $value
*
* @return $this
*/
public function setConstraint($key, $value)
{
$this->constraints[$key] = $value;

return $this;
}

/**
* Gets constraints to validate argument.
*
* @param int|string $index
*
* @return mixed The arguments constraints
*
* @throws OutOfBoundsException When the constraint does not exist
*/
public function getConstraint($index)
{
if (!\array_key_exists($index, $this->constraints)) {
throw new OutOfBoundsException(sprintf('The constraint "%s" doesn\'t exist.', $index));
}

return $this->constraints[$index];
}

/**
* Sets the methods to call after service initialization.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ private function addService(string $id, Definition $definition): string
$code .= sprintf(" arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0));
}

if ($definition->getConstraints()) {
$code .= sprintf(" constraints: %s\n", $this->dumper->dump($this->dumpValue($definition->getConstraints()), 0));
}

if ($definition->getProperties()) {
$code .= sprintf(" properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class YamlFileLoader extends FileLoader
'autowire' => 'autowire',
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
'constraints' => 'constraints',
];

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

if (isset($service['constraints'])) {
$definition->setConstraints($this->resolveServices($service['constraints'], $file));
}

if (isset($service['properties'])) {
$definition->setProperties($this->resolveServices($service['properties'], $file));
}
Expand Down Expand Up @@ -868,7 +873,7 @@ private function resolveServices($value, string $file, bool $isParameter = false
}
} elseif (\is_string($value) && 0 === strpos($value, '@=')) {
if (!class_exists(Expression::class)) {
throw new \LogicException(sprintf('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'));
throw new \LogicException('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
}

return new Expression(substr($value, 2));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\ValidateConstructorArgumentsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\Constraints\Ip;
use Symfony\Component\Validator\Exception\ValidationFailedException;

class ValidateConstructorArgumentsPassTest extends TestCase
{
public function testValidationSuccess()
{
$container = new ContainerBuilder();
$definition = $container->register('service', \stdClass::class);
$definition
->setArguments([
'$int' => 1,
'$array' => [1, 2, 3],
'$email' => 'test@email.com',
'$datetime' => '2020-12-31 23:59:59',
'$ipAddresses' => ['8.8.4.4', '8.8.8.8'],
'$noConstraints' => 'no constraints for this argument',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OskarStark, sorry, didn't understand what do you mean

])
->setConstraints([
'$int' => ['EqualTo' => 1],
'$array' => [
'Count' => [
'min' => 1,
'max' => 5,
],
],
'$email' => ['Email' => null],
'$datetime' => [
'NotBlank' => null,
'DateTime' => null,
],
'$ipAddresses' => [
'All' => [
'NotBlank' => null,
'Ip' => ['version' => Ip::V4_ONLY_PUBLIC],
],
],
]);

$pass = new ValidateConstructorArgumentsPass(false);
$pass->process($container);

$this->assertCount(0, $definition->getErrors());
}

public function testValidationFailedWithThrowExceptionOnFailure()
{
$this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Provided string does not look like JSON. (code 0789c8ad-2d2b-49a4-8356-e2ce63998504)');

$container = new ContainerBuilder();
$definition = $container->register('service', \stdClass::class);
$definition
->setArguments([
'$json' => 'wrong json',
])
->setConstraints([
'$json' => [
'Json' => ['message' => 'Provided string does not look like JSON.'],
],
]);

$pass = new ValidateConstructorArgumentsPass();
$pass->process($container);
}

public function testValidationFailedWithDoNotThrowExceptionOnFailure()
{
$container = new ContainerBuilder();
$definition = $container->register('service', \stdClass::class);
$definition
->setArguments([
'$choice' => 'foo',
])
->setConstraints([
'$choice' => [
'Choice' => [
'choices' => ['bar', 'baz'],
'message' => 'Choice should be one of: bar, baz.',
],
],
]);

$pass = new ValidateConstructorArgumentsPass(false);
$pass->process($container);

$this->assertCount(1, $definition->getErrors());
$this->assertMatchesRegularExpression(
'/Choice should be one of: bar, baz. \(code 8e179f1b-97aa-4560-a02f-2a8b42e49df7\)/',
$definition->getErrors()[0]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function isCompiled(): bool
public function getRemovedIds(): array
{
return [
'.service_locator.mtT6G8y' => true,
'.service_locator.yG6Rg7I' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'foo' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function isCompiled(): bool
public function getRemovedIds(): array
{
return [
'.service_locator.PWbaRiJ' => true,
'.service_locator.k59fPaB' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function isCompiled(): bool
public function getRemovedIds(): array
{
return [
'.service_locator.ZP1tNYN' => true,
'.service_locator.wX0ALtJ' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'foo2' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public function isCompiled(): bool
public function getRemovedIds(): array
{
return [
'.service_locator.DlIAmAe' => true,
'.service_locator.DlIAmAe.foo_service' => true,
'.service_locator.u.4vYl9' => true,
'.service_locator.u.4vYl9.foo_service' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy