diff --git a/README.md b/README.md index 6709896..574d1a0 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ Add new `use TraitName;` statements to a class, trait, or enum. This method auto **Note:** Need to provide the full trait class name (FQCN); the method will import it automatically. +#### removeClassAttribute + +Remove a PHP attribute from the specified class. If the class does not exist in the file a `NodeNotExistException` is thrown. + ## Special Laravel structure builders ### Bootstrap app diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 0f9128e..8b15c11 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -16,6 +16,7 @@ use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty; +use RonasIT\Larabuilder\Visitors\RemoveClassAttribute; class PHPFileBuilder { @@ -85,6 +86,13 @@ public function insertCodeToMethod(string $methodName, string $code, InsertPosit return $this; } + public function removeClassAttribute(string $className, string $attributeName): self + { + $this->traverser->addVisitor(new RemoveClassAttribute($className, $attributeName)); + + return $this; + } + public function save(): void { $this->traverser->addVisitor(new CloningVisitor()); diff --git a/src/Visitors/RemoveClassAttribute.php b/src/Visitors/RemoveClassAttribute.php new file mode 100644 index 0000000..00269a9 --- /dev/null +++ b/src/Visitors/RemoveClassAttribute.php @@ -0,0 +1,55 @@ +validateClassName($node); + + $this->removeMatchingAttributes($node); + + return $node; + } + + protected function validateClassName(Class_ $node): void + { + if ($this->className !== $node->name->name) { + throw new NodeNotExistException('Class', $this->className); + } + } + + protected function removeMatchingAttributes(Class_ $node): void + { + foreach ($node->attrGroups as $key => $attrGroup) { + if ($attrGroup instanceof Nop) { + continue; + } + + $attrGroup->attrs = array_filter($attrGroup->attrs, fn (Attribute $attr) => $attr->name->name !== $this->attributeName); + + // Replace with Nop to avoid leftover `#[]` and preserve original blank lines (full removal shifts token offsets in Printer) + if (empty($attrGroup->attrs)) { + $node->attrGroups[$key] = new Nop(); + } + } + } +} diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index d6148d1..ab80981 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -530,4 +530,82 @@ public function testInsertDuplicateCodeToMethod(string $code): void ->insertCodeToMethod('someMethod', $code) ->save(); } + + public function testRemoveClassAttributeNotClass(): void + { + $file = $this->generateOriginalStructurePath('interface.php'); + + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'RemoveClassAttribute' operation may only be applied to: Class."); + + new PHPFileBuilder($file) + ->removeClassAttribute('SomeClass', 'someAttribute') + ->save(); + } + + public function testRemoveClassAttributeWhenClassNotExist(): void + { + $file = $this->generateOriginalStructurePath('class_with_properties.php'); + + $this->assertExceptionThrew(NodeNotExistException::class, "Class 'AnotherClass' does not exist."); + + new PHPFileBuilder($file) + ->removeClassAttribute('AnotherClass', 'someAttribute') + ->save(); + } + + public function testRemoveClassAttribute(): void + { + $file = $this->generateOriginalStructurePath('class_with_attributes.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'class_without_attributes.php'), + ); + + new PHPFileBuilder($file) + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->removeClassAttribute('SomeClass', 'AnotherAttribute') + ->save(); + } + + public function testRemoveClassAttributeFromGroupedAttributes(): void + { + $file = $this->generateOriginalStructurePath('class_with_grouped_attributes.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'class_with_grouped_attributes_one_removed.php'), + ); + + new PHPFileBuilder($file) + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->save(); + } + + public function testRemoveClassAttributeMultipleSameNameAttributes(): void + { + $file = $this->generateOriginalStructurePath('class_with_multiple_same_attributes.php'); + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'class_without_attributes.php'), + ); + + new PHPFileBuilder($file) + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->save(); + } + + public function testRemoveClassAttributeNoMatchingAttribute(): void + { + $file = $this->generateOriginalStructurePath('class_with_attributes.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'class_with_attributes_unchanged.php'), + ); + + new PHPFileBuilder($file) + ->removeClassAttribute('SomeClass', 'NonExistentAttribute') + ->save(); + } } diff --git a/tests/Support/OriginStructures/class_with_attributes.php b/tests/Support/OriginStructures/class_with_attributes.php new file mode 100644 index 0000000..43f0016 --- /dev/null +++ b/tests/Support/OriginStructures/class_with_attributes.php @@ -0,0 +1,19 @@ +