Skip to content

Commit f281244

Browse files
committed
[Validator] Debug validator command
1 parent e983035 commit f281244

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Helper\Table;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\Finder\Finder;
22+
use Symfony\Component\Validator\Constraint;
23+
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
24+
use Symfony\Component\Validator\Validator\ValidatorInterface;
25+
26+
/**
27+
* A console command to debug Validators information.
28+
*
29+
* @author Loïc Frémont <lc.fremont@gmail.com>
30+
*/
31+
class DebugCommand extends Command
32+
{
33+
protected static $defaultName = 'debug:validator';
34+
35+
private $validator;
36+
37+
public function __construct(ValidatorInterface $validator)
38+
{
39+
parent::__construct();
40+
41+
$this->validator = $validator;
42+
}
43+
44+
protected function configure()
45+
{
46+
$this
47+
->addArgument('class', InputArgument::OPTIONAL, 'A class to debug')
48+
->addOption('path', null, InputOption::VALUE_OPTIONAL, 'A path for the classes to debug', 'src/Entity')
49+
->addOption('with-results-only', null, InputOption::VALUE_NONE, 'Show only classes with results')
50+
->setDescription('Displays validators for classes')
51+
->setHelp(<<<'EOF'
52+
The <info>%command.name%</info> command dumps the validators configuration for all classes in
53+
`src/Entity` directory.
54+
55+
The <info>%command.name% 'App\Entity\Dummy'</info> command dumps the validators for the dummy class.
56+
57+
The <info>%command.name% --path=src/</info> command dumps the validators for the `src` directory.
58+
EOF
59+
)
60+
;
61+
}
62+
63+
protected function execute(InputInterface $input, OutputInterface $output): int
64+
{
65+
$class = $input->getArgument('class');
66+
$path = $input->getOption('path');
67+
68+
if (null !== $class) {
69+
$this->dumpValidatorsForClass($input, $output, $class);
70+
71+
return 0;
72+
}
73+
74+
if (null !== $path) {
75+
foreach ($this->getResourcesByPath($path) as $class) {
76+
$this->dumpValidatorsForClass($input, $output, $class);
77+
}
78+
79+
return 0;
80+
}
81+
82+
return 0;
83+
}
84+
85+
private function dumpValidatorsForClass(InputInterface $input, OutputInterface $output, string $class): void
86+
{
87+
$io = new SymfonyStyle($input, $output);
88+
$title = sprintf('Information for class "<info>%s</info>"', $class);
89+
$rows = [];
90+
91+
foreach ($this->getConstrainedPropertiesData($class) as $propertyName => $constraintsData) {
92+
foreach ($constraintsData as $data) {
93+
$rows[] = [$propertyName, $data['class'], implode(', ', $data['groups']), json_encode($data['options'])];
94+
}
95+
}
96+
97+
if (empty($rows)) {
98+
if ($input->getOption('with-results-only')) {
99+
return;
100+
}
101+
102+
$io->title($title);
103+
$io->text('No validators were found for this class.');
104+
105+
return;
106+
}
107+
108+
$io->title($title);
109+
110+
$table = new Table($output);
111+
$table->setHeaders(['Property', 'Name', 'Groups', 'Options']);
112+
$table->setRows($rows);
113+
$table->setColumnMaxWidth(3, 80);
114+
$table->render();
115+
}
116+
117+
private function getConstrainedPropertiesData(string $class): array
118+
{
119+
$data = [];
120+
121+
/** @var ClassMetadataInterface $classMetadata */
122+
$classMetadata = $this->validator->getMetadataFor($class);
123+
124+
foreach ($classMetadata->getConstrainedProperties() as $constrainedProperty) {
125+
$data[$constrainedProperty] = $this->getPropertyData($classMetadata, $constrainedProperty);
126+
}
127+
128+
return $data;
129+
}
130+
131+
private function getPropertyData(ClassMetadataInterface $classMetadata, string $constrainedProperty): array
132+
{
133+
$data = [];
134+
135+
$propertyMetadata = $classMetadata->getPropertyMetadata($constrainedProperty);
136+
foreach ($propertyMetadata as $metadata) {
137+
foreach ($metadata->getConstraints() as $constraint) {
138+
$data[] = [
139+
'class' => \get_class($constraint),
140+
'groups' => $constraint->groups,
141+
'options' => $this->getConstraintOptions($constraint),
142+
];
143+
}
144+
}
145+
146+
return $data;
147+
}
148+
149+
private function getConstraintOptions(Constraint $constraint): array
150+
{
151+
$options = [];
152+
153+
$constraintClass = \get_class($constraint);
154+
155+
$reflect = new \ReflectionClass($constraintClass);
156+
foreach ($reflect->getProperties() as $property) {
157+
if (!$property->isPublic()) {
158+
continue;
159+
}
160+
161+
$propertyName = $property->getName();
162+
try {
163+
$options[$propertyName] = $constraint->$propertyName;
164+
} catch (\Exception $exception) {
165+
}
166+
}
167+
168+
return $options;
169+
}
170+
171+
private function getResourcesByPath(string $path): array
172+
{
173+
$finder = new Finder();
174+
$finder->in($path)->name('*.php')->sortByName(true);
175+
$classes = [];
176+
177+
foreach ($finder as $file) {
178+
$fileContent = file_get_contents($file->getRealPath());
179+
180+
preg_match('/namespace (.+);/', $fileContent, $matches);
181+
182+
$namespace = $matches[1] ?? null;
183+
184+
if (false == preg_match('/class (.+)/', $fileContent, $matches)) {
185+
// no class found
186+
continue;
187+
}
188+
189+
$className = explode(' ', $matches[1])[0];
190+
191+
if (null !== $namespace) {
192+
$classes[] = $namespace.'\\'.$className;
193+
} else {
194+
$classes[] = $className;
195+
}
196+
}
197+
198+
return $classes;
199+
}
200+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Tester\CommandTester;
16+
use Symfony\Component\Validator\Command\DebugCommand;
17+
use Symfony\Component\Validator\Constraints\Email;
18+
use Symfony\Component\Validator\Constraints\NotBlank;
19+
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
20+
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
21+
use Symfony\Component\Validator\Tests\Dummy\DummyClassOne;
22+
use Symfony\Component\Validator\Validator\ValidatorInterface;
23+
24+
/**
25+
* @author Loïc Frémont <lc.fremont@gmail.com>
26+
*/
27+
class DebugCommandTest extends TestCase
28+
{
29+
public function testOutputWithClassArgument(): void
30+
{
31+
$validator = $this->createMock(ValidatorInterface::class);
32+
$classMetadata = $this->createMock(ClassMetadataInterface::class);
33+
$propertyMetadata = $this->createMock(PropertyMetadataInterface::class);
34+
35+
$validator
36+
->expects($this->once())
37+
->method('getMetadataFor')
38+
->with(DummyClassOne::class)
39+
->willReturn($classMetadata);
40+
41+
$classMetadata
42+
->expects($this->once())
43+
->method('getConstrainedProperties')
44+
->willReturn([
45+
'firstArgument',
46+
]);
47+
48+
$classMetadata
49+
->expects($this->once())
50+
->method('getPropertyMetadata')
51+
->with('firstArgument')
52+
->willReturn([
53+
$propertyMetadata,
54+
]);
55+
56+
$propertyMetadata
57+
->expects($this->once())
58+
->method('getConstraints')
59+
->willReturn([new NotBlank(), new Email()]);
60+
61+
$command = new DebugCommand($validator);
62+
63+
$tester = new CommandTester($command);
64+
$tester->execute(['class' => DummyClassOne::class], ['decorated' => false]);
65+
66+
$this->assertSame(<<<TXT
67+
68+
Information for class "Symfony\Component\Validator\Tests\Dummy\DummyClassOne"
69+
=============================================================================
70+
71+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
72+
| Property | Name | Groups | Options |
73+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
74+
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | {"message":"This value should not be blank.","allowNull":false,"normalizer":null |
75+
| | | | ,"payload":null} |
76+
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | {"message":"This value is not a valid email address.","mode":null,"normalizer":n |
77+
| | | | ull,"payload":null} |
78+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
79+
80+
TXT
81+
, $tester->getDisplay(true)
82+
);
83+
}
84+
85+
public function testOutputWithPathArgument(): void
86+
{
87+
$validator = $this->createMock(ValidatorInterface::class);
88+
$classMetadata = $this->createMock(ClassMetadataInterface::class);
89+
$secondClassMetadata = $this->createMock(ClassMetadataInterface::class);
90+
$propertyMetadata = $this->createMock(PropertyMetadataInterface::class);
91+
92+
$validator
93+
->expects($this->exactly(2))
94+
->method('getMetadataFor')
95+
->withAnyParameters()
96+
->willReturn($classMetadata);
97+
98+
$classMetadata
99+
->method('getConstrainedProperties')
100+
->willReturn([
101+
'firstArgument',
102+
]);
103+
104+
$classMetadata
105+
->method('getPropertyMetadata')
106+
->with('firstArgument')
107+
->willReturn([
108+
$propertyMetadata,
109+
]);
110+
111+
$propertyMetadata
112+
->method('getConstraints')
113+
->willReturn([new NotBlank(), new Email()]);
114+
115+
$command = new DebugCommand($validator);
116+
117+
$tester = new CommandTester($command);
118+
$tester->execute(['--path' => __DIR__.'/../Dummy'], ['decorated' => false]);
119+
120+
$this->assertSame(<<<TXT
121+
122+
Information for class "Symfony\Component\Validator\Tests\Dummy\DummyClassOne"
123+
=============================================================================
124+
125+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
126+
| Property | Name | Groups | Options |
127+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
128+
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | {"message":"This value should not be blank.","allowNull":false,"normalizer":null |
129+
| | | | ,"payload":null} |
130+
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | {"message":"This value is not a valid email address.","mode":null,"normalizer":n |
131+
| | | | ull,"payload":null} |
132+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
133+
134+
Information for class "Symfony\Component\Validator\Tests\Dummy\DummyClassTwo"
135+
=============================================================================
136+
137+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
138+
| Property | Name | Groups | Options |
139+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
140+
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | {"message":"This value should not be blank.","allowNull":false,"normalizer":null |
141+
| | | | ,"payload":null} |
142+
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | {"message":"This value is not a valid email address.","mode":null,"normalizer":n |
143+
| | | | ull,"payload":null} |
144+
+---------------+--------------------------------------------------+---------+----------------------------------------------------------------------------------+
145+
146+
TXT
147+
, $tester->getDisplay(true)
148+
);
149+
}
150+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Dummy;
4+
5+
class DummyClassOne
6+
{
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Dummy;
4+
5+
class DummyClassTwo
6+
{
7+
}

0 commit comments

Comments
 (0)