Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ 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.

#### addItemToReturnArray

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.

```php
new PHPFileBuilder(app_path('Models/User.php'))
->addItemToReturnArray('casts', 'RoleEnum::class', 'role')
->addItemToReturnArray('getAvailableRelations', 'logo')
->save();
```

#### addMethod

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

```php
new PHPFileBuilder(app_path('Http/Controllers/UserController.php'))
->addMethod(
name: 'delete',
code: '
$service->delete($id);
return response()->noContent();
',
params: new MethodParams(
new MethodParam(name: 'request', type: 'DeleteRequest'),
new MethodParam(name: 'service', type: 'Service'),
new MethodParam(name: 'id', type: 'int'),
),
returnType: 'Response',
)
->save();
```

Each `MethodParam` accepts: `name`, `type` (e.g. `'int'`, `'?string'`, `'MyClass'`), `default` (`DefaultValue::None` to omit), `variadic`, `byRef`.

`addMethod` also supports `static: true` and `returnsByRef: true` (for methods that return by reference, e.g. `public function &items(): array`).

## Special Laravel structure builders

### Bootstrap app
Expand Down
27 changes: 26 additions & 1 deletion src/Builders/PHPFileBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\ParserFactory;
use RonasIT\Larabuilder\Enums\AccessModifierEnum;
use RonasIT\Larabuilder\Enums\DefaultValue;
use RonasIT\Larabuilder\Enums\InsertPositionEnum;
use RonasIT\Larabuilder\Exceptions\InvalidPHPFileException;
use RonasIT\Larabuilder\NodeTraverser;
use RonasIT\Larabuilder\Printer;
use RonasIT\Larabuilder\ValueOptions\MethodParams;
use RonasIT\Larabuilder\Visitors\AddImports;
use RonasIT\Larabuilder\Visitors\AddTraits;
use RonasIT\Larabuilder\Visitors\InsertCodeToMethod;
use RonasIT\Larabuilder\Visitors\MethodVisitors\AddItemToReturnArray;
use RonasIT\Larabuilder\Visitors\MethodVisitors\AddMethod;
use RonasIT\Larabuilder\Visitors\MethodVisitors\InsertCodeToMethod;
use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem;
use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem;
use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty;
Expand Down Expand Up @@ -78,6 +82,27 @@ public function addTraits(array $traits): self
return $this;
}

public function addMethod(
string $name,
string $code,
MethodParams $params = new MethodParams(),
?string $returnType = null,
AccessModifierEnum $accessModifier = AccessModifierEnum::Public,
bool $static = false,
bool $returnsByRef = false,
): self {
$this->traverser->addVisitor(new AddMethod($name, $code, $params, $returnType, $accessModifier, $static, $returnsByRef));

return $this;
}

public function addItemToReturnArray(string $methodName, string $value, string|DefaultValue $key = DefaultValue::None): self
{
$this->traverser->addVisitor(new AddItemToReturnArray($methodName, $value, $key));

return $this;
}

public function insertCodeToMethod(string $methodName, string $code, InsertPositionEnum $position = InsertPositionEnum::End): self
{
$this->traverser->addVisitor(new InsertCodeToMethod($methodName, $code, $position));
Expand Down
8 changes: 8 additions & 0 deletions src/Enums/DefaultValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace RonasIT\Larabuilder\Enums;

enum DefaultValue
{
case None;
}
13 changes: 13 additions & 0 deletions src/Exceptions/NodeAlreadyExistsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace RonasIT\Larabuilder\Exceptions;

use Exception;

class NodeAlreadyExistsException extends Exception
{
public function __construct(string $node, string $name)
{
parent::__construct("{$node} '{$name}' already exists.");
}
}
16 changes: 16 additions & 0 deletions src/Exceptions/UnexpectedReturnTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace RonasIT\Larabuilder\Exceptions;

use Exception;

class UnexpectedReturnTypeException extends Exception
{
public function __construct(string $method, string $expectedType, ?string $actualType = null)
{
parent::__construct(
"Method '{$method}' return value has unexpected type. Expected '{$expectedType}'"
. (!empty($actualType) ? ", actual '{$actualType}'." : '.'),
);
}
}
60 changes: 60 additions & 0 deletions src/Nodes/PreformattedExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace RonasIT\Larabuilder\Nodes;

use Illuminate\Support\Str;
use PhpParser\Error;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Stmt\Expression;
use PhpParser\ParserFactory;
use RonasIT\Larabuilder\Exceptions\InvalidPHPCodeException;

/**
* Used to insert expression code with saving original formatting
*/
class PreformattedExpression extends Expr
{
public readonly array $code;

public function __construct(
public string $value,
public array $attributes = [],
) {
parent::__construct($this->attributes);

$this->value = Str::chopStart($this->value, '<?php');

$this->code = $this->parsePHPCode($this->value);
}

public function getSubNodeNames(): array
{
return ['value'];
}

public function getType(): string
{
return 'Expr_PreformattedExpression';
}

protected function parsePHPCode(string $code): array
{
try {
$stmts = new ParserFactory()->createForHostVersion()->parse("<?php\n{$code};");

if (
count($stmts) === 1
&& $stmts[0] instanceof Expression
&& $stmts[0]->expr instanceof ConstFetch
&& !in_array(strtolower($stmts[0]->expr->name->name), ['null', 'true', 'false'])
) {
$this->value = "'{$code}'";
}

return $stmts;
} catch (Error) {
throw new InvalidPHPCodeException($code);
}
}
}
13 changes: 12 additions & 1 deletion src/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PhpParser\PrettyPrinter\Standard;
use RonasIT\Larabuilder\Enums\StatementAttributeEnum;
use RonasIT\Larabuilder\Nodes\PreformattedCode;
use RonasIT\Larabuilder\Nodes\PreformattedExpression;

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

protected function pExpr_PreformattedExpression(PreformattedExpression $node): string
{
return $this->formatPreformattedCode($node->value);
}

protected function pStmt_PreformattedCode(PreformattedCode $node): string
{
$value = $this->preparePreformattedCode($node->value);
return $this->formatPreformattedCode($node->value);
}

private function formatPreformattedCode(string $value): string
{
$value = $this->preparePreformattedCode($value);

$indentLength = strspn($value, " \t");
$indent = substr($value, 0, $indentLength);
Expand Down
17 changes: 17 additions & 0 deletions src/ValueOptions/MethodParam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace RonasIT\Larabuilder\ValueOptions;

use RonasIT\Larabuilder\Enums\DefaultValue;

class MethodParam
{
public function __construct(
public readonly string $name,
public readonly ?string $type = null,
public readonly mixed $default = DefaultValue::None,
public readonly bool $variadic = false,
public readonly bool $byRef = false,
) {
}
}
13 changes: 13 additions & 0 deletions src/ValueOptions/MethodParams.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace RonasIT\Larabuilder\ValueOptions;

class MethodParams
{
public readonly array $params;

public function __construct(MethodParam ...$params)
{
$this->params = $params;
}
}
32 changes: 32 additions & 0 deletions src/Visitors/MethodVisitors/AbstractMethodVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace RonasIT\Larabuilder\Visitors\MethodVisitors;

use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Trait_;
use RonasIT\Larabuilder\Exceptions\NodeNotExistException;
use RonasIT\Larabuilder\Visitors\AbstractNodeVisitor;

abstract class AbstractMethodVisitor extends AbstractNodeVisitor
{
protected bool $hasTargetMethod = false;

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

protected array $allowedParentNodesTypes = [
Class_::class,
Trait_::class,
Enum_::class,
];

protected function updatableNodeNotFoundHook(): void
{
if (!$this->hasTargetMethod) {
throw new NodeNotExistException('Method', $this->methodName);
}
}
}
72 changes: 72 additions & 0 deletions src/Visitors/MethodVisitors/AddItemToReturnArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace RonasIT\Larabuilder\Visitors\MethodVisitors;

use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use RonasIT\Larabuilder\Contracts\UpdateNodeContract;
use RonasIT\Larabuilder\Enums\DefaultValue;
use RonasIT\Larabuilder\Exceptions\UnexpectedReturnTypeException;
use RonasIT\Larabuilder\Nodes\PreformattedExpression;
use RonasIT\Larabuilder\Printer;

class AddItemToReturnArray extends AbstractMethodVisitor implements UpdateNodeContract
{
protected PreformattedExpression $valueExpr;
protected ?PreformattedExpression $keyExpr;

public function __construct(
protected string $methodName,
string $value,
string|DefaultValue $key = DefaultValue::None,
) {
parent::__construct($methodName);

$this->valueExpr = new PreformattedExpression($value);
$this->keyExpr = ($key === DefaultValue::None) ? null : new PreformattedExpression($key);
}

public function shouldUpdateNode(Node $node): bool
{
$isTarget = $node instanceof ClassMethod && $this->methodName === $node->name->name;

if ($isTarget) {
$this->hasTargetMethod = true;
}

return $isTarget;
}

public function updateNode(Node $node): void
{
$returnNode = array_find(array_reverse($node->stmts ?? []), fn ($stmt) => $stmt instanceof Return_);

if (!$returnNode?->expr instanceof Array_) {
throw new UnexpectedReturnTypeException($this->methodName, 'array', $node->returnType?->toString());
}

if (empty($this->keyExpr)) {
$returnNode->expr->items[] = new ArrayItem($this->valueExpr);

return;
}

$printer = new Printer();

foreach ($returnNode->expr->items as $item) {
if ($item instanceof ArrayItem
&& !empty($item->key)
&& $printer->prettyPrintExpr($item->key) === $this->keyExpr->value
) {
$item->value = $this->valueExpr;

return;
}
}

$returnNode->expr->items[] = new ArrayItem($this->valueExpr, $this->keyExpr);
}
}
Loading