Skip to content

Commit

Permalink
[Profiler][Validator] Add a validator panel in profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
ogizanagi committed Jun 19, 2017
1 parent b223241 commit ac5e884
Show file tree
Hide file tree
Showing 11 changed files with 551 additions and 3 deletions.
Expand Up @@ -81,6 +81,7 @@ class FrameworkExtension extends Extension
private $translationConfigEnabled = false;
private $sessionConfigEnabled = false;
private $annotationsConfigEnabled = false;
private $validatorConfigEnabled = false;

/**
* @var string|null
Expand Down Expand Up @@ -456,6 +457,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
$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');
Expand Down Expand Up @@ -1107,7 +1112,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
*/
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!$this->isConfigEnabled($container, $config)) {
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
return;
}

Expand Down
@@ -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>
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
@@ -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 %}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -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 {
Expand Down
@@ -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);
}
}
@@ -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();
}
}

0 comments on commit ac5e884

Please sign in to comment.