Skip to content
8 changes: 3 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand All @@ -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.
Expand Down
12 changes: 8 additions & 4 deletions src/Builders/PHPFileBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Comment on lines +70 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter invalid import entries before adding visitors

addImports() now forwards each raw array element directly into new AddImport($import), but AddImport::__construct requires a string and no longer benefits from the old filter() step. If callers pass optional values (for example null or empty strings from config/merge pipelines), this now throws a TypeError (for null) or can emit an invalid use ; statement (for empty string), where the previous implementation silently skipped those values.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve import order when adding per-import visitors

When callers pass multiple new imports, this now enqueues one AddImport visitor per item, but the project’s NodeTraverser::traverse() reverses the visitor list before traversal. Since these imports are inserted from AddImport::afterTraverse(), they are applied last-to-first, so addImports([SecondClass, ThirdClass]) emits ThirdClass before SecondClass (and addTraits() inherits the same reversed import order), breaking the previously preserved caller order and the existing fixture expectations.

Useful? React with 👍 / 👎.

}

return $this;
}

public function addTraits(array $traits): self
{
$this->traverser->addVisitor(new AddTraits($traits));
foreach ($traits as $trait) {
$this->traverser->addVisitor(new AddTrait($trait));
Comment on lines +79 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize trait list before constructing AddTrait visitors

addTraits() also removed the prior filter() behavior and now instantiates AddTrait for every raw element. Passing nullable/empty trait entries (common when composing arrays conditionally) now fails at runtime (TypeError for null) or may build malformed trait/use nodes for empty strings, instead of being ignored as before, so this refactor introduces a backward-incompatible failure mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve trait insertion order

When addTraits() receives multiple new traits for a class/trait/enum with no existing trait uses, this loop registers one AddTrait visitor per trait, but NodeTraverser::traverse() reverses the visitor list before traversal. Since each AddTrait inserts during leaveNode, addTraits([FirstTrait, SecondTrait, ThirdTrait]) emits the trait-use statements as ThirdTrait, SecondTrait, FirstTrait, rather than the caller order expected by the existing add_traits_to_enum.php/add_traits_to_trait.php fixtures.

Useful? React with 👍 / 👎.

}

$this->addImports($traits);

Expand Down
15 changes: 7 additions & 8 deletions src/Support/NodeInserter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
79 changes: 0 additions & 79 deletions src/Visitors/AbstractInsertNodesVisitor.php

This file was deleted.

38 changes: 21 additions & 17 deletions src/Visitors/AbstractNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
58 changes: 58 additions & 0 deletions src/Visitors/AddImport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace RonasIT\Larabuilder\Visitors;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\UseItem;
use RonasIT\Larabuilder\Contracts\InsertNodeContract;

class AddImport extends AbstractNodeVisitor implements InsertNodeContract
{
protected array $allowedParentNodesTypes = self::ANY_TYPE;

public function __construct(
protected string $import,
) {
}

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->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;
}
}
58 changes: 0 additions & 58 deletions src/Visitors/AddImports.php

This file was deleted.

48 changes: 48 additions & 0 deletions src/Visitors/AddTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace RonasIT\Larabuilder\Visitors;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\TraitUse;
use RonasIT\Larabuilder\Contracts\InsertNodeContract;

class AddTrait extends AbstractNodeVisitor implements InsertNodeContract
{
protected array $allowedParentNodesTypes = [
Class_::class,
Trait_::class,
Enum_::class,
];

public function __construct(
protected string $trait,
) {
$this->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;
}
}
Loading