8000 [Profiler][Validator] Add a validator panel in profiler · symfony/symfony@ac5e884 · GitHub
[go: up one dir, main page]

Skip to content

Commit ac5e884

Browse files
committed
[Profiler][Validator] Add a validator panel in profiler
1 parent b223241 commit ac5e884

File tree

11 files changed

+551
-3
lines changed

11 files changed

+551
-3
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class FrameworkExtension extends Extension
8181
private $translationConfigEnabled = false;
8282
private $sessionConfigEnabled = false;
8383
private $annotationsConfigEnabled = false;
84+
private $validatorConfigEnabled = false;
8485

8586
/**
8687
* @var string|null
@@ -456,6 +457,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
456457
$loader->load('form_debug.xml');
457458
}
458459

460+
if ($this->validatorConfigEnabled) {
461+
$loader->load('validator_debug.xml');
462+
}
463+
459464
if ($this->translationConfigEnabled) {
460465
$loader->load('translation_debug.xml');
461466
$container->getDefinition('translator.data_collector')->setDecoratedService('translator');
@@ -1107,7 +1112,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
11071112
*/
11081113
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
11091114
{
1110-
if (!$this->isConfigEnabled($container, $config)) {
1115+
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
11111116
return;
11121117
}
11131118

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<defaults public="false" />
9+
10+
<service id="debug.validator" decorates="validator" decoration-priority="255" class="Symfony\Component\Validator\Validator\TraceableValidator">
11+
<argument type="service" id="debug.validator.inner" />
12+
</service>
13+
14+
<!-- DataCollector -->
15+
<service id="data_collector.validator" class="Symfony\Component\Validator\DataCollector\ValidatorDataCollector">
16+
<argument type="service" id="debug.validator"/>
17+
<tag name="data_collector" template="@WebProfiler/Collector/validator.html.twig" id="validator" priority="320" />
18+
</service>
19+
</services>
20+
</container>

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"symfony/stopwatch": "~2.8|~3.0|~4.0",
5050
"symfony/translation": "~3.2|~4.0",
5151
"symfony/templating": "~2.8|~3.0|~4.0",
52-
"symfony/validator": "~3.3|~4.0",
52+
"symfony/validator": "~3.4|~4.0",
5353
"symfony/var-dumper": "~3.3|~4.0",
5454
"symfony/workflow": "~3.3|~4.0",
5555
"symfony/yaml": "~3.2|~4.0",
@@ -69,7 +69,7 @@
6969
"symfony/property-info": "<3.3",
7070
"symfony/serializer": "<3.3",
7171
"symfony/translation": "<3.2",
72-
"symfony/validator": "<3.3",
72+
"symfony/validator": "<3.4",
7373
"symfony/workflow": "<3.3"
7474
},
7575
"suggest": {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
2+
3+
{% block toolbar %}
4+
{% if collector.violationsCount > 0 or collector.calls|length %}
5+
{% set status_color = collector.violationsCount ? 'red' : '' %}
6+
{% set icon %}
7+
{{ include('@WebProfiler/Icon/validator.svg') }}
8+
<span class="sf-toolbar-value">
9+
{{ collector.violationsCount }}
10+
</span>
11+
{% endset %}
12+
13+
{% set text %}
14+
<div class="sf-toolbar-info-piece">
15+
<b>Validator calls</b>
16+
<span class="sf-toolbar-status">{{ collector.calls|length }}</span>
17+
</div>
18+
<div class="sf-toolbar-info-piece">
19+
<b>Number of violations</b>
20+
<span class="sf-toolbar-status {{- collector.violationsCount > 0 ? ' sf-toolbar-status-red' }}">{{ collector.violationsCount }}</span>
21+
</div>
22+
{% endset %}
23+
24+
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
25+
{% endif %}
26+
{% endblock %}
27+
28+
{% block menu %}
29+
<span class="label {{- collector.violationsCount ? ' label-status-error' }} {{ collector.calls is empty ? 'disabled' }}">
30+
<span class="icon">{{ include('@WebProfiler/Icon/validator.svg') }}</span>
31+
<strong>Validator</strong>
32+
{% if collector.violationsCount > 0 %}
33+
<span class="count">
34+
<span>{{ collector.violationsCount }}</span>
35+
</span>
36+
{% endif %}
37+
</span>
38+
{% endblock %}
39+
40+
{% block panel %}
41+
<h2>Validator calls</h2>
42+
43+
{% for call in collector.calls %}
44+
<div class="sf-validator sf-reset">
45+
<span class="metadata">In
46+
{% set caller = call.caller %}
47+
{% if caller.line %}
48+
{% set link = caller.file|file_link(caller.line) %}
49+
{% if link %}
50+
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
51+
{% else %}
52+
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
53+
{% endif %}
54+
{% else %}
55+
{{ caller.name }}
56+
{% endif %}
57+
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>):
58+
</span>
59+
60+
<div class="sf-validator-compact hidden" id="sf-trace-{{ loop.index0 }}">
61+
<div class="trace">
62+
{{ caller.file|file_excerpt(caller.line) }}
63+
</div>
64+
</div>
65+
66+
<div class="sf-validator-compact hidden sf-validator-context" id="sf-context-{{ loop.index0 }}">
67+
{{ profiler_dump(call.context, maxDepth=1) }}
68+
</div>
69+
70+
{% if call.violations|length %}
71+
<table>
72+
<thead>
73+
<tr>
74+
<th>Path</th>
75+
<th>Message</th>
76+
<th>Invalid value</th>
77+
<th>Violation</th>
78+
</tr>
79+
</thead>
80+
{% for violation in call.violations %}
81+
<tr>
82+
<td>{{ violation.propertyPath }}</td>
83+
<td>{{ violation.message }}</td>
84+
<td>{{ profiler_dump(violation.seek('invalidValue')) }}</td>
85+
<td>{{ profiler_dump(violation) }}</td>
86+
</tr>
87+
{% endfor %}
88+
</table>
89+
{% else %}
90+
No violations
91+
{% endif %}
92+
</div>
93+
{% else %}
94+
<div class="empty">
95+
<p>No calls to the validator were collected during this request.</p>
96+
</div>
97+
{% endfor %}
98+
{% endblock %}
Lines changed: 1 addition & 0 deletions
Loading

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,31 @@ table.logs .metadata {
880880
white-space: pre-wrap;
881881
}
882882

883+
{# Validator panel
884+
========================================================================= #}
885+
886+
#collector-content .sf-validator {
887+
margin-bottom: 2em;
888+
}
889+
890+
#collector-content .sf-validator .sf-validator-context,
891+
#collector-content .sf-validator .trace {
892+
border: 1px solid #DDD;
893+
background: #FFF;
894+
padding: 10px;
895+
margin: 0.5em 0;
896+
}
897+
#collector-content .sf-validator .trace {
898+
font-size: 12px;
899+
}
900+
#collector-content .sf-validator .trace li {
901+
margin-bottom: 0;
902+
padding: 0;
903+
}
904+
#collector-content .sf-validator .trace li.selected {
905+
background: rgba(255, 255, 153, 0.5);
906+
}
907+
883908
{# Dump panel
884909
========================================================================= #}
885910
#collector-content .sf-dump {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\DataCollector;
13+
14+
use Symfony\Component\Form\FormInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
18+
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
19+
use Symfony\Component\Validator\Validator\TraceableValidator;
20+
use Symfony\Component\VarDumper\Caster\Caster;
21+
use Symfony\Component\VarDumper\Caster\ClassStub;
22+
use Symfony\Component\VarDumper\Cloner\Data;
23+
use Symfony\Component\VarDumper\Cloner\VarCloner;
24+
25+
/**
26+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
27+
*/
28+
class ValidatorDataCollector extends DataCollector implements LateDataCollectorInterface
29+
{
30+
private $validator;
31+
private $cloner;
32+
33+
public function __construct(TraceableValidator $validator)
34+
{
35+
$this->validator = $validator;
36+
$this->data = array(
37+
'calls' => array(),
38+
'violations_count' => 0,
39+
);
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function collect(Request $request, Response $response, \Exception $exception = null)
46+
{
47+
// Everything is collected once, on kernel terminate.
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function lateCollect()
54+
{
55+
$collected = $this->validator->getCollectedData();
56+
$this->data['calls'] = $this->cloneVar($collected);
57+
$this->data['violations_count'] += array_reduce($collected, function ($previous, $item) {
58+
return $previous += count($item['violations']);
59+
}, 0);
60+
}
61+
62+
public function getCalls()
63+
{
64+
return $this->data['calls'];
65+
}
66+
67+
public function getViolationsCount()
68+
{
69+
return $this->data['violations_count'];
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function getName()
76+
{
77+
return 'validator';
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
protected function cloneVar($var)
84+
{
85+
if ($var instanceof Data) {
86+
return $var;
87+
}
88+
89+
if (null === $this->cloner) {
90+
$this->cloner = new VarCloner();
91+
$this->cloner->setMaxItems(-1);
92+
$this->cloner->addCasters(array(
93+
FormInterface::class => function (FormInterface $f, array $a) {
94+
return array(
95+
Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
96+
Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(get_class($f->getConfig()->getType()->getInnerType())),
97+
Caster::PREFIX_VIRTUAL.'data' => $f->getData(),
98+
);
99+
},
100+
));
101+
}
102+
103+
return $this->cloner->cloneVar($var, Caster::EXCLUDE_VERBOSE);
104+
}
105+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\DataCollector;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\ConstraintViolation;
16+
use Symfony\Component\Validator\ConstraintViolationList;
17+
use Symfony\Component\Validator\DataCollector\ValidatorDataCollector;
18+
use Symfony\Component\Validator\Validator\TraceableValidator;
19+
use Symfony\Component\Validator\Validator\ValidatorInterface;
20+
21+
class ValidatorDataCollectorTest extends TestCase
22+
{
23+
public function testCollectsValidatorCalls()
24+
{
25+
$originalValidator = $this->createMock(ValidatorInterface::class);
26+
$validator = new TraceableValidator($originalValidator);
27+
28+
$collector = new ValidatorDataCollector($validator);
29+
30+
$violations = new ConstraintViolationList(array(
31+
$this->createMock(ConstraintViolation::class),
32+
$this->createMock(ConstraintViolation::class),
33+
));
34+
$originalValidator->method('validate')->willReturn($violations);
35+
36+
$validator->validate(new \stdClass());
37+
38+
$collector->lateCollect();
39+
40+
$calls = $collector->getCalls();
41+
42+
$this->assertCount(1, $calls);
43+
$this->assertSame(2, $collector->getViolationsCount());
44+
45+
$call = $calls[0];
46+
47+
$this->assertArrayHasKey('caller', $call);
48+
$this->assertArrayHasKey('context', $call);
49+
$this->assertArrayHasKey('violations', $call);
50+
$this->assertCount(2, $call['violations']);
51+
}
52+
53+
protected function createMock($classname)
54+
{
55+
return $this->getMockBuilder($classname)->disableOriginalConstructor()->getMock();
56+
}
57+
}

0 commit comments

Comments
 (0)
0