diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f57e761..ceddf73 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,12 +43,10 @@ For modifying classes, traits, enums, etc. The base class handles: #### Contracts - **`UpdateNodeContract`** — The visitor can update an existing node. Requires `shouldUpdateNode(Node): bool` and `updateNode(Node): void`. The base class iterates over child statements and calls `updateNode()` on the first match. -- **`InsertNodeContract`** — The visitor can insert a new node. Requires `getInsertableNode(): Node`. The base class handles positioning (via `NodeInserter`) and empty line insertion. +- **`InsertNodeContract`** — The visitor inserts a single node. Requires `getInsertableNode(): Node`. The base class handles positioning (via `NodeInserter`) and empty line insertion. - **`RemoveNodeContract`** — The visitor can remove an existing node. Requires `shouldRemoveNode(Node): bool`. The base class iterates over child statements and removes the first matching node. -A visitor may implement both contracts. In that case, update is attempted first — insertion happens only if no existing node matched. - -**Bulk insertion visitors** extend `InsertNodesAbstractVisitor` (which extends `AbstractNodeVisitor`) and handle inserting multiple nodes with built-in duplicate filtering. +A visitor may implement both contracts. Update is attempted first — insertion happens only if no existing node matched. ### App bootstrap visitors (`AbstractAppBootstrapVisitor`) @@ -72,7 +70,7 @@ Located in `src/Support/`: ## Creating a New Visitor -1. Extend `AbstractNodeVisitor` (or `AbstractInsertNodesVisitor` for bulk insertions). +1. Extend `AbstractNodeVisitor`. 2. Set `$allowedParentNodesTypes` to the node types your visitor targets. 3. Implement `InsertNodeContract`, `UpdateNodeContract`, `RemoveNodeContract`, or a combination. 4. Add a corresponding fluent method in `PHPFileBuilder` that creates and registers the visitor. diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 25ef523..1122fd7 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -11,8 +11,8 @@ use RonasIT\Larabuilder\Exceptions\InvalidPHPFileException; use RonasIT\Larabuilder\NodeTraverser; use RonasIT\Larabuilder\Printer; -use RonasIT\Larabuilder\Visitors\AddImports; -use RonasIT\Larabuilder\Visitors\AddTraits; +use RonasIT\Larabuilder\Visitors\AddImport; +use RonasIT\Larabuilder\Visitors\AddTrait; use RonasIT\Larabuilder\Visitors\MethodVisitors\AddMethod; use RonasIT\Larabuilder\Visitors\MethodVisitors\InsertCodeToMethod; use RonasIT\Larabuilder\Visitors\MethodVisitors\RemoveMethod; @@ -67,14 +67,18 @@ public function removeArrayPropertyItem(string $propertyName, array $values): se public function addImports(array $imports): self { - $this->traverser->addVisitor(new AddImports($imports)); + foreach ($imports as $import) { + $this->traverser->addVisitor(new AddImport($import)); + } return $this; } public function addTraits(array $traits): self { - $this->traverser->addVisitor(new AddTraits($traits)); + foreach ($traits as $trait) { + $this->traverser->addVisitor(new AddTrait($trait)); + } $this->addImports($traits); diff --git a/src/Support/NodeInserter.php b/src/Support/NodeInserter.php index feffe93..6fe3d54 100644 --- a/src/Support/NodeInserter.php +++ b/src/Support/NodeInserter.php @@ -3,6 +3,7 @@ namespace RonasIT\Larabuilder\Support; use Illuminate\Support\Arr; +use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; @@ -29,19 +30,17 @@ class NodeInserter ClassMethod::class, ]; - public function insertNodes(array &$stmts, array $newNodes): void + public function insertNode(array &$stmts, Node $newNode): void { - foreach ($newNodes as $newNode) { - $newNodeClass = get_class($newNode); + $newNodeClass = get_class($newNode); - $insertIndex = $this->getInsertIndex($stmts, $newNodeClass); + $insertIndex = $this->getInsertIndex($stmts, $newNodeClass); - $newNode->setAttribute(StatementAttributeEnum::Previous->value, Arr::get($stmts, $insertIndex - 1)); + $newNode->setAttribute(StatementAttributeEnum::Previous->value, Arr::get($stmts, $insertIndex - 1)); - array_splice($stmts, $insertIndex, 0, [$newNode]); + array_splice($stmts, $insertIndex, 0, [$newNode]); - $this->insertEmptyLineIfNeeded($stmts, $insertIndex + 1, $newNodeClass); - } + $this->insertEmptyLineIfNeeded($stmts, $insertIndex + 1, $newNodeClass); } protected function getInsertIndex(array $statements, string $insertType): int diff --git a/src/Visitors/AbstractInsertNodesVisitor.php b/src/Visitors/AbstractInsertNodesVisitor.php deleted file mode 100644 index 5f6dee6..0000000 --- a/src/Visitors/AbstractInsertNodesVisitor.php +++ /dev/null @@ -1,79 +0,0 @@ -nodeInserter = new NodeInserter(); - } - - /** @param Class_|Enum_|Trait_ $node */ - protected function modify(Node $node): Node - { - $this->insertNodes($node->stmts); - - return $node; - } - - protected function insertNodes(array &$nodes): void - { - $newNodes = $this->getNodesToAdd($nodes); - - if (!empty($newNodes)) { - $nodes = $this->addNodes($nodes, $newNodes); - } - } - - protected function getNodesToAdd(array $nodes): Collection - { - $existingNodes = []; - - foreach ($nodes as $node) { - if (!($node instanceof $this->targetNodeClass)) { - continue; - } - - /** @var TraitUse|Use_ $node */ - $childNodes = $this->getChildNodes($node); - - foreach ($childNodes as $childNode) { - if ($this->nodesToInsert->contains($childNode->name)) { - $existingNodes[] = $childNode->name; - } - } - } - - return $this - ->nodesToInsert - ->diff($existingNodes) - ->values(); - } - - protected function addNodes(array $nodes, Collection $newNodes): array - { - $insertableNodes = $newNodes->map(fn ($node) => $this->getInsertableNode($node))->all(); - - $this->nodeInserter->insertNodes($nodes, $insertableNodes); - - return $nodes; - } -} diff --git a/src/Visitors/AbstractNodeVisitor.php b/src/Visitors/AbstractNodeVisitor.php index ee5b043..8aad7a0 100644 --- a/src/Visitors/AbstractNodeVisitor.php +++ b/src/Visitors/AbstractNodeVisitor.php @@ -81,15 +81,33 @@ protected function modify(Node $node): Node $this->updatableNodeNotFoundHook(); } - return ($this instanceof InsertNodeContract) - ? $this->insertNode($node) - : $node; + $this->insertNode($node->stmts); + + return $node; } protected function updatableNodeNotFoundHook(): void { } + protected function insertNode(array &$stmts): void + { + if ($this instanceof InsertNodeContract && !$this->isDuplicate($stmts)) { + $this->nodeInserter ??= new NodeInserter(); + + $newNode = $this->getInsertableNode(); + + $this->linkParents($newNode); + + $this->nodeInserter->insertNode($stmts, $newNode); + } + } + + protected function isDuplicate(array $stmts): bool + { + return false; + } + protected function linkParents(Node $parent): void { foreach ($parent->getSubNodeNames() as $name) { @@ -101,18 +119,4 @@ protected function linkParents(Node $parent): void } } } - - /** @param Class_|Trait_|Enum_ $node */ - private function insertNode(Node $node): Node - { - $this->nodeInserter ??= new NodeInserter(); - - $newNode = $this->getInsertableNode(); - - $this->linkParents($newNode); - - $this->nodeInserter->insertNodes($node->stmts, [$newNode]); - - return $node; - } } diff --git a/src/Visitors/AddImport.php b/src/Visitors/AddImport.php new file mode 100644 index 0000000..2e2828d --- /dev/null +++ b/src/Visitors/AddImport.php @@ -0,0 +1,58 @@ + $node instanceof Namespace_); + + if (!is_null($targetNamespace)) { + /** @var Namespace_ $targetNamespace */ + $targetNodes = &$targetNamespace->stmts; + } else { + $targetNodes = &$nodes; + } + + $this->insertNode($targetNodes); + + return $nodes; + } + + public function getInsertableNode(): Node + { + return new Use_([new UseItem(new Name($this->import))]); + } + + protected function isDuplicate(array $stmts): bool + { + foreach ($stmts as $stmt) { + if (!($stmt instanceof Use_)) { + continue; + } + + foreach ($stmt->uses as $useItem) { + if ($useItem->name->toString() === $this->import) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Visitors/AddImports.php b/src/Visitors/AddImports.php deleted file mode 100644 index b5142de..0000000 --- a/src/Visitors/AddImports.php +++ /dev/null @@ -1,58 +0,0 @@ -filter() - ->unique(); - - parent::__construct( - nodesToInsert: $nodesToInsert, - targetNodeClass: Use_::class, - ); - } - - public function leaveNode(Node $node): Node - { - return $node; - } - - public function afterTraverse(array $nodes): ?array - { - $targetNamespace = array_find($nodes, fn ($node) => $node instanceof Namespace_); - - if (!is_null($targetNamespace)) { - /** @var Namespace_ $targetNamespace */ - $targetNodes = &$targetNamespace->stmts; - } else { - $targetNodes = &$nodes; - } - - $this->insertNodes($targetNodes); - - return $nodes; - } - - /** @param Use_ $node */ - protected function getChildNodes(Node $node): array - { - return $node->uses; - } - - protected function getInsertableNode(string $name): Node - { - return new Use_([new UseItem(new Name($name))]); - } -} diff --git a/src/Visitors/AddTrait.php b/src/Visitors/AddTrait.php new file mode 100644 index 0000000..2fcd754 --- /dev/null +++ b/src/Visitors/AddTrait.php @@ -0,0 +1,48 @@ +trait = class_basename($this->trait); + } + + public function getInsertableNode(): Node + { + return new TraitUse([new Name($this->trait)]); + } + + protected function isDuplicate(array $stmts): bool + { + foreach ($stmts as $stmt) { + if (!($stmt instanceof TraitUse)) { + continue; + } + + foreach ($stmt->traits as $traitName) { + if ($traitName->getLast() === $this->trait) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Visitors/AddTraits.php b/src/Visitors/AddTraits.php deleted file mode 100644 index b3b1412..0000000 --- a/src/Visitors/AddTraits.php +++ /dev/null @@ -1,43 +0,0 @@ -filter() - ->unique() - ->map(fn ($trait) => class_basename($trait)); - - parent::__construct( - nodesToInsert: $nodesToInsert, - targetNodeClass: TraitUse::class, - ); - } - - /** @param TraitUse $node */ - protected function getChildNodes(Node $node): array - { - return $node->traits; - } - - protected function getInsertableNode(string $name): Node - { - return new TraitUse([new Name($name)]); - } -} diff --git a/tests/NodeInserterTest.php b/tests/NodeInserterTest.php deleted file mode 100644 index fd1278f..0000000 --- a/tests/NodeInserterTest.php +++ /dev/null @@ -1,81 +0,0 @@ -inserter = new NodeInserter(); - } - - public function testInsertMixedNodes(): void - { - $this->prepareFixture('class.php'); - - $classNode = (new NodeFinder())->findFirstInstanceOf($this->newSyntaxTree, Class_::class); - - $this->inserter->insertNodes($classNode->stmts, [ - new Property(Modifiers::PUBLIC, [new PropertyItem('anotherProperty')]), - new ClassMethod('newMethod', ['flags' => Modifiers::PUBLIC]), - new ClassConst([new Const_('NEW_CONST', new Int_(42))], Modifiers::PUBLIC), - new ClassMethod('anotherMethod', ['flags' => Modifiers::PUBLIC]), - new Property(Modifiers::PUBLIC, [new PropertyItem('newProperty')]), - new TraitUse([new Name('NewTrait')]), - new ClassConst([new Const_('ANOTHER_CONST', new Int_(0))], Modifiers::PUBLIC), - new TraitUse([new Name('AnotherTrait')]), - ], true); - - $this->assertSame( - $this->getFixture('class_with_mixed_nodes_inserted.php'), - $this->printResult(), - ); - } - - protected function prepareFixture(string $fixture): void - { - $parser = (new ParserFactory())->createForHostVersion(); - - $code = file_get_contents($this->generateOriginalStructurePath($fixture)); - - $syntaxTree = $parser->parse($code); - - $this->oldTokens = $parser->getTokens(); - - $traverser = new NodeTraverser(); - - $traverser->addVisitor(new CloningVisitor()); - - $this->oldSyntaxTree = $syntaxTree; - $this->newSyntaxTree = $traverser->traverse($syntaxTree); - } - - protected function printResult(): string - { - return (new Printer())->printFormatPreserving($this->newSyntaxTree, $this->oldSyntaxTree, $this->oldTokens); - } -} diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index b302fa4..1b27f08 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -362,7 +362,7 @@ public function testAddTraitsNotClassTraitEnum(): void { $file = $this->generateOriginalStructurePath('interface.php'); - $this->assertExceptionThrew(InvalidStructureTypeException::class, "'AddTraits' operation may only be applied to: Class, Trait, Enum."); + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'AddTrait' operation may only be applied to: Class, Trait, Enum."); new PHPFileBuilder($file) ->addTraits([