diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 45fec81..06da59a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,11 +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. +- **`InsertNodesContract`** — The visitor inserts multiple nodes with duplicate filtering. Requires `getInsertableNodes(): array` (returns all candidate nodes) and `getSubNodes(Node): array` (returns child nodes used for duplicate detection). The base class calls `filterExistingNodes()` to exclude already-present nodes before inserting. -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 `UpdateNodeContract` together with `InsertNodeContract` or `InsertNodesContract`. In that case, update is attempted first — insertion happens only if no existing node matched. ### App bootstrap visitors (`AbstractAppBootstrapVisitor`) @@ -71,8 +70,11 @@ 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`, or both. +3. Implement the appropriate contract(s): + - `InsertNodeContract` — for inserting a single node, implement `getInsertableNode(): Node` + - `InsertNodesContract` — for inserting multiple nodes with dedup, implement `getInsertableNodes(): array` and `getSubNodes(Node): array` + - `UpdateNodeContract` — for updating an existing node, implement `shouldUpdateNode(Node): bool` and `updateNode(Node): void` 4. Add a corresponding fluent method in `PHPFileBuilder` that creates and registers the visitor. 5. Add fixture files and a test case. diff --git a/src/Contracts/InsertNodesContract.php b/src/Contracts/InsertNodesContract.php new file mode 100644 index 0000000..da64427 --- /dev/null +++ b/src/Contracts/InsertNodesContract.php @@ -0,0 +1,12 @@ +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 cbc4951..21de695 100644 --- a/src/Visitors/AbstractNodeVisitor.php +++ b/src/Visitors/AbstractNodeVisitor.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeVisitorAbstract; use RonasIT\Larabuilder\Contracts\InsertNodeContract; +use RonasIT\Larabuilder\Contracts\InsertNodesContract; use RonasIT\Larabuilder\Contracts\UpdateNodeContract; use RonasIT\Larabuilder\Enums\StatementAttributeEnum; use RonasIT\Larabuilder\Exceptions\InvalidStructureTypeException; @@ -75,9 +76,9 @@ protected function modify(Node $node): Node $this->updatableNodeNotFoundHook(); } - return ($this instanceof InsertNodeContract) - ? $this->insertNode($node) - : $node; + $this->insertNodes($node->stmts); + + return $node; } protected function updatableNodeNotFoundHook(): void @@ -96,17 +97,50 @@ protected function linkParents(Node $parent): void } } - /** @param Class_|Trait_|Enum_ $node */ - private function insertNode(Node $node): Node + protected function insertNodes(array &$nodes): void { - $this->nodeInserter ??= new NodeInserter(); + $newNodes = match (true) { + $this instanceof InsertNodesContract => $this->filterExistingNodes($nodes), + $this instanceof InsertNodeContract => [$this->getInsertableNode()], + default => null, + }; - $newNode = $this->getInsertableNode(); + if (!empty($newNodes)) { + $this->nodeInserter ??= new NodeInserter(); - $this->linkParents($newNode); + foreach ($newNodes as $newNode) { + $this->linkParents($newNode); + } - $this->nodeInserter->insertNodes($node->stmts, [$newNode]); + $this->nodeInserter->insertNodes($nodes, $newNodes); + } + } - return $node; + private function filterExistingNodes(array $nodes): array + { + $insertableNodes = $this->getInsertableNodes(); + + if (empty($insertableNodes)) { + return []; + } + + $targetNodeClass = get_class($insertableNodes[0]); + + $existingNames = []; + + foreach ($nodes as $node) { + if (!($node instanceof $targetNodeClass)) { + continue; + } + + foreach ($this->getSubNodes($node) as $childNode) { + $existingNames[] = (string) $childNode->name; + } + } + + return array_values(array_filter( + $insertableNodes, + fn (Node $newNode) => !in_array((string) $this->getSubNodes($newNode)[0]->name, $existingNames), + )); } } diff --git a/src/Visitors/AddImports.php b/src/Visitors/AddImports.php index b5142de..4c1d6c7 100644 --- a/src/Visitors/AddImports.php +++ b/src/Visitors/AddImports.php @@ -7,26 +7,15 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; +use RonasIT\Larabuilder\Contracts\InsertNodesContract; -class AddImports extends AbstractInsertNodesVisitor +class AddImports extends AbstractNodeVisitor implements InsertNodesContract { protected array $allowedParentNodesTypes = self::ANY_TYPE; - public function __construct(array $imports) - { - $nodesToInsert = collect($imports) - ->filter() - ->unique(); - - parent::__construct( - nodesToInsert: $nodesToInsert, - targetNodeClass: Use_::class, - ); - } - - public function leaveNode(Node $node): Node - { - return $node; + public function __construct( + protected array $imports, + ) { } public function afterTraverse(array $nodes): ?array @@ -45,14 +34,17 @@ public function afterTraverse(array $nodes): ?array return $nodes; } - /** @param Use_ $node */ - protected function getChildNodes(Node $node): array + public function getInsertableNodes(): array { - return $node->uses; + return array_map( + fn ($import) => new Use_([new UseItem(new Name($import))]), + array_unique(array_filter($this->imports)), + ); } - protected function getInsertableNode(string $name): Node + /** @param Use_ $node */ + public function getSubNodes(Node $node): array { - return new Use_([new UseItem(new Name($name))]); + return $node->uses; } } diff --git a/src/Visitors/AddTraits.php b/src/Visitors/AddTraits.php index b3b1412..3723187 100644 --- a/src/Visitors/AddTraits.php +++ b/src/Visitors/AddTraits.php @@ -8,8 +8,9 @@ use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\Stmt\TraitUse; +use RonasIT\Larabuilder\Contracts\InsertNodesContract; -class AddTraits extends AbstractInsertNodesVisitor +class AddTraits extends AbstractNodeVisitor implements InsertNodesContract { protected array $allowedParentNodesTypes = [ Class_::class, @@ -17,27 +18,22 @@ class AddTraits extends AbstractInsertNodesVisitor Enum_::class, ]; - public function __construct(array $traits) - { - $nodesToInsert = collect($traits) - ->filter() - ->unique() - ->map(fn ($trait) => class_basename($trait)); + public function __construct( + protected array $traits, + ) { + } - parent::__construct( - nodesToInsert: $nodesToInsert, - targetNodeClass: TraitUse::class, + public function getInsertableNodes(): array + { + return array_map( + fn ($trait) => new TraitUse([new Name(class_basename($trait))]), + array_unique(array_filter($this->traits)), ); } /** @param TraitUse $node */ - protected function getChildNodes(Node $node): array + public function getSubNodes(Node $node): array { return $node->traits; } - - protected function getInsertableNode(string $name): Node - { - return new TraitUse([new Name($name)]); - } }