phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->phpDocTagRemover = $phpDocTagRemover; $this->requiredAnnotationPropertyAnalyzer = $requiredAnnotationPropertyAnalyzer; $this->constructorClassMethodFactory = $constructorClassMethodFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change annotation to attribute compatible form, see https://tomasvotruba.com/blog/doctrine-annotations-and-attributes-living-together-in-peace/', [new CodeSample(<<<'CODE_SAMPLE' use Doctrine\Common\Annotations\Annotation\Required; /** * @annotation */ class SomeAnnotation { /** * @var string[] * @Required() */ public array $enum; } CODE_SAMPLE , <<<'CODE_SAMPLE' use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; /** * @annotation * @NamedArgumentConstructor */ class SomeAnnotation { /** * @param string[] $enum */ public function __construct( public array $enum ) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } if ($this->shouldSkipClass($phpDocInfo, $node)) { return null; } // add "NamedArgumentConstructor" $phpDocInfo->addTagValueNode(new DoctrineAnnotationTagValueNode(new IdentifierTypeNode('Doctrine\\Common\\Annotations\\Annotation\\NamedArgumentConstructor'))); // resolve required properties $requiredPropertiesWithPhpDocInfos = []; foreach ($node->getProperties() as $property) { if (!$property->isPublic()) { continue; } $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); if (!$this->requiredAnnotationPropertyAnalyzer->isRequiredProperty($propertyPhpDocInfo, $property)) { continue; } $propertyName = $this->getName($property); $requiredPropertiesWithPhpDocInfos[] = new PropertyWithPhpDocInfo($propertyName, $property, $propertyPhpDocInfo); } $params = $this->createConstructParams($requiredPropertiesWithPhpDocInfos); $constructorClassMethod = $this->constructorClassMethodFactory->createConstructorClassMethod($requiredPropertiesWithPhpDocInfos, $params); $node->stmts = \array_merge($node->stmts, [$constructorClassMethod]); return $node; } private function shouldSkipClass(PhpDocInfo $phpDocInfo, Class_ $class) : bool { if (!$phpDocInfo->hasByNames(['Annotation', 'annotation'])) { return \true; } if ($phpDocInfo->hasByAnnotationClass('Doctrine\\Common\\Annotations\\Annotation\\NamedArgumentConstructor')) { return \true; } // has attribute? skip it return $this->phpAttributeAnalyzer->hasPhpAttribute($class, AttributeName::ATTRIBUTE); } /** * @param PropertyWithPhpDocInfo[] $requiredPropertiesWithPhpDocInfos * @return Param[] */ private function createConstructParams(array $requiredPropertiesWithPhpDocInfos) : array { $params = []; foreach ($requiredPropertiesWithPhpDocInfos as $requiredPropertyWithPhpDocInfo) { $property = $requiredPropertyWithPhpDocInfo->getProperty(); $propertyName = $this->getName($property); // unwrap nullable type, as variable is required $propertyType = $property->type; if ($propertyType instanceof NullableType) { $propertyType = $propertyType->type; } $params[] = new Param(new Variable($propertyName), null, $propertyType, \false, \false, [], $property->flags); $propertyPhpDocInfo = $requiredPropertyWithPhpDocInfo->getPhpDocInfo(); // remove required $this->phpDocTagRemover->removeByName($propertyPhpDocInfo, 'Doctrine\\Common\\Annotations\\Annotation\\Required'); $this->removeNode($property); } return $params; } }