From 16c51cfb0784069dc87735c80b7457af582a3a94 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Wed, 8 Apr 2026 21:24:14 +0300 Subject: [PATCH 1/5] feat: add removeClassAttribute method for class attributes refs: https://github.com/RonasIT/larabuilder/issues/57 --- src/Builders/PHPFileBuilder.php | 8 ++ tests/PHPFileBuilderTest.php | 81 +++++++++++++++++++ .../original/class_with_attributes.php | 19 +++++ .../class_with_grouped_attributes.php | 16 ++++ .../class_with_multiple_same_attributes.php | 19 +++++ .../class_with_attributes_unchanged.php | 19 +++++ ...ss_with_grouped_attributes_one_removed.php | 16 ++++ .../results/class_without_attributes.php | 17 ++++ 8 files changed, 195 insertions(+) create mode 100644 tests/fixtures/PHPFileBuilderTest/original/class_with_attributes.php create mode 100644 tests/fixtures/PHPFileBuilderTest/original/class_with_grouped_attributes.php create mode 100644 tests/fixtures/PHPFileBuilderTest/original/class_with_multiple_same_attributes.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/class_with_attributes_unchanged.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/class_with_grouped_attributes_one_removed.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/class_without_attributes.php diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index c21ce1b..32900bc 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\SetPropertyValue; +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/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index d298a65..bc218fb 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -508,4 +508,85 @@ public function testInsertDuplicateCodeToMethod(string $code): void ->insertCodeToMethod('someMethod', $code) ->save(); } + + public function testRemoveClassAttributeNotClass(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'add_imports_to_interface.php'), + ); + + $this->assertExceptionThrew(Exception::class, 'Only nodes with the next types can be modified: Class'); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('SomeClass', 'someAttribute') + ->save(); + } + + public function testRemoveClassAttributeWhenClassNotExist(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'class_with_properties.php'), + ); + + $this->assertExceptionThrew(NodeNotExistException::class, "Class 'AnotherClass' does not exist."); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('AnotherClass', 'someAttribute') + ->save(); + } + + public function testRemoveClassAttribute(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'class_with_attributes.php'), + $this->callFilePutContent('some_file_path.php', 'class_without_attributes.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->removeClassAttribute('SomeClass', 'AnotherAttribute') + ->save(); + } + + public function testRemoveClassAttributeFromGroupedAttributes(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'class_with_grouped_attributes.php'), + $this->callFilePutContent('some_file_path.php', 'class_with_grouped_attributes_one_removed.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->save(); + } + + public function testRemoveClassAttributeMultipleSameNameAttributes(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'class_with_multiple_same_attributes.php'), + $this->callFilePutContent('some_file_path.php', 'class_without_attributes.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('SomeClass', 'MyAttribute') + ->save(); + } + + public function testRemoveClassAttributeNoMatchingAttribute(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'class_with_attributes.php'), + $this->callFilePutContent('some_file_path.php', 'class_with_attributes_unchanged.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeClassAttribute('SomeClass', 'NonExistentAttribute') + ->save(); + } } diff --git a/tests/fixtures/PHPFileBuilderTest/original/class_with_attributes.php b/tests/fixtures/PHPFileBuilderTest/original/class_with_attributes.php new file mode 100644 index 0000000..43f0016 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/original/class_with_attributes.php @@ -0,0 +1,19 @@ + Date: Wed, 8 Apr 2026 21:56:00 +0300 Subject: [PATCH 2/5] feat: add removeClassAttribute method for class attributes refs: https://github.com/RonasIT/larabuilder/issues/57 --- src/Visitors/BaseNodeVisitorAbstract.php | 15 ++++ .../InsertOrUpdateNodeAbstractVisitor.php | 13 ---- src/Visitors/RemoveClassAttribute.php | 71 +++++++++++++++++++ 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 src/Visitors/RemoveClassAttribute.php diff --git a/src/Visitors/BaseNodeVisitorAbstract.php b/src/Visitors/BaseNodeVisitorAbstract.php index 1bb484a..c90a1d5 100644 --- a/src/Visitors/BaseNodeVisitorAbstract.php +++ b/src/Visitors/BaseNodeVisitorAbstract.php @@ -22,6 +22,8 @@ abstract class BaseNodeVisitorAbstract extends NodeVisitorAbstract { + public bool $hasParentNode = false; + protected const TYPE_ORDER = [ Namespace_::class, Use_::class, @@ -34,6 +36,19 @@ abstract class BaseNodeVisitorAbstract extends NodeVisitorAbstract ClassMethod::class, ]; + public function parentNodeNotFoundHook(): void + { + } + + public function afterTraverse(array $nodes): ?array + { + if (!$this->hasParentNode) { + $this->parentNodeNotFoundHook(); + } + + return null; + } + protected function getInsertIndex(array $statements, string $insertType): int { $insertIndex = 0; diff --git a/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php b/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php index 37f4184..395776c 100644 --- a/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php +++ b/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php @@ -9,8 +9,6 @@ abstract class InsertOrUpdateNodeAbstractVisitor extends BaseNodeVisitorAbstract { - public bool $hasParentNode = false; - abstract protected function shouldUpdateNode(Node $node): bool; /** @@ -23,10 +21,6 @@ abstract protected function updateNode(Node $node): void; abstract protected function getInsertableNode(): Node; - public function parentNodeNotFoundHook(): void - { - } - public function leaveNode(Node $node): Node { if ($this->isParentNode($node)) { @@ -47,13 +41,6 @@ public function leaveNode(Node $node): Node return $node; } - public function afterTraverse(array $nodes): void - { - if (!$this->hasParentNode) { - $this->parentNodeNotFoundHook(); - } - } - /** @param Class_|Trait_ $node */ protected function insertNode(Node $node): Node { diff --git a/src/Visitors/RemoveClassAttribute.php b/src/Visitors/RemoveClassAttribute.php new file mode 100644 index 0000000..16d0625 --- /dev/null +++ b/src/Visitors/RemoveClassAttribute.php @@ -0,0 +1,71 @@ +isParentNode($node)) { + $this->hasParentNode = true; + + /** @var Class_ $node */ + $this->validateClassName($node); + + $this->removeMatchingAttributes($node); + } + + return $node; + } + + protected function isParentNode(Node $node): bool + { + return $node instanceof Class_; + } + + 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) => !$this->shouldRemoveAttribute($attr)); + + // 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(); + } + } + } + + protected function shouldRemoveAttribute(Attribute $node): bool + { + return $this->attributeName === $node->name->name; + } + + public function parentNodeNotFoundHook(): void + { + throw new InvalidNodeTypeException('Class'); + } +} From 799fe92c1bd3cd65c422f4eb38334dcea973ea1e Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 10 Apr 2026 13:05:39 +0300 Subject: [PATCH 3/5] fix: remarks --- src/Visitors/RemoveClassAttribute.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Visitors/RemoveClassAttribute.php b/src/Visitors/RemoveClassAttribute.php index 9e5cf88..8ec1c2a 100644 --- a/src/Visitors/RemoveClassAttribute.php +++ b/src/Visitors/RemoveClassAttribute.php @@ -44,7 +44,7 @@ protected function removeMatchingAttributes(Class_ $node): void continue; } - $attrGroup->attrs = array_filter($attrGroup->attrs, fn (Attribute $attr) => !$this->shouldRemoveAttribute($attr)); + $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)) { @@ -52,9 +52,4 @@ protected function removeMatchingAttributes(Class_ $node): void } } } - - protected function shouldRemoveAttribute(Attribute $node): bool - { - return $this->attributeName === $node->name->name; - } } From 8cd3f129b451866f51270f18906ba7305561f9a5 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 10 Apr 2026 22:49:06 +0300 Subject: [PATCH 4/5] docs: add removeClassAttribute to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 11ff580..32134ff 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,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 From eb2719b28783de4308410db2e315bc63400cffbd Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 08:21:04 +0300 Subject: [PATCH 5/5] fix: remarks --- tests/PHPFileBuilderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index 12e1ecb..4ae49ff 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -542,7 +542,7 @@ public function testRemoveClassAttributeNotClass(): void } public function testRemoveClassAttributeWhenClassNotExist(): void - { + { $file = $this->generateOriginalStructurePath('class_with_properties.php'); $this->assertExceptionThrew(NodeNotExistException::class, "Class 'AnotherClass' does not exist."); @@ -553,7 +553,7 @@ public function testRemoveClassAttributeWhenClassNotExist(): void } public function testRemoveClassAttribute(): void - { + { $file = $this->generateOriginalStructurePath('class_with_attributes.php'); $this->mockNativeFunction(