Explorar el Código

[DependencyInjection] added a check for circular references in parameter definitions

Fabien Potencier hace 14 años
padre
commit
2438a73c7b

+ 34 - 0
src/Symfony/Component/DependencyInjection/Exception/ParameterCircularReferenceException.php

@@ -0,0 +1,34 @@
+<?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\Exception;
+
+/**
+ * This exception is thrown when a circular reference in a parameter is detected.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ParameterCircularReferenceException extends RuntimeException
+{
+    private $parameters;
+
+    public function __construct($parameters)
+    {
+        parent::__construct(sprintf('Circular reference detected for parameter "%s" ("%s" > "%s").', $parameters[0], implode('" > "', $parameters), $parameters[0]));
+
+        $this->parameters = $parameters;
+    }
+
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+}

+ 34 - 12
src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php

@@ -12,6 +12,7 @@
 namespace Symfony\Component\DependencyInjection\ParameterBag;
 
 use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
+use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
 
 /**
  *
@@ -125,18 +126,20 @@ class ParameterBag implements ParameterBagInterface
      * Replaces parameter placeholders (%name%) by their values.
      *
      * @param mixed $value A value
+     * @param array $resolving An array of keys that are being resolved (used internally to detect circular references)
      *
      * @return mixed The resolved value
      *
      * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist
+     * @throws ParameterCircularReferenceException if a circular reference if detected
      * @throws \LogicException when a given parameter has a type problem.
      */
-    public function resolveValue($value)
+    public function resolveValue($value, array $resolving = array())
     {
         if (is_array($value)) {
             $args = array();
             foreach ($value as $k => $v) {
-                $args[$this->resolveValue($k)] = $this->resolveValue($v);
+                $args[$this->resolveValue($k, $resolving)] = $this->resolveValue($v, $resolving);
             }
 
             return $args;
@@ -146,36 +149,55 @@ class ParameterBag implements ParameterBagInterface
             return $value;
         }
 
-        return $this->resolveString($value);
+        return $this->resolveString($value, $resolving);
     }
 
     /**
      * Resolves parameters inside a string
      *
-     * @param string $value The string to resolve
+     * @param string $value     The string to resolve
+     * @param array  $resolving An array of keys that are being resolved (used internally to detect circular references)
+     *
      * @return string The resolved string
      *
      * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist
+     * @throws ParameterCircularReferenceException if a circular reference if detected
      * @throws \LogicException when a given parameter has a type problem.
      */
-    public function resolveString($value)
+    public function resolveString($value, array $resolving = array())
     {
+        // we do this to deal with non string values (Boolean, integer, ...)
+        // as the preg_replace_callback throw an exception when trying
+        // a non-string in a parameter value
         if (preg_match('/^%([^%]+)%$/', $value, $match)) {
-            // we do this to deal with non string values (Boolean, integer, ...)
-            // as the preg_replace_callback throw an exception when trying
-            // a non-string in a parameter value
-            return $this->resolveValue($this->get(strtolower($match[1])));
+            $key = strtolower($match[1]);
+
+            if (isset($resolving[$key])) {
+                throw new ParameterCircularReferenceException(array_keys($resolving));
+            }
+
+            $resolving[$key] = true;
+
+            return $this->resolveValue($this->get($key), $resolving);
         }
 
         $self = $this;
-        return str_replace('%%', '%', preg_replace_callback('/(?<!%)%([^%]+)%/', function ($match) use ($self) {
-            $resolved = $self->get(strtolower($match[1]));
+        return str_replace('%%', '%', preg_replace_callback('/(?<!%)%([^%]+)%/', function ($match) use ($self, $resolving) {
+            $key = strtolower($match[1]);
+
+            if (isset($resolving[$key])) {
+                throw new ParameterCircularReferenceException(array_keys($resolving));
+            }
+
+            $resolved = $self->get($key);
 
             if (!is_string($resolved)) {
                 throw new \LogicException('A parameter cannot contain a non-string parameter.');
             }
 
-            return $self->resolveString($resolved);
+            $resolving[$key] = true;
+
+            return $self->resolveString($resolved, $resolving);
         }, $value));
     }
 }

+ 17 - 0
tests/Symfony/Tests/Component/DependencyInjection/ParameterBag/ParameterBagTest.php

@@ -13,6 +13,7 @@ namespace Symfony\Tests\Component\DependencyInjection\ParameterBag;
 
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
 use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
+use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
 
 class ParameterBagTest extends \PHPUnit_Framework_TestCase
 {
@@ -120,6 +121,22 @@ class ParameterBagTest extends \PHPUnit_Framework_TestCase
         } catch (\LogicException $e) {
             $this->assertEquals('A parameter cannot contain a non-string parameter.', $e->getMessage(), '->resolveValue() throws a LogicException when a parameter embeds another non-string parameter');
         }
+
+        $bag = new ParameterBag(array('foo' => '%bar%', 'bar' => '%foobar%', 'foobar' => '%foo%'));
+        try {
+            $bag->resolveValue('%foo%');
+            $this->fail('->resolveValue() throws a ParameterCircularReferenceException when a parameter has a circular reference');
+        } catch (ParameterCircularReferenceException $e) {
+            $this->assertEquals('Circular reference detected for parameter "foo" ("foo" > "bar" > "foobar" > "foo").', $e->getMessage(), '->resolveValue() throws a ParameterCircularReferenceException when a parameter has a circular reference');
+        }
+
+        $bag = new ParameterBag(array('foo' => 'a %bar%', 'bar' => 'a %foobar%', 'foobar' => 'a %foo%'));
+        try {
+            $bag->resolveValue('%foo%');
+            $this->fail('->resolveValue() throws a ParameterCircularReferenceException when a parameter has a circular reference');
+        } catch (ParameterCircularReferenceException $e) {
+            $this->assertEquals('Circular reference detected for parameter "foo" ("foo" > "bar" > "foobar" > "foo").', $e->getMessage(), '->resolveValue() throws a ParameterCircularReferenceException when a parameter has a circular reference');
+        }
     }
 
     /**