* * 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\Attribute\AsCommand; 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\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * A console command to debug Validators information. * * @author Loïc Frémont */ #[AsCommand(name: 'debug:validator', description: 'Display validation constraints for classes')] class DebugCommand extends Command { public function __construct( private MetadataFactoryInterface $validator, ) { parent::__construct(); } protected function configure(): void { $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') ->setHelp(<<<'EOF' The %command.name% 'App\Entity\Dummy' command dumps the validators for the dummy class. The %command.name% src/ 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) { $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('%s', $class); $rows = []; $dump = new Dumper($output); /** @var ClassMetadataInterface $classMetadata */ $classMetadata = $this->validator->getMetadataFor($class); foreach ($this->getClassConstraintsData($classMetadata) as $data) { $rows[] = [ '-', $data['class'], implode(', ', $data['groups']), $dump($data['options']), ]; } foreach ($this->getConstrainedPropertiesData($classMetadata) 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 getClassConstraintsData(ClassMetadataInterface $classMetadata): iterable { foreach ($classMetadata->getConstraints() as $constraint) { yield [ 'class' => $constraint::class, 'groups' => $constraint->groups, 'options' => $this->getConstraintOptions($constraint), ]; } } private function getConstrainedPropertiesData(ClassMetadataInterface $classMetadata): array { $data = []; 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) { $autoMapingStrategy = 'Not supported'; if ($metadata instanceof GenericMetadata) { $autoMapingStrategy = match ($metadata->getAutoMappingStrategy()) { AutoMappingStrategy::ENABLED => 'Enabled', AutoMappingStrategy::DISABLED => 'Disabled', AutoMappingStrategy::NONE => 'None', }; } $traversalStrategy = 'None'; if (TraversalStrategy::TRAVERSE === $metadata->getTraversalStrategy()) { $traversalStrategy = 'Traverse'; } if (TraversalStrategy::IMPLICIT === $metadata->getTraversalStrategy()) { $traversalStrategy = 'Implicit'; } $data[] = [ 'class' => 'property options', 'groups' => [], 'options' => [ 'cascadeStrategy' => CascadingStrategy::CASCADE === $metadata->getCascadingStrategy() ? 'Cascade' : 'None', 'autoMappingStrategy' => $autoMapingStrategy, 'traversalStrategy' => $traversalStrategy, ], ]; foreach ($metadata->getConstraints() as $constraint) { $data[] = [ 'class' => $constraint::class, '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; } ksort($options); 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 (!preg_match('/class +([^{ ]+)/', $fileContent, $matches)) { // no class found continue; } $className = trim($matches[1]); if (null !== $namespace) { $classes[] = $namespace.'\\'.$className; } else { $classes[] = $className; } } return $classes; } }