A while back, I added a post which discussed how to validate PHP's scripts to ensure that all attributes had an available class. My reason for doing so is annotations will throw an error yet attributes will not work but will not provide a warning and are difficult to troubleshot. For example, I will get a warning for $propertyWithError but not for $propertyWithoutError.
<?php
namespace App\Entity;
// use Symfony\Component\Validator\Constraints as Assert;
class SomeEntity
{
/**
* @Assert\NotBlank
*/
private string $propertyWithError;
#[Assert\NotBlank]
private string $propertyWithoutError;
}
I then created the a composer package which would parse all the PHP scripts and look for attributes without existing classes.
I then created a Symfony command script (App\Command/AttributeValidator) which would use my package and at the CLI I could execute: bin/console app:attribute-validator
<?php
declare(strict_types=1);
namespace App\Command;
use Nette\Utils\Strings;
use Nette\Utils\Json;
use RuntimeException;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use NotionCommotion\AttributeValidator\AttributeValidator as NotionCommotionAttributeValidator;
use NotionCommotion\AttributeValidator\AttributeValidatorException;
final class AttributeValidator extends Command
{
private const COMMANDS = ['validate', 'getClassesWithUndeclaredAttributes', 'getClassesWithoutUndeclaredAttributes', 'getSuspectClasses', 'getNotFoundClasses', 'getTraits', 'getInterfaces', 'getAbstracts', 'jsonSerialize', 'debugSuspectFiles', 'debugFile'];
// the name of the command (the part after "bin/console")
/**
* @var string
*/
protected static $defaultName = 'app:attribute-validator';
protected function configure(): void
{
$this
->setDescription('Attribute Validator. --help')
->setHelp('This command allows you to check for PHP8 attributes without classes')
->addArgument('path', InputArgument::OPTIONAL, 'Path to check', 'src')
->addOption('command', null, InputOption::VALUE_REQUIRED, 'What command to run?', 'validate')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$command = $input->getOption('command');
if(!in_array($command, self::COMMANDS)) {
throw new \Exception(sprintf('Invalid command %s. Only %s are allowed', $command, implode(', ', self::COMMANDS)));
}
if($command==='validate') {
return $this->$command($input, $output);
}
else {
$output->writeln([
'Attribute Validator',
'============',
'',
]);
$output->writeln("Command: ".$command);
$path = $input->getArgument('path');
$output->writeln("Path to check: ".$path);
if($command==='debugFile'){
print_r(NotionCommotionAttributeValidator::debugFile($path));
}
else{
print_r(NotionCommotionAttributeValidator::create($path)->$command());
}
return Command::SUCCESS;
}
}
private function validate(InputInterface $input, OutputInterface $output): int
{
$output->writeln([
'Attribute Validator',
'============',
'',
]);
$path = $input->getArgument('path');
$output->writeln("Path to check: ".$path);
$helper = $this->getHelper('question');
/*
$question = new ConfirmationQuestion('Continue with this action?', true);
if (!$helper->ask($input, $output, $question)) {
return Command::SUCCESS;
}
*/
$validator = NotionCommotionAttributeValidator::create($path);
$errors=0;
foreach($validator->validate() as $type=>$errs) {
$output->writeln($type.' errors');
switch($type) {
case 'classesWithUndeclaredAttributes':
foreach($errs as $e) {
$output->writeln(sprintf(' %s: %s', $e['fqcn'], $e['filename']));
if(!empty($e['classAttributes'])) {
$errors++;
$output->writeln(sprintf(' classAttributes: %s', implode(', ', $e['classAttributes'])));
}
foreach(array_intersect_key($e, array_flip(['propertyAttributes', 'methodAttributes', 'parameterAttributes', 'classConstantAttributes'])) as $attrType=>$ar) {
$a = [];
foreach($ar as $class=>$t) {
$errors++;
$a[] = sprintf('%s: %s', $class, implode(', ', $t));
}
if($a) {
$output->writeln(sprintf(' %s: %s', $attrType, implode(', ', $a)));
}
}
}
break;
case 'notFoundClasses':
case 'suspectClasses':
foreach($errs as $e) {
$errors++;
$a = [];
foreach(['class', 'trait', 'interface', 'abstract'] as $t) {
if($e[$t]) {
$a[] = sprintf('%s: %s', $t, implode(', ', $e[$t]));
}
}
$output->writeln(sprintf(' %s: %s %s', $e['filename'], $e['namespace'], implode(' | ', $a)));
}
break;
default: throw new AttributeValidatorException('Invalid test: '.$type);
}
}
$output->writeln('Error count: '.$errors);
return Command::SUCCESS;
}
}
It all works except I don't want to have to copy App\Command/AttributeValidator to each project and so created a second composer package to install it. When executing composer require notion-commotion/attribute-validator-command, this second package is installed but not any of its dependencies.
What am I doing wrong?
Also, off topic and my primary question is related to composer installing dependencies, however, if anyone knows the proper way to have composer install a symfony command, please let me know.
Thanks!
{
"name": "notion-commotion/attribute-validator-command",
"description": "Creates Symfony command to find attributes which do not have classes assigned.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Michael Reed",
"email": "xxx@gmail.com"
}
],
"require": {
"php": ">=8",
"symfony/console": "5.3.*",
"nette/utils": "4",
"notion-commotion/attribute-validator": "^1"
},
"autoload": {
"psr-4": {
"NotionCommotion\\AttributeValidatorCommand\\": "src/"
}
},
"minimum-stability": "stable",
"require": {}
}