Skip to content

Commit

Permalink
[DependencyInjection] Handle env var placeholders in CheckTypeDeclara…
Browse files Browse the repository at this point in the history
…tionsPass
  • Loading branch information
fancyweb committed Dec 5, 2019
1 parent bfe697b commit 47e9a0f
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ExpressionLanguage\Expression;
Expand Down Expand Up @@ -104,27 +106,29 @@ private function checkTypeDeclarations(Definition $checkedDefinition, \Reflectio
$reflectionParameters = $reflectionFunction->getParameters();
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));

$envPlaceholderUniquePrefix = $this->container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $this->container->getParameterBag()->getEnvPlaceholderUniquePrefix() : null;

for ($i = 0; $i < $checksCount; ++$i) {
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
continue;
}

$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]);
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i], $envPlaceholderUniquePrefix);
}

if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
$variadicParameters = \array_slice($values, $lastParameter->getPosition());

foreach ($variadicParameters as $variadicParameter) {
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter);
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter, $envPlaceholderUniquePrefix);
}
}
}

/**
* @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
*/
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix): void
{
$type = $parameter->getType()->getName();

Expand Down Expand Up @@ -174,12 +178,24 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar
throw new InvalidParameterTypeException($this->currentId, $class, $parameter);
}

if ($value instanceof Parameter) {
$value = $this->container->getParameter($value);
} elseif ($value instanceof Expression) {
if ($value instanceof Expression) {
$value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this->container]);
} elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
$value = $this->container->getParameter($match[1]);
} elseif (\is_string($value)) {
if ('%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
// Only array parameters are not inlined when dumped.
$value = [];
} elseif ($envPlaceholderUniquePrefix && false !== strpos($value, 'env_')) {
// If the value is an env placeholder that is either mixed with a string or with another env placeholder, then its resolved value will always be a string, so we don't need to resolve it.
// We don't need to change the value because it is already a string.
if ('' === preg_replace('/'.$envPlaceholderUniquePrefix.'_\w+_[a-f0-9]{32}/U', '', $value, -1, $c) && 1 === $c) {
try {
$value = $this->container->resolveEnvPlaceholders($value, true);
} catch (EnvNotFoundException | RuntimeException $e) {
// If an env placeholder cannot be resolved, we skip the validation.
return;
}
}
}
}

if (null === $value && $parameter->allowsNull()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall;
Expand All @@ -33,7 +37,7 @@ class CheckTypeDeclarationsPassTest extends TestCase
{
public function testProcessThrowsExceptionOnInvalidTypesConstructorArguments()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');

$container = new ContainerBuilder();
Expand All @@ -47,7 +51,7 @@ public function testProcessThrowsExceptionOnInvalidTypesConstructorArguments()

public function testProcessThrowsExceptionOnInvalidTypesMethodCallArguments()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');

$container = new ContainerBuilder();
Expand All @@ -61,7 +65,7 @@ public function testProcessThrowsExceptionOnInvalidTypesMethodCallArguments()

public function testProcessFailsWhenPassingNullToRequiredArgument()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "NULL" passed.');

$container = new ContainerBuilder();
Expand All @@ -74,7 +78,7 @@ public function testProcessFailsWhenPassingNullToRequiredArgument()

public function testProcessThrowsExceptionWhenMissingArgumentsInConstructor()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct()" requires 1 arguments, 0 passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -111,7 +115,7 @@ public function testProcessRegisterWithClassName()

public function testProcessThrowsExceptionWhenMissingArgumentsInMethodCall()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo()" requires 1 arguments, 0 passed.');

$container = new ContainerBuilder();
Expand All @@ -126,7 +130,7 @@ public function testProcessThrowsExceptionWhenMissingArgumentsInMethodCall()

public function testProcessVariadicFails()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');

$container = new ContainerBuilder();
Expand All @@ -145,7 +149,7 @@ public function testProcessVariadicFails()

public function testProcessVariadicFailsOnPassingBadTypeOnAnotherArgument()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -209,7 +213,7 @@ public function testProcessSuccessWhenUsingOptionalArgumentWithGoodType()

public function testProcessFailsWhenUsingOptionalArgumentWithBadType()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosOptional" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -239,7 +243,7 @@ public function testProcessSuccessWhenPassingNullToOptional()

public function testProcessSuccessWhenPassingNullToOptionalThatDoesNotAcceptNull()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgumentNotNull::__construct" accepts "int", "NULL" passed.');

$container = new ContainerBuilder();
Expand All @@ -252,7 +256,7 @@ public function testProcessSuccessWhenPassingNullToOptionalThatDoesNotAcceptNull

public function testProcessFailsWhenPassingBadTypeToOptional()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgument::__construct" accepts "stdClass", "string" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -282,7 +286,7 @@ public function testProcessSuccessScalarType()

public function testProcessFailsOnPassingScalarTypeToConstructorTypedWithClass()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "integer" passed.');

$container = new ContainerBuilder();
Expand All @@ -295,7 +299,7 @@ public function testProcessFailsOnPassingScalarTypeToConstructorTypedWithClass()

public function testProcessFailsOnPassingScalarTypeToMethodTypedWithClass()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "string" passed.');

$container = new ContainerBuilder();
Expand All @@ -310,7 +314,7 @@ public function testProcessFailsOnPassingScalarTypeToMethodTypedWithClass()

public function testProcessFailsOnPassingClassToScalarTypedParameter()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setScalars" accepts "int", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -370,7 +374,7 @@ public function testProcessSuccessWhenPassingArray()

public function testProcessSuccessWhenPassingIntegerToArrayTypedParameter()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException::class);
$this->expectException(InvalidParameterTypeException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "integer" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -430,7 +434,7 @@ public function testProcessFactory()

public function testProcessFactoryFailsOnInvalidParameterType()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');

$container = new ContainerBuilder();
Expand All @@ -448,7 +452,7 @@ public function testProcessFactoryFailsOnInvalidParameterType()

public function testProcessFactoryFailsOnInvalidParameterTypeOptional()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');

$container = new ContainerBuilder();
Expand Down Expand Up @@ -558,7 +562,7 @@ public function testProcessDoesNotThrowsExceptionOnValidTypes()

public function testProcessThrowsOnIterableTypeWhenScalarPassed()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar_call": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setIterable" accepts "iterable", "integer" passed.');

$container = new ContainerBuilder();
Expand All @@ -571,6 +575,20 @@ public function testProcessThrowsOnIterableTypeWhenScalarPassed()
$this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo);
}

public function testProcessResolveArrayParameters()
{
$container = new ContainerBuilder();
$container->setParameter('ccc', ['foobar']);

$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['%ccc%']);

(new CheckTypeDeclarationsPass(true))->process($container);

$this->addToAssertionCount(1);
}

public function testProcessResolveExpressions()
{
$container = new ContainerBuilder();
Expand All @@ -584,4 +602,73 @@ public function testProcessResolveExpressions()

$this->addToAssertionCount(1);
}

public function testProcessHandleMixedEnvPlaceholder()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "foobar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "string" passed.');

$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
'ccc' => '%env(FOO)%',
]));

$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['foo%ccc%']);

(new CheckTypeDeclarationsPass(true))->process($container);
}

public function testProcessHandleMultipleEnvPlaceholder()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "foobar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "string" passed.');

$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
'ccc' => '%env(FOO)%',
'fcy' => '%env(int:BAR)%',
]));

$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['%ccc%%fcy%']);

(new CheckTypeDeclarationsPass(true))->process($container);
}

public function testProcessHandleExistingEnvPlaceholder()
{
putenv('ARRAY={"foo":"bar"}');

$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
'ccc' => '%env(json:ARRAY)%',
]));

$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['%ccc%']);

(new ResolveParameterPlaceHoldersPass())->process($container);
(new CheckTypeDeclarationsPass(true))->process($container);

$this->addToAssertionCount(1);

putenv('ARRAY=');
}

public function testProcessHandleNotFoundEnvPlaceholder()
{
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
'ccc' => '%env(json:ARRAY)%',
]));

$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['%ccc%']);

(new ResolveParameterPlaceHoldersPass())->process($container);
(new CheckTypeDeclarationsPass(true))->process($container);

$this->addToAssertionCount(1);
}
}

0 comments on commit 47e9a0f

Please sign in to comment.