diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 8537497d6a..1769258215 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -81,6 +81,7 @@ class FrameworkExtension extends Extension
private $translationConfigEnabled = false;
private $sessionConfigEnabled = false;
private $annotationsConfigEnabled = false;
+ private $validatorConfigEnabled = false;
/**
* @var string|null
@@ -456,6 +457,10 @@ class FrameworkExtension extends Extension
$loader->load('form_debug.xml');
}
+ if ($this->validatorConfigEnabled) {
+ $loader->load('validator_debug.xml');
+ }
+
if ($this->translationConfigEnabled) {
$loader->load('translation_debug.xml');
$container->getDefinition('translator.data_collector')->setDecoratedService('translator');
@@ -1107,7 +1112,7 @@ class FrameworkExtension extends Extension
*/
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
- if (!$this->isConfigEnabled($container, $config)) {
+ if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
return;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml
new file mode 100644
index 0000000000..ac4724580a
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index d46d5e128b..9699ae85d6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -49,7 +49,7 @@
"symfony/stopwatch": "~2.8|~3.0|~4.0",
"symfony/translation": "~3.2|~4.0",
"symfony/templating": "~2.8|~3.0|~4.0",
- "symfony/validator": "~3.3|~4.0",
+ "symfony/validator": "~3.4|~4.0",
"symfony/var-dumper": "~3.3|~4.0",
"symfony/workflow": "~3.3|~4.0",
"symfony/yaml": "~3.2|~4.0",
@@ -69,7 +69,7 @@
"symfony/property-info": "<3.3",
"symfony/serializer": "<3.3",
"symfony/translation": "<3.2",
- "symfony/validator": "<3.3",
+ "symfony/validator": "<3.4",
"symfony/workflow": "<3.3"
},
"suggest": {
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig
new file mode 100644
index 0000000000..6b8ba44dac
--- /dev/null
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig
@@ -0,0 +1,98 @@
+{% extends '@WebProfiler/Profiler/layout.html.twig' %}
+
+{% block toolbar %}
+ {% if collector.violationsCount > 0 or collector.calls|length %}
+ {% set status_color = collector.violationsCount ? 'red' : '' %}
+ {% set icon %}
+ {{ include('@WebProfiler/Icon/validator.svg') }}
+
+ {{ collector.violationsCount }}
+
+ {% endset %}
+
+ {% set text %}
+
+ Validator calls
+ {{ collector.calls|length }}
+
+
+ Number of violations
+ {{ collector.violationsCount }}
+
+ {% endset %}
+
+ {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
+ {% endif %}
+{% endblock %}
+
+{% block menu %}
+
+ {{ include('@WebProfiler/Icon/validator.svg') }}
+ Validator
+ {% if collector.violationsCount > 0 %}
+
+ {{ collector.violationsCount }}
+
+ {% endif %}
+
+{% endblock %}
+
+{% block panel %}
+ Validator calls
+
+ {% for call in collector.calls %}
+
+
In
+ {% set caller = call.caller %}
+ {% if caller.line %}
+ {% set link = caller.file|file_link(caller.line) %}
+ {% if link %}
+ {{ caller.name }}
+ {% else %}
+ {{ caller.name }}
+ {% endif %}
+ {% else %}
+ {{ caller.name }}
+ {% endif %}
+ line {{ caller.line }} (context):
+
+
+
+
+ {{ caller.file|file_excerpt(caller.line) }}
+
+
+
+
+ {{ profiler_dump(call.context, maxDepth=1) }}
+
+
+ {% if call.violations|length %}
+
+
+
+ Path |
+ Message |
+ Invalid value |
+ Violation |
+
+
+ {% for violation in call.violations %}
+
+ {{ violation.propertyPath }} |
+ {{ violation.message }} |
+ {{ profiler_dump(violation.seek('invalidValue')) }} |
+ {{ profiler_dump(violation) }} |
+
+ {% endfor %}
+
+ {% else %}
+ No violations
+ {% endif %}
+
+ {% else %}
+
+
No calls to the validator were collected during this request.
+
+ {% endfor %}
+{% endblock %}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg
new file mode 100644
index 0000000000..b501fe5486
--- /dev/null
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg
@@ -0,0 +1 @@
+
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
index 5b6647df88..94ac2abd25 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
@@ -880,6 +880,31 @@ table.logs .metadata {
white-space: pre-wrap;
}
+{# Validator panel
+ ========================================================================= #}
+
+#collector-content .sf-validator {
+ margin-bottom: 2em;
+}
+
+#collector-content .sf-validator .sf-validator-context,
+#collector-content .sf-validator .trace {
+ border: 1px solid #DDD;
+ background: #FFF;
+ padding: 10px;
+ margin: 0.5em 0;
+}
+#collector-content .sf-validator .trace {
+ font-size: 12px;
+}
+#collector-content .sf-validator .trace li {
+ margin-bottom: 0;
+ padding: 0;
+}
+#collector-content .sf-validator .trace li.selected {
+ background: rgba(255, 255, 153, 0.5);
+}
+
{# Dump panel
========================================================================= #}
#collector-content .sf-dump {
diff --git a/src/Symfony/Component/Validator/DataCollector/ValidatorDataCollector.php b/src/Symfony/Component/Validator/DataCollector/ValidatorDataCollector.php
new file mode 100644
index 0000000000..02c672191e
--- /dev/null
+++ b/src/Symfony/Component/Validator/DataCollector/ValidatorDataCollector.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\DataCollector;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\DataCollector\DataCollector;
+use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
+use Symfony\Component\Validator\Validator\TraceableValidator;
+use Symfony\Component\VarDumper\Caster\Caster;
+use Symfony\Component\VarDumper\Caster\ClassStub;
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+
+/**
+ * @author Maxime Steinhausser
+ */
+class ValidatorDataCollector extends DataCollector implements LateDataCollectorInterface
+{
+ private $validator;
+ private $cloner;
+
+ public function __construct(TraceableValidator $validator)
+ {
+ $this->validator = $validator;
+ $this->data = array(
+ 'calls' => array(),
+ 'violations_count' => 0,
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function collect(Request $request, Response $response, \Exception $exception = null)
+ {
+ // Everything is collected once, on kernel terminate.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function lateCollect()
+ {
+ $collected = $this->validator->getCollectedData();
+ $this->data['calls'] = $this->cloneVar($collected);
+ $this->data['violations_count'] += array_reduce($collected, function ($previous, $item) {
+ return $previous += count($item['violations']);
+ }, 0);
+ }
+
+ public function getCalls()
+ {
+ return $this->data['calls'];
+ }
+
+ public function getViolationsCount()
+ {
+ return $this->data['violations_count'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'validator';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function cloneVar($var)
+ {
+ if ($var instanceof Data) {
+ return $var;
+ }
+
+ if (null === $this->cloner) {
+ $this->cloner = new VarCloner();
+ $this->cloner->setMaxItems(-1);
+ $this->cloner->addCasters(array(
+ FormInterface::class => function (FormInterface $f, array $a) {
+ return array(
+ Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
+ Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(get_class($f->getConfig()->getType()->getInnerType())),
+ Caster::PREFIX_VIRTUAL.'data' => $f->getData(),
+ );
+ },
+ ));
+ }
+
+ return $this->cloner->cloneVar($var, Caster::EXCLUDE_VERBOSE);
+ }
+}
diff --git a/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php b/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php
new file mode 100644
index 0000000000..811a55829a
--- /dev/null
+++ b/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Tests\DataCollector;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationList;
+use Symfony\Component\Validator\DataCollector\ValidatorDataCollector;
+use Symfony\Component\Validator\Validator\TraceableValidator;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+class ValidatorDataCollectorTest extends TestCase
+{
+ public function testCollectsValidatorCalls()
+ {
+ $originalValidator = $this->createMock(ValidatorInterface::class);
+ $validator = new TraceableValidator($originalValidator);
+
+ $collector = new ValidatorDataCollector($validator);
+
+ $violations = new ConstraintViolationList(array(
+ $this->createMock(ConstraintViolation::class),
+ $this->createMock(ConstraintViolation::class),
+ ));
+ $originalValidator->method('validate')->willReturn($violations);
+
+ $validator->validate(new \stdClass());
+
+ $collector->lateCollect();
+
+ $calls = $collector->getCalls();
+
+ $this->assertCount(1, $calls);
+ $this->assertSame(2, $collector->getViolationsCount());
+
+ $call = $calls[0];
+
+ $this->assertArrayHasKey('caller', $call);
+ $this->assertArrayHasKey('context', $call);
+ $this->assertArrayHasKey('violations', $call);
+ $this->assertCount(2, $call['violations']);
+ }
+
+ protected function createMock($classname)
+ {
+ return $this->getMockBuilder($classname)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php
new file mode 100644
index 0000000000..b2eef769ec
--- /dev/null
+++ b/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php
@@ -0,0 +1,104 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Tests\Validator;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationList;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Mapping\MetadataInterface;
+use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
+use Symfony\Component\Validator\Validator\TraceableValidator;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+class TraceableValidatorTest extends TestCase
+{
+ public function testValidate()
+ {
+ $originalValidator = $this->createMock(ValidatorInterface::class);
+ $violations = new ConstraintViolationList(array(
+ $this->createMock(ConstraintViolation::class),
+ $this->createMock(ConstraintViolation::class),
+ ));
+ $originalValidator->expects($this->exactly(2))->method('validate')->willReturn($violations);
+
+ $validator = new TraceableValidator($originalValidator);
+
+ $object = new \stdClass();
+ $constraints = array($this->createMock(Constraint::class));
+ $groups = array('Default', 'Create');
+
+ $validator->validate($object, $constraints, $groups);
+ $line = __LINE__ - 1;
+
+ $collectedData = $validator->getCollectedData();
+
+ $this->assertCount(1, $collectedData);
+
+ $callData = $collectedData[0];
+
+ $this->assertSame(iterator_to_array($violations), $callData['violations']);
+
+ $this->assertSame(array(
+ 'value' => $object,
+ 'constraints' => $constraints,
+ 'groups' => $groups,
+ ), $callData['context']);
+
+ $this->assertEquals(array(
+ 'name' => 'TraceableValidatorTest.php',
+ 'file' => __FILE__,
+ 'line' => $line,
+ ), $callData['caller']);
+
+ $validator->validate($object, $constraints, $groups);
+ $collectedData = $validator->getCollectedData();
+
+ $this->assertCount(2, $collectedData);
+ }
+
+ public function testForwardsToOriginalValidator()
+ {
+ $originalValidator = $this->createMock(ValidatorInterface::class);
+ $validator = new TraceableValidator($originalValidator);
+
+ $expects = function ($method) use ($originalValidator) { return $originalValidator->expects($this->once())->method($method); };
+
+ $expects('getMetadataFor')->willReturn($expected = $this->createMock(MetadataInterface::class));
+ $this->assertSame($expected, $validator->getMetadataFor('value'), 'returns original validator getMetadataFor() result');
+
+ $expects('hasMetadataFor')->willReturn($expected = false);
+ $this->assertSame($expected, $validator->hasMetadataFor('value'), 'returns original validator hasMetadataFor() result');
+
+ $expects('inContext')->willReturn($expected = $this->createMock(ContextualValidatorInterface::class));
+ $this->assertSame($expected, $validator->inContext($this->createMock(ExecutionContextInterface::class)), 'returns original validator inContext() result');
+
+ $expects('startContext')->willReturn($expected = $this->createMock(ContextualValidatorInterface::class));
+ $this->assertSame($expected, $validator->startContext(), 'returns original validator startContext() result');
+
+ $expects('validate')->willReturn($expected = $this->createMock(ConstraintViolationListInterface::class));
+ $this->assertSame($expected, $validator->validate('value'), 'returns original validator validate() result');
+
+ $expects('validateProperty')->willReturn($expected = $this->createMock(ConstraintViolationListInterface::class));
+ $this->assertSame($expected, $validator->validateProperty(new \stdClass(), 'property'), 'returns original validator validateProperty() result');
+
+ $expects('validatePropertyValue')->willReturn($expected = $this->createMock(ConstraintViolationListInterface::class));
+ $this->assertSame($expected, $validator->validatePropertyValue(new \stdClass(), 'property', 'value'), 'returns original validator validatePropertyValue() result');
+ }
+
+ protected function createMock($classname)
+ {
+ return $this->getMockBuilder($classname)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/src/Symfony/Component/Validator/Validator/TraceableValidator.php b/src/Symfony/Component/Validator/Validator/TraceableValidator.php
new file mode 100644
index 0000000000..019559ae00
--- /dev/null
+++ b/src/Symfony/Component/Validator/Validator/TraceableValidator.php
@@ -0,0 +1,131 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Validator;
+
+use Symfony\Component\Validator\ConstraintViolationList;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+
+/**
+ * Collects some data about validator calls.
+ *
+ * @author Maxime Steinhausser
+ */
+class TraceableValidator implements ValidatorInterface
+{
+ private $validator;
+ private $collectedData = array();
+
+ public function __construct(ValidatorInterface $validator)
+ {
+ $this->validator = $validator;
+ }
+
+ /**
+ * @return ConstraintViolationList[]
+ */
+ public function getCollectedData()
+ {
+ return $this->collectedData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadataFor($value)
+ {
+ return $this->validator->getMetadataFor($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasMetadataFor($value)
+ {
+ return $this->validator->hasMetadataFor($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value, $constraints = null, $groups = null)
+ {
+ $violations = $this->validator->validate($value, $constraints, $groups);
+
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7);
+
+ $file = $trace[0]['file'];
+ $line = $trace[0]['line'];
+
+ for ($i = 1; $i < 7; ++$i) {
+ if (isset($trace[$i]['class'], $trace[$i]['function'])
+ && 'validate' === $trace[$i]['function']
+ && is_a($trace[$i]['class'], ValidatorInterface::class, true)
+ ) {
+ $file = $trace[$i]['file'];
+ $line = $trace[$i]['line'];
+
+ while (++$i < 7) {
+ if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) {
+ $file = $trace[$i]['file'];
+ $line = $trace[$i]['line'];
+
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ $name = str_replace('\\', '/', $file);
+ $name = substr($name, strrpos($name, '/') + 1);
+
+ $this->collectedData[] = array(
+ 'caller' => compact('name', 'file', 'line'),
+ 'context' => compact('value', 'constraints', 'groups'),
+ 'violations' => iterator_to_array($violations),
+ );
+
+ return $violations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateProperty($object, $propertyName, $groups = null)
+ {
+ return $this->validator->validateProperty($object, $propertyName, $groups);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
+ {
+ return $this->validator->validatePropertyValue($objectOrClass, $propertyName, $value, $groups);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function startContext()
+ {
+ return $this->validator->startContext();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function inContext(ExecutionContextInterface $context)
+ {
+ return $this->validator->inContext($context);
+ }
+}
diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json
index 21c0a8a7cd..2b0a0cab29 100644
--- a/src/Symfony/Component/Validator/composer.json
+++ b/src/Symfony/Component/Validator/composer.json
@@ -22,6 +22,8 @@
},
"require-dev": {
"symfony/http-foundation": "~2.8|~3.0|~4.0",
+ "symfony/http-kernel": "~2.8|~3.0|~4.0.0",
+ "symfony/var-dumper": "~3.3|~4.0.0",
"symfony/intl": "^2.8.18|^3.2.5|~4.0",
"symfony/yaml": "~3.3|~4.0",
"symfony/config": "~2.8|~3.0|~4.0",