[Profiler][Validator] Add a validator panel in profiler

This commit is contained in:
Maxime Steinhausser 2017-04-27 17:08:18 +02:00
parent b223241f64
commit ac5e884f36
11 changed files with 551 additions and 3 deletions

View File

@ -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;
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="false" />
<service id="debug.validator" decorates="validator" decoration-priority="255" class="Symfony\Component\Validator\Validator\TraceableValidator">
<argument type="service" id="debug.validator.inner" />
</service>
<!-- DataCollector -->
<service id="data_collector.validator" class="Symfony\Component\Validator\DataCollector\ValidatorDataCollector">
<argument type="service" id="debug.validator"/>
<tag name="data_collector" template="@WebProfiler/Collector/validator.html.twig" id="validator" priority="320" />
</service>
</services>
</container>

View File

@ -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": {

View File

@ -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') }}
<span class="sf-toolbar-value">
{{ collector.violationsCount }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Validator calls</b>
<span class="sf-toolbar-status">{{ collector.calls|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of violations</b>
<span class="sf-toolbar-status {{- collector.violationsCount > 0 ? ' sf-toolbar-status-red' }}">{{ collector.violationsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{- collector.violationsCount ? ' label-status-error' }} {{ collector.calls is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/validator.svg') }}</span>
<strong>Validator</strong>
{% if collector.violationsCount > 0 %}
<span class="count">
<span>{{ collector.violationsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Validator calls</h2>
{% for call in collector.calls %}
<div class="sf-validator sf-reset">
<span class="metadata">In
{% set caller = call.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <a class="text-small sf-toggle" data-toggle-selector="#sf-trace-{{ loop.index0 }}">{{ caller.line }}</a> (<a class="text-small sf-toggle" data-toggle-selector="#sf-context-{{ loop.index0 }}">context</a>):
</span>
<div class="sf-validator-compact hidden" id="sf-trace-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line) }}
</div>
</div>
<div class="sf-validator-compact hidden sf-validator-context" id="sf-context-{{ loop.index0 }}">
{{ profiler_dump(call.context, maxDepth=1) }}
</div>
{% if call.violations|length %}
<table>
<thead>
<tr>
<th>Path</th>
<th>Message</th>
<th>Invalid value</th>
<th>Violation</th>
</tr>
</thead>
{% for violation in call.violations %}
<tr>
<td>{{ violation.propertyPath }}</td>
<td>{{ violation.message }}</td>
<td>{{ profiler_dump(violation.seek('invalidValue')) }}</td>
<td>{{ profiler_dump(violation) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No violations
{% endif %}
</div>
{% else %}
<div class="empty">
<p>No calls to the validator were collected during this request.</p>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"><path fill="#aaaaaa" d="M19.54,22.5H4.29a2.88,2.88,0,0,1-2.87-2.87V4.37A2.88,2.88,0,0,1,4.29,1.5H18.54a1,1,0,0,1,0,2H4.29a.88.88,0,0,0-.87.87V19.63a.88.88,0,0,0,.87.87H19.54a.88.88,0,0,0,.87-.87V11.29a1,1,0,1,1,2,0v8.33A2.88,2.88,0,0,1,19.54,22.5ZM13,17.29,22.88,6a1.5,1.5,0,1,0-2.26-2L12,14,8,9.11A1.5,1.5,0,0,0,5.65,11l5.1,6.25a1.5,1.5,0,0,0,1.14.55h0A1.5,1.5,0,0,0,13,17.29Z"/></svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -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 {

View File

@ -0,0 +1,105 @@
<?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\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 <maxime.steinhausser@gmail.com>
*/
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);
}
}

View File

@ -0,0 +1,57 @@
<?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\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();
}
}

View File

@ -0,0 +1,104 @@
<?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\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();
}
}

View File

@ -0,0 +1,131 @@
<?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\Validator\Validator;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Collects some data about validator calls.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
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);
}
}

View File

@ -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",