Skip to content

Commit 2d7ffe6

Browse files
committed
feat: implement addItemToReturnArray method
refs: https://app.clickup.com/t/24336023/PRD-2433
1 parent fdbe9a8 commit 2d7ffe6

33 files changed

Lines changed: 720 additions & 30 deletions

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ Add new `use TraitName;` statements to a class, trait, or enum. This method auto
5555

5656
**Note:** Need to provide the full trait class name (FQCN); the method will import it automatically.
5757

58+
#### addItemToReturnArray
59+
60+
Add or update an item in the array returned by a method. Targets the top-level `return` statement of the method body. If a key is given and already exists, its value is updated; otherwise the item is appended. Works with classes, traits, and enums.
61+
62+
```php
63+
new PHPFileBuilder(app_path('Models/User.php'))
64+
->addItemToReturnArray('casts', 'RoleEnum::class', 'role')
65+
->addItemToReturnArray('getAvailableRelations', 'logo')
66+
->save();
67+
```
68+
5869
#### addMethod
5970

6071
Add a new method to a class, trait, or enum. Throws `NodeAlreadyExistsException` if a method with the given name already exists.

src/Builders/PHPFileBuilder.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
use PhpParser\NodeVisitor\CloningVisitor;
77
use PhpParser\ParserFactory;
88
use RonasIT\Larabuilder\Enums\AccessModifierEnum;
9+
use RonasIT\Larabuilder\Enums\DefaultValue;
910
use RonasIT\Larabuilder\Enums\InsertPositionEnum;
1011
use RonasIT\Larabuilder\Exceptions\InvalidPHPFileException;
1112
use RonasIT\Larabuilder\NodeTraverser;
1213
use RonasIT\Larabuilder\Printer;
1314
use RonasIT\Larabuilder\ValueOptions\MethodParams;
1415
use RonasIT\Larabuilder\Visitors\AddImports;
1516
use RonasIT\Larabuilder\Visitors\AddTraits;
17+
use RonasIT\Larabuilder\Visitors\MethodVisitors\AddItemToReturnArray;
1618
use RonasIT\Larabuilder\Visitors\MethodVisitors\AddMethod;
1719
use RonasIT\Larabuilder\Visitors\MethodVisitors\InsertCodeToMethod;
1820
use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem;
@@ -94,6 +96,13 @@ public function addMethod(
9496
return $this;
9597
}
9698

99+
public function addItemToReturnArray(string $methodName, string $value, string|DefaultValue $key = DefaultValue::None): self
100+
{
101+
$this->traverser->addVisitor(new AddItemToReturnArray($methodName, $value, $key));
102+
103+
return $this;
104+
}
105+
97106
public function insertCodeToMethod(string $methodName, string $code, InsertPositionEnum $position = InsertPositionEnum::End): self
98107
{
99108
$this->traverser->addVisitor(new InsertCodeToMethod($methodName, $code, $position));
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace RonasIT\Larabuilder\Exceptions;
4+
5+
use Exception;
6+
7+
class UnexpectedReturnTypeException extends Exception
8+
{
9+
public function __construct(string $method, string $expectedType, ?string $actualType = null)
10+
{
11+
parent::__construct(
12+
"Method '{$method}' return value has unexpected type. Expected '{$expectedType}'"
13+
. (!empty($actualType) ? ", actual '{$actualType}'." : '.'),
14+
);
15+
}
16+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace RonasIT\Larabuilder\Nodes;
4+
5+
use Illuminate\Support\Str;
6+
use PhpParser\Error;
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\ConstFetch;
9+
use PhpParser\Node\Stmt\Expression;
10+
use PhpParser\ParserFactory;
11+
use RonasIT\Larabuilder\Exceptions\InvalidPHPCodeException;
12+
13+
/**
14+
* Used to insert expression code with saving original formatting
15+
*/
16+
class PreformattedExpression extends Expr
17+
{
18+
public readonly array $code;
19+
20+
public function __construct(
21+
public string $value,
22+
public array $attributes = [],
23+
) {
24+
parent::__construct($this->attributes);
25+
26+
$this->value = Str::chopStart($this->value, '<?php');
27+
28+
$this->code = $this->parsePHPCode($this->value);
29+
}
30+
31+
public function getSubNodeNames(): array
32+
{
33+
return ['value'];
34+
}
35+
36+
public function getType(): string
37+
{
38+
return 'Expr_PreformattedExpression';
39+
}
40+
41+
protected function parsePHPCode(string $code): array
42+
{
43+
try {
44+
$stmts = new ParserFactory()->createForHostVersion()->parse("<?php\n{$code};");
45+
46+
if (
47+
count($stmts) === 1
48+
&& $stmts[0] instanceof Expression
49+
&& $stmts[0]->expr instanceof ConstFetch
50+
&& !in_array(strtolower($stmts[0]->expr->name->name), ['null', 'true', 'false'])
51+
) {
52+
$this->value = "'{$code}'";
53+
}
54+
55+
return $stmts;
56+
} catch (Error) {
57+
throw new InvalidPHPCodeException($code);
58+
}
59+
}
60+
}

src/Printer.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpParser\PrettyPrinter\Standard;
1212
use RonasIT\Larabuilder\Enums\StatementAttributeEnum;
1313
use RonasIT\Larabuilder\Nodes\PreformattedCode;
14+
use RonasIT\Larabuilder\Nodes\PreformattedExpression;
1415

1516
class Printer extends Standard
1617
{
@@ -98,9 +99,19 @@ protected function shouldAddNewlineBeforeIfTypeSame(Node $node, string $type): b
9899
return $previousNode !== null && $previousNode instanceof $type;
99100
}
100101

102+
protected function pExpr_PreformattedExpression(PreformattedExpression $node): string
103+
{
104+
return $this->formatPreformattedCode($node->value);
105+
}
106+
101107
protected function pStmt_PreformattedCode(PreformattedCode $node): string
102108
{
103-
$value = $this->preparePreformattedCode($node->value);
109+
return $this->formatPreformattedCode($node->value);
110+
}
111+
112+
private function formatPreformattedCode(string $value): string
113+
{
114+
$value = $this->preparePreformattedCode($value);
104115

105116
$indentLength = strspn($value, " \t");
106117
$indent = substr($value, 0, $indentLength);

src/Visitors/MethodVisitors/AbstractMethodVisitor.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,28 @@
55
use PhpParser\Node\Stmt\Class_;
66
use PhpParser\Node\Stmt\Enum_;
77
use PhpParser\Node\Stmt\Trait_;
8+
use RonasIT\Larabuilder\Exceptions\NodeNotExistException;
89
use RonasIT\Larabuilder\Visitors\AbstractNodeVisitor;
910

1011
abstract class AbstractMethodVisitor extends AbstractNodeVisitor
1112
{
13+
protected bool $hasTargetMethod = false;
14+
15+
public function __construct(
16+
protected string $methodName,
17+
) {
18+
}
19+
1220
protected array $allowedParentNodesTypes = [
1321
Class_::class,
1422
Trait_::class,
1523
Enum_::class,
1624
];
25+
26+
protected function updatableNodeNotFoundHook(): void
27+
{
28+
if (!$this->hasTargetMethod) {
29+
throw new NodeNotExistException('Method', $this->methodName);
30+
}
31+
}
1732
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace RonasIT\Larabuilder\Visitors\MethodVisitors;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\ArrayItem;
7+
use PhpParser\Node\Expr\Array_;
8+
use PhpParser\Node\Stmt\ClassMethod;
9+
use PhpParser\Node\Stmt\Return_;
10+
use RonasIT\Larabuilder\Contracts\UpdateNodeContract;
11+
use RonasIT\Larabuilder\Enums\DefaultValue;
12+
use RonasIT\Larabuilder\Exceptions\UnexpectedReturnTypeException;
13+
use RonasIT\Larabuilder\Nodes\PreformattedExpression;
14+
use RonasIT\Larabuilder\Printer;
15+
16+
class AddItemToReturnArray extends AbstractMethodVisitor implements UpdateNodeContract
17+
{
18+
protected PreformattedExpression $valueExpr;
19+
protected ?PreformattedExpression $keyExpr;
20+
21+
public function __construct(
22+
protected string $methodName,
23+
string $value,
24+
string|DefaultValue $key = DefaultValue::None,
25+
) {
26+
parent::__construct($methodName);
27+
28+
$this->valueExpr = new PreformattedExpression($value);
29+
$this->keyExpr = ($key === DefaultValue::None) ? null : new PreformattedExpression($key);
30+
}
31+
32+
public function shouldUpdateNode(Node $node): bool
33+
{
34+
$isTarget = $node instanceof ClassMethod && $this->methodName === $node->name->name;
35+
36+
if ($isTarget) {
37+
$this->hasTargetMethod = true;
38+
}
39+
40+
return $isTarget;
41+
}
42+
43+
public function updateNode(Node $node): void
44+
{
45+
$returnNode = array_find(array_reverse($node->stmts ?? []), fn ($stmt) => $stmt instanceof Return_);
46+
47+
if (!$returnNode?->expr instanceof Array_) {
48+
throw new UnexpectedReturnTypeException($this->methodName, 'array', $node->returnType?->toString());
49+
}
50+
51+
if (empty($this->keyExpr)) {
52+
$returnNode->expr->items[] = new ArrayItem($this->valueExpr);
53+
54+
return;
55+
}
56+
57+
$printer = new Printer();
58+
59+
foreach ($returnNode->expr->items as $item) {
60+
if ($item instanceof ArrayItem
61+
&& !empty($item->key)
62+
&& $printer->prettyPrintExpr($item->key) === $this->keyExpr->value
63+
) {
64+
$item->value = $this->valueExpr;
65+
66+
return;
67+
}
68+
}
69+
70+
$returnNode->expr->items[] = new ArrayItem($this->valueExpr, $this->keyExpr);
71+
}
72+
}

src/Visitors/MethodVisitors/InsertCodeToMethod.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@
77
use PhpParser\Node\Stmt\Nop;
88
use RonasIT\Larabuilder\Contracts\UpdateNodeContract;
99
use RonasIT\Larabuilder\Enums\InsertPositionEnum;
10-
use RonasIT\Larabuilder\Exceptions\NodeNotExistException;
1110
use RonasIT\Larabuilder\Nodes\PreformattedCode;
1211
use RonasIT\Larabuilder\Support\StatementDuplicateChecker;
1312

1413
class InsertCodeToMethod extends AbstractMethodVisitor implements UpdateNodeContract
1514
{
16-
protected bool $hasTargetMethod = false;
17-
1815
protected PreformattedCode $code;
1916
protected StatementDuplicateChecker $statementDuplicateChecker;
2017

@@ -23,6 +20,8 @@ public function __construct(
2320
string $code,
2421
protected InsertPositionEnum $insertPosition,
2522
) {
23+
parent::__construct($methodName);
24+
2625
$this->code = new PreformattedCode($code);
2726
$this->statementDuplicateChecker = new StatementDuplicateChecker();
2827
}
@@ -50,11 +49,4 @@ public function updateNode(Node $node): void
5049
? [$this->code, ...$separator, ...$existingStmts]
5150
: [...$existingStmts, ...$separator, $this->code];
5251
}
53-
54-
protected function updatableNodeNotFoundHook(): void
55-
{
56-
if (!$this->hasTargetMethod) {
57-
throw new NodeNotExistException('Method', $this->methodName);
58-
}
59-
}
6052
}

tests/NodeInserterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function testInsertMixedNodes(): void
4848
new TraitUse([new Name('NewTrait')]),
4949
new ClassConst([new Const_('ANOTHER_CONST', new Int_(0))], Modifiers::PUBLIC),
5050
new TraitUse([new Name('AnotherTrait')]),
51-
], true);
51+
]);
5252

5353
$this->assertSame(
5454
$this->getFixture('class_with_mixed_nodes_inserted.php'),

0 commit comments

Comments
 (0)