[Validator] Debug validator command
This commit is contained in:
parent
e983035e1a
commit
5dd85e4371
@ -1217,6 +1217,8 @@ class FrameworkExtension extends Extension
|
|||||||
private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled)
|
private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled)
|
||||||
{
|
{
|
||||||
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
|
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
|
||||||
|
$container->removeDefinition('console.command.validator_debug');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
|
|||||||
use Symfony\Component\Messenger\Command\SetupTransportsCommand;
|
use Symfony\Component\Messenger\Command\SetupTransportsCommand;
|
||||||
use Symfony\Component\Messenger\Command\StopWorkersCommand;
|
use Symfony\Component\Messenger\Command\StopWorkersCommand;
|
||||||
use Symfony\Component\Translation\Command\XliffLintCommand;
|
use Symfony\Component\Translation\Command\XliffLintCommand;
|
||||||
|
use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand;
|
||||||
|
|
||||||
return static function (ContainerConfigurator $container) {
|
return static function (ContainerConfigurator $container) {
|
||||||
$container->services()
|
$container->services()
|
||||||
@ -225,6 +226,12 @@ return static function (ContainerConfigurator $container) {
|
|||||||
])
|
])
|
||||||
->tag('console.command', ['command' => 'translation:update'])
|
->tag('console.command', ['command' => 'translation:update'])
|
||||||
|
|
||||||
|
->set('console.command.validator_debug', ValidatorDebugCommand::class)
|
||||||
|
->args([
|
||||||
|
service('validator'),
|
||||||
|
])
|
||||||
|
->tag('console.command', ['command' => 'debug:validator'])
|
||||||
|
|
||||||
->set('console.command.workflow_dump', WorkflowDumpCommand::class)
|
->set('console.command.workflow_dump', WorkflowDumpCommand::class)
|
||||||
->tag('console.command', ['command' => 'workflow:dump'])
|
->tag('console.command', ['command' => 'workflow:dump'])
|
||||||
|
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"symfony/polyfill-php80": "^1.15",
|
"symfony/polyfill-php80": "^1.15",
|
||||||
"symfony/filesystem": "^4.4|^5.0",
|
"symfony/filesystem": "^4.4|^5.0",
|
||||||
"symfony/finder": "^4.4|^5.0",
|
"symfony/finder": "^4.4|^5.0",
|
||||||
"symfony/routing": "^5.1"
|
"symfony/routing": "^5.1",
|
||||||
|
"symfony/validator": "^5.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/annotations": "~1.7",
|
"doctrine/annotations": "~1.7",
|
||||||
@ -57,7 +58,6 @@
|
|||||||
"symfony/string": "^5.0",
|
"symfony/string": "^5.0",
|
||||||
"symfony/translation": "^5.0",
|
"symfony/translation": "^5.0",
|
||||||
"symfony/twig-bundle": "^4.4|^5.0",
|
"symfony/twig-bundle": "^4.4|^5.0",
|
||||||
"symfony/validator": "^4.4|^5.0",
|
|
||||||
"symfony/workflow": "^5.2",
|
"symfony/workflow": "^5.2",
|
||||||
"symfony/yaml": "^4.4|^5.0",
|
"symfony/yaml": "^4.4|^5.0",
|
||||||
"symfony/property-info": "^4.4|^5.0",
|
"symfony/property-info": "^4.4|^5.0",
|
||||||
|
200
src/Symfony/Component/Validator/Command/DebugCommand.php
Normal file
200
src/Symfony/Component/Validator/Command/DebugCommand.php
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<?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\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\Dumper;
|
||||||
|
use Symfony\Component\Console\Helper\Table;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
|
||||||
|
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A console command to debug Validators information.
|
||||||
|
*
|
||||||
|
* @author Loïc Frémont <lc.fremont@gmail.com>
|
||||||
|
*/
|
||||||
|
class DebugCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'debug:validator';
|
||||||
|
|
||||||
|
private $validator;
|
||||||
|
|
||||||
|
public function __construct(MetadataFactoryInterface $validator)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->validator = $validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('class', InputArgument::REQUIRED, 'A fully qualified class name or a path')
|
||||||
|
->addOption('show-all', null, InputOption::VALUE_NONE, 'Show all classes even if they have no validation constraints')
|
||||||
|
->setDescription('Displays validation constraints for classes')
|
||||||
|
->setHelp(<<<'EOF'
|
||||||
|
The <info>%command.name% 'App\Entity\Dummy'</info> command dumps the validators for the dummy class.
|
||||||
|
|
||||||
|
The <info>%command.name% src/</info> command dumps the validators for the `src` directory.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$class = $input->getArgument('class');
|
||||||
|
|
||||||
|
if (class_exists($class)) {
|
||||||
|
$this->dumpValidatorsForClass($input, $output, $class);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($this->getResourcesByPath($class) as $class) {
|
||||||
|
$this->dumpValidatorsForClass($input, $output, $class);
|
||||||
|
}
|
||||||
|
} catch (DirectoryNotFoundException $exception) {
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$io->error(sprintf('Neither class nor path were found with "%s" argument.', $input->getArgument('class')));
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dumpValidatorsForClass(InputInterface $input, OutputInterface $output, string $class): void
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$title = sprintf('<info>%s</info>', $class);
|
||||||
|
$rows = [];
|
||||||
|
$dump = new Dumper($output);
|
||||||
|
|
||||||
|
foreach ($this->getConstrainedPropertiesData($class) as $propertyName => $constraintsData) {
|
||||||
|
foreach ($constraintsData as $data) {
|
||||||
|
$rows[] = [
|
||||||
|
$propertyName,
|
||||||
|
$data['class'],
|
||||||
|
implode(', ', $data['groups']),
|
||||||
|
$dump($data['options']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$rows) {
|
||||||
|
if (false === $input->getOption('show-all')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->section($title);
|
||||||
|
$io->text('No validators were found for this class.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->section($title);
|
||||||
|
|
||||||
|
$table = new Table($output);
|
||||||
|
$table->setHeaders(['Property', 'Name', 'Groups', 'Options']);
|
||||||
|
$table->setRows($rows);
|
||||||
|
$table->setColumnMaxWidth(3, 80);
|
||||||
|
$table->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConstrainedPropertiesData(string $class): array
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
/** @var ClassMetadataInterface $classMetadata */
|
||||||
|
$classMetadata = $this->validator->getMetadataFor($class);
|
||||||
|
|
||||||
|
foreach ($classMetadata->getConstrainedProperties() as $constrainedProperty) {
|
||||||
|
$data[$constrainedProperty] = $this->getPropertyData($classMetadata, $constrainedProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPropertyData(ClassMetadataInterface $classMetadata, string $constrainedProperty): array
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$propertyMetadata = $classMetadata->getPropertyMetadata($constrainedProperty);
|
||||||
|
foreach ($propertyMetadata as $metadata) {
|
||||||
|
foreach ($metadata->getConstraints() as $constraint) {
|
||||||
|
$data[] = [
|
||||||
|
'class' => \get_class($constraint),
|
||||||
|
'groups' => $constraint->groups,
|
||||||
|
'options' => $this->getConstraintOptions($constraint),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConstraintOptions(Constraint $constraint): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
foreach (array_keys(get_object_vars($constraint)) as $propertyName) {
|
||||||
|
// Groups are dumped on a specific column.
|
||||||
|
if ('groups' === $propertyName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options[$propertyName] = $constraint->$propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getResourcesByPath(string $path): array
|
||||||
|
{
|
||||||
|
$finder = new Finder();
|
||||||
|
$finder->files()->in($path)->name('*.php')->sortByName(true);
|
||||||
|
$classes = [];
|
||||||
|
|
||||||
|
foreach ($finder as $file) {
|
||||||
|
$fileContent = file_get_contents($file->getRealPath());
|
||||||
|
|
||||||
|
preg_match('/namespace (.+);/', $fileContent, $matches);
|
||||||
|
|
||||||
|
$namespace = $matches[1] ?? null;
|
||||||
|
|
||||||
|
if (false === preg_match('/class +([^{ ]+)/', $fileContent, $matches)) {
|
||||||
|
// no class found
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$className = trim($matches[1]);
|
||||||
|
|
||||||
|
if (null !== $namespace) {
|
||||||
|
$classes[] = $namespace.'\\'.$className;
|
||||||
|
} else {
|
||||||
|
$classes[] = $className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
<?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\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Symfony\Component\Validator\Command\DebugCommand;
|
||||||
|
use Symfony\Component\Validator\Constraints\Email;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
|
||||||
|
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
|
||||||
|
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
|
||||||
|
use Symfony\Component\Validator\Tests\Dummy\DummyClassOne;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Loïc Frémont <lc.fremont@gmail.com>
|
||||||
|
*/
|
||||||
|
class DebugCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testOutputWithClassArgument(): void
|
||||||
|
{
|
||||||
|
$validator = $this->createMock(MetadataFactoryInterface::class);
|
||||||
|
$classMetadata = $this->createMock(ClassMetadataInterface::class);
|
||||||
|
$propertyMetadata = $this->createMock(PropertyMetadataInterface::class);
|
||||||
|
|
||||||
|
$validator
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getMetadataFor')
|
||||||
|
->with(DummyClassOne::class)
|
||||||
|
->willReturn($classMetadata);
|
||||||
|
|
||||||
|
$classMetadata
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getConstrainedProperties')
|
||||||
|
->willReturn([
|
||||||
|
'firstArgument',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$classMetadata
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getPropertyMetadata')
|
||||||
|
->with('firstArgument')
|
||||||
|
->willReturn([
|
||||||
|
$propertyMetadata,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$propertyMetadata
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getConstraints')
|
||||||
|
->willReturn([new NotBlank(), new Email()]);
|
||||||
|
|
||||||
|
$command = new DebugCommand($validator);
|
||||||
|
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute(['class' => DummyClassOne::class], ['decorated' => false]);
|
||||||
|
|
||||||
|
$this->assertSame(<<<TXT
|
||||||
|
|
||||||
|
Symfony\Component\Validator\Tests\Dummy\DummyClassOne
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| Property | Name | Groups | Options |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | [ |
|
||||||
|
| | | | "message" => "This value should not be blank.", |
|
||||||
|
| | | | "allowNull" => false, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | [ |
|
||||||
|
| | | | "message" => "This value is not a valid email address.", |
|
||||||
|
| | | | "mode" => null, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
|
||||||
|
TXT
|
||||||
|
, $tester->getDisplay(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOutputWithPathArgument(): void
|
||||||
|
{
|
||||||
|
$validator = $this->createMock(MetadataFactoryInterface::class);
|
||||||
|
$classMetadata = $this->createMock(ClassMetadataInterface::class);
|
||||||
|
$propertyMetadata = $this->createMock(PropertyMetadataInterface::class);
|
||||||
|
|
||||||
|
$validator
|
||||||
|
->expects($this->exactly(2))
|
||||||
|
->method('getMetadataFor')
|
||||||
|
->withAnyParameters()
|
||||||
|
->willReturn($classMetadata);
|
||||||
|
|
||||||
|
$classMetadata
|
||||||
|
->method('getConstrainedProperties')
|
||||||
|
->willReturn([
|
||||||
|
'firstArgument',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$classMetadata
|
||||||
|
->method('getPropertyMetadata')
|
||||||
|
->with('firstArgument')
|
||||||
|
->willReturn([
|
||||||
|
$propertyMetadata,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$propertyMetadata
|
||||||
|
->method('getConstraints')
|
||||||
|
->willReturn([new NotBlank(), new Email()]);
|
||||||
|
|
||||||
|
$command = new DebugCommand($validator);
|
||||||
|
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute(['class' => __DIR__.'/../Dummy'], ['decorated' => false]);
|
||||||
|
|
||||||
|
$this->assertSame(<<<TXT
|
||||||
|
|
||||||
|
Symfony\Component\Validator\Tests\Dummy\DummyClassOne
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| Property | Name | Groups | Options |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | [ |
|
||||||
|
| | | | "message" => "This value should not be blank.", |
|
||||||
|
| | | | "allowNull" => false, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | [ |
|
||||||
|
| | | | "message" => "This value is not a valid email address.", |
|
||||||
|
| | | | "mode" => null, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
|
||||||
|
Symfony\Component\Validator\Tests\Dummy\DummyClassTwo
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| Property | Name | Groups | Options |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\NotBlank | Default | [ |
|
||||||
|
| | | | "message" => "This value should not be blank.", |
|
||||||
|
| | | | "allowNull" => false, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
| firstArgument | Symfony\Component\Validator\Constraints\Email | Default | [ |
|
||||||
|
| | | | "message" => "This value is not a valid email address.", |
|
||||||
|
| | | | "mode" => null, |
|
||||||
|
| | | | "normalizer" => null, |
|
||||||
|
| | | | "payload" => null |
|
||||||
|
| | | | ] |
|
||||||
|
+---------------+--------------------------------------------------+---------+------------------------------------------------------------+
|
||||||
|
|
||||||
|
TXT
|
||||||
|
, $tester->getDisplay(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOutputWithInvalidClassArgument(): void
|
||||||
|
{
|
||||||
|
$validator = $this->createMock(MetadataFactoryInterface::class);
|
||||||
|
|
||||||
|
$command = new DebugCommand($validator);
|
||||||
|
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute(['class' => 'App\\NotFoundResource'], ['decorated' => false]);
|
||||||
|
|
||||||
|
$this->assertStringContainsString(<<<TXT
|
||||||
|
Neither class nor path were found with "App\NotFoundResource" argument.
|
||||||
|
TXT
|
||||||
|
, $tester->getDisplay(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Validator\Tests\Dummy;
|
||||||
|
|
||||||
|
class DummyClassOne
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Validator\Tests\Dummy;
|
||||||
|
|
||||||
|
class DummyClassTwo
|
||||||
|
{
|
||||||
|
}
|
Reference in New Issue
Block a user