From 1a32fc30f7e0bd5e5158491041540fb386301c0c Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Thu, 22 Jan 2026 16:15:55 +0500 Subject: [PATCH 01/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 12 ++ src/Visitors/AddArrayPropertyItem.php | 2 +- .../AbstractAppBootstrapVisitor.php | 2 +- .../AddScheduleCommand.php | 130 ++++++++++++++++++ tests/AppBootstrapBuilderTest.php | 68 +++++++++ .../original/schedule_exists.php | 17 +++ .../results/combine_render.php | 28 ++++ .../results/schedule.php | 23 ++++ .../results/schedule_exists.php | 20 +++ 9 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/original/schedule_exists.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/results/schedule.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index f3a0316..3ada9b3 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -3,6 +3,7 @@ namespace RonasIT\Larabuilder\Builders; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; +use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand; class AppBootstrapBuilder extends PHPFileBuilder { @@ -25,4 +26,15 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } + + public function addScheduleCommand(string $command, ?string $environment = null): self + { + $this->traverser->addVisitor(new AddScheduleCommand($command, $environment)); + + $this->addImports([ + 'Illuminate\Support\Facades\Schedule' + ]); + + return $this; + } } diff --git a/src/Visitors/AddArrayPropertyItem.php b/src/Visitors/AddArrayPropertyItem.php index 2bc7dfe..347acd0 100644 --- a/src/Visitors/AddArrayPropertyItem.php +++ b/src/Visitors/AddArrayPropertyItem.php @@ -20,7 +20,7 @@ public function __construct( ) { parent::__construct($name, $value); - list($propertyValue, $propertyType) = $this->getPropertyValue($value); + list($propertyValue) = $this->getPropertyValue($value); $this->arrayItem = new ArrayItem($propertyValue); $arrayNode = new Array_([$this->arrayItem]); diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index ae2fef8..3efab01 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -15,7 +15,7 @@ abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { - protected const FORBIDDEN_NODES = [ + protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, Interface_::class, diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php new file mode 100644 index 0000000..6a65a09 --- /dev/null +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -0,0 +1,130 @@ +renderStatement = $this->buildRenderCall(); + + self::$statements[] = clone $this->renderStatement; + + parent::__construct( + parentMethod: 'withSchedule', + targetMethod: 'command', + ); + } + + protected function insertNode(MethodCall $node): MethodCall + { + self::$statements = []; + + $currentStatements = $node->args[0]->value->stmts; + + if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { + $node->args[0]->value->stmts = [$this->renderStatement]; + + return $node; + } + + $lastExistingStatement = end($currentStatements); + + $this->renderStatement->setAttribute('previous', $lastExistingStatement); + + $node->args[0]->value->stmts[] = $this->renderStatement; + + return $node; + } + + protected function buildRenderCall(): Expression + { + $call = new StaticCall( + class: new Name('Schedule'), + name: new Identifier('command'), + args: [ + new Arg(new String_($this->command)), + ], + ); + + if ($this->environment) { + $call = new MethodCall( + var: $call, + name: new Identifier('environments'), + args: [ + new Arg(new String_($this->environment)), + ], + ); + } + + return new Expression($call); + } + + public function leaveNode(Node $node): Node + { + if ($this->shouldInsertParentNode($node)) { + $node = $this->insertParentNode($node); + } + + return parent::leaveNode($node); + } + + protected function shouldInsertParentNode(Node $node): bool + { + return ($node instanceof MethodCall) + && ($node->name->toString() === 'create') + && $this->isParentMethodMissing($node); + } + + protected function isParentMethodMissing(Node $node): bool + { + $nodeVar = $node->var; + + foreach ($nodeVar as $var) { + if (!empty($var->name) && $var->name === $this->parentMethod) { + return false; + } + + $nodeVar = $var; + } + + return true; + } + + protected function insertParentNode(Node $node): ?Node + { + $statements = []; + + foreach (self::$statements as $statement) { + $statements[] = $statement; + $statements[] = new Nop(); + } + + array_pop($statements); + + $closure = new Closure([ + 'returnType' => new Identifier('void'), + 'stmts' => $statements, + ]); + + $withScheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); + + return new MethodCall($withScheduleCall, new Identifier('create')); + } +} diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 1e81da9..a433bb8 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -119,4 +119,72 @@ public function testInvalidBootstrapAppFileException(string $fixture, string $ty ) ->save(); } + + public function testAddScheduleRenderEmpty(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('bootstrap/app.php', 'expression_empty.php'), + $this->callFilePutContent('bootstrap/app.php', 'schedule.php'), + ); + + new AppBootstrapBuilder() + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + environment: 'production', + ) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:12222', + ) + ->save(); + } + + public function testAddScheduleRenderWithScheduleExists(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('bootstrap/app.php', 'schedule_exists.php'), + $this->callFilePutContent('bootstrap/app.php', 'schedule_exists.php'), + ); + + new AppBootstrapBuilder() + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + environment: 'production', + ) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:12222', + ) + ->save(); + } + + public function testCombineRenderEmpty(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('bootstrap/app.php', 'expression_empty.php'), + $this->callFilePutContent('bootstrap/app.php', 'combine_render.php'), + ); + + new AppBootstrapBuilder() + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + environment: 'production', + ) + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: 'return;', + ) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception_2', + ) + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: 'return;', + ) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception_3', + ) + ->save(); + } } diff --git a/tests/fixtures/AppBootstrapBuilderTest/original/schedule_exists.php b/tests/fixtures/AppBootstrapBuilderTest/original/schedule_exists.php new file mode 100644 index 0000000..a6d5e43 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/original/schedule_exists.php @@ -0,0 +1,17 @@ +withRouting( + web: __DIR__ . '/../routes/web.php', + commands: __DIR__ . '/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withSchedule(function (): void { + + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php new file mode 100644 index 0000000..b465441 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php @@ -0,0 +1,28 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->render(function (HttpException $exception) { + return; + }); + })->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + + Schedule::command('telescope:prune --set-hours=resolved_exception_2'); + + Schedule::command('telescope:prune --set-hours=resolved_exception_3'); +})->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php new file mode 100644 index 0000000..c8b56ed --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -0,0 +1,23 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + + Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); +})->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php new file mode 100644 index 0000000..3d19318 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php @@ -0,0 +1,20 @@ +withRouting( + web: __DIR__ . '/../routes/web.php', + commands: __DIR__ . '/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + + Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); + })->create(); From 1ec1f9110fa792386f16295331a78324e6adb4ea Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Thu, 22 Jan 2026 16:22:07 +0500 Subject: [PATCH 02/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 3ada9b3..43f356e 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -19,7 +19,7 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, $imports = [$exceptionClass]; if ($includeRequestArg) { - $imports[] = 'Illuminate\Http\Request'; + $imports[] = 'Illuminate\\Http\\Request'; } $this->addImports($imports); @@ -32,7 +32,7 @@ public function addScheduleCommand(string $command, ?string $environment = null) $this->traverser->addVisitor(new AddScheduleCommand($command, $environment)); $this->addImports([ - 'Illuminate\Support\Facades\Schedule' + 'Illuminate\\Support\\Facades\\Schedule', ]); return $this; From 8071f7f7b2a3008e93ec2cc781f47bc4ef73cb88 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Thu, 22 Jan 2026 18:25:48 +0500 Subject: [PATCH 03/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 5 +- src/DTO/ScheduleFrequencyOptionsDTO.php | 14 +++++ src/Enums/ScheduleFrequencyMethodEnum.php | 55 +++++++++++++++++ src/Traits/PropertyBuilderTrait.php | 59 +++++++++++++++++++ src/Visitors/AbstractPropertyVisitor.php | 47 +-------------- .../AbstractAppBootstrapVisitor.php | 3 + .../AddScheduleCommand.php | 17 ++++++ tests/AppBootstrapBuilderTest.php | 13 +++- .../results/schedule.php | 2 +- 9 files changed, 166 insertions(+), 49 deletions(-) create mode 100644 src/DTO/ScheduleFrequencyOptionsDTO.php create mode 100644 src/Enums/ScheduleFrequencyMethodEnum.php create mode 100644 src/Traits/PropertyBuilderTrait.php diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 43f356e..140b708 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -2,6 +2,7 @@ namespace RonasIT\Larabuilder\Builders; +use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand; @@ -27,9 +28,9 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } - public function addScheduleCommand(string $command, ?string $environment = null): self + public function addScheduleCommand(string $command, ?string $environment = null, ScheduleFrequencyOptionsDTO ...$frequencyOptions): self { - $this->traverser->addVisitor(new AddScheduleCommand($command, $environment)); + $this->traverser->addVisitor(new AddScheduleCommand($command, $environment, ...$frequencyOptions)); $this->addImports([ 'Illuminate\\Support\\Facades\\Schedule', diff --git a/src/DTO/ScheduleFrequencyOptionsDTO.php b/src/DTO/ScheduleFrequencyOptionsDTO.php new file mode 100644 index 0000000..5e4cdba --- /dev/null +++ b/src/DTO/ScheduleFrequencyOptionsDTO.php @@ -0,0 +1,14 @@ +getPropertyValue($value); + + return new Arg($value); + } + + protected function getPropertyValue(mixed $value): array + { + $type = get_debug_type($value); + + $value = match ($type) { + 'int' => new Int_($value), + 'array' => $this->makeArrayValue($value), + 'string' => new String_($value), + 'float' => new Float_($value), + 'bool' => $this->makeBoolValue($value), + 'null' => new ConstFetch(new Name('null')), + }; + + return [$value, $type]; + } + + protected function makeBoolValue(bool $value): ConstFetch + { + $name = new Name(($value) ? 'true' : 'false'); + + return new ConstFetch($name); + } + + protected function makeArrayValue(array $values): Array_ + { + $items = []; + + foreach ($values as $key => $val) { + list($val) = $this->getPropertyValue($val); + list($key) = $this->getPropertyValue($key); + + $items[] = new ArrayItem($val, $key); + } + + return new Array_($items); + } +} \ No newline at end of file diff --git a/src/Visitors/AbstractPropertyVisitor.php b/src/Visitors/AbstractPropertyVisitor.php index 0eb4838..15b010a 100644 --- a/src/Visitors/AbstractPropertyVisitor.php +++ b/src/Visitors/AbstractPropertyVisitor.php @@ -3,19 +3,15 @@ namespace RonasIT\Larabuilder\Visitors; use PhpParser\Node; -use PhpParser\Node\ArrayItem; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Name; -use PhpParser\Node\Scalar\Float_; -use PhpParser\Node\Scalar\Int_; -use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; +use RonasIT\Larabuilder\Traits\PropertyBuilderTrait; abstract class AbstractPropertyVisitor extends InsertOrUpdateNodeAbstractVisitor { + use PropertyBuilderTrait; + public function __construct( protected string $name, ) { @@ -31,41 +27,4 @@ protected function isParentNode(Node $node): bool { return $node instanceof Class_ || $node instanceof Trait_; } - - protected function getPropertyValue(mixed $value): array - { - $type = get_debug_type($value); - - $value = match ($type) { - 'int' => new Int_($value), - 'array' => $this->makeArrayValue($value), - 'string' => new String_($value), - 'float' => new Float_($value), - 'bool' => $this->makeBoolValue($value), - 'null' => new ConstFetch(new Name('null')), - }; - - return [$value, $type]; - } - - protected function makeBoolValue(bool $value): ConstFetch - { - $name = new Name(($value) ? 'true' : 'false'); - - return new ConstFetch($name); - } - - protected function makeArrayValue(array $values): Array_ - { - $items = []; - - foreach ($values as $key => $val) { - list($val) = $this->getPropertyValue($val); - list($key) = $this->getPropertyValue($key); - - $items[] = new ArrayItem($val, $key); - } - - return new Array_($items); - } } diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 3efab01..473ce97 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -12,9 +12,12 @@ use PhpParser\NodeVisitorAbstract; use PhpParser\ParserFactory; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; +use RonasIT\Larabuilder\Traits\PropertyBuilderTrait; abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { + use PropertyBuilderTrait; + protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 6a65a09..3eed363 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -12,16 +12,21 @@ use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Nop; +use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; class AddScheduleCommand extends AbstractAppBootstrapVisitor { protected Expression $renderStatement; protected static array $statements = []; + protected array $frequencyOptions; public function __construct( protected string $command, protected ?string $environment, + ScheduleFrequencyOptionsDTO ...$frequencyOptions, ) { + $this->frequencyOptions = $frequencyOptions; + $this->renderStatement = $this->buildRenderCall(); self::$statements[] = clone $this->renderStatement; @@ -73,6 +78,18 @@ class: new Name('Schedule'), ); } + if ($this->frequencyOptions) { + foreach ($this->frequencyOptions as $option) { + $args = array_map(fn ($arg) => $this->makeArg($arg), $option->attributes); + + $call = new MethodCall( + var: $call, + name: new Identifier($option->method->value), + args: $args, + ); + } + } + return new Expression($call); } diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index a433bb8..7025e9e 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -5,6 +5,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use RonasIT\Larabuilder\Builders\AppBootstrapBuilder; +use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; +use RonasIT\Larabuilder\Enums\ScheduleFrequencyMethodEnum; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -130,8 +132,15 @@ public function testAddScheduleRenderEmpty(): void new AppBootstrapBuilder() ->addScheduleCommand( - command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - environment: 'production', + 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + 'production', + new ScheduleFrequencyOptionsDTO( + method: ScheduleFrequencyMethodEnum::Daily, + ), + new ScheduleFrequencyOptionsDTO( + method: ScheduleFrequencyMethodEnum::Timezone, + attributes: ['America/New_York'], + ), ) ->addScheduleCommand( command: 'telescope:prune --set-hours=resolved_exception:12222', diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php index c8b56ed..a9f24e9 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -17,7 +17,7 @@ ->withExceptions(function (Exceptions $exceptions): void { // })->withSchedule(function (): void { - Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->daily()->timezone('America/New_York'); Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); })->create(); From 05bac0d16a5926f11074b498c3fe27fa5994f6b7 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Thu, 22 Jan 2026 18:35:47 +0500 Subject: [PATCH 04/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/DTO/ScheduleFrequencyOptionsDTO.php | 2 +- ...PropertyBuilderTrait.php => AstValueBuilderTrait.php} | 2 +- src/Visitors/AbstractPropertyVisitor.php | 4 ++-- .../AppBootstrapVisitors/AbstractAppBootstrapVisitor.php | 4 ++-- src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php | 9 +++++---- 5 files changed, 11 insertions(+), 10 deletions(-) rename src/Traits/{PropertyBuilderTrait.php => AstValueBuilderTrait.php} (98%) diff --git a/src/DTO/ScheduleFrequencyOptionsDTO.php b/src/DTO/ScheduleFrequencyOptionsDTO.php index 5e4cdba..cf82453 100644 --- a/src/DTO/ScheduleFrequencyOptionsDTO.php +++ b/src/DTO/ScheduleFrequencyOptionsDTO.php @@ -11,4 +11,4 @@ public function __construct( public array $attributes = [], ) { } -} \ No newline at end of file +} diff --git a/src/Traits/PropertyBuilderTrait.php b/src/Traits/AstValueBuilderTrait.php similarity index 98% rename from src/Traits/PropertyBuilderTrait.php rename to src/Traits/AstValueBuilderTrait.php index e622a33..9fbf717 100644 --- a/src/Traits/PropertyBuilderTrait.php +++ b/src/Traits/AstValueBuilderTrait.php @@ -11,7 +11,7 @@ use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Scalar\String_; -trait PropertyBuilderTrait +trait AstValueBuilderTrait { protected function makeArg(mixed $value): Arg { diff --git a/src/Visitors/AbstractPropertyVisitor.php b/src/Visitors/AbstractPropertyVisitor.php index 15b010a..de895c2 100644 --- a/src/Visitors/AbstractPropertyVisitor.php +++ b/src/Visitors/AbstractPropertyVisitor.php @@ -6,11 +6,11 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; -use RonasIT\Larabuilder\Traits\PropertyBuilderTrait; +use RonasIT\Larabuilder\Traits\AstValueBuilderTrait; abstract class AbstractPropertyVisitor extends InsertOrUpdateNodeAbstractVisitor { - use PropertyBuilderTrait; + use AstValueBuilderTrait; public function __construct( protected string $name, diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 473ce97..1de3a23 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -12,11 +12,11 @@ use PhpParser\NodeVisitorAbstract; use PhpParser\ParserFactory; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; -use RonasIT\Larabuilder\Traits\PropertyBuilderTrait; +use RonasIT\Larabuilder\Traits\AstValueBuilderTrait; abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { - use PropertyBuilderTrait; + use AstValueBuilderTrait; protected const array FORBIDDEN_NODES = [ Class_::class, diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 3eed363..5f8b49d 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -113,18 +113,19 @@ protected function isParentMethodMissing(Node $node): bool { $nodeVar = $node->var; - foreach ($nodeVar as $var) { - if (!empty($var->name) && $var->name === $this->parentMethod) { + while ($nodeVar instanceof MethodCall) + { + if ($nodeVar->name->toString() === $this->parentMethod) { return false; } - $nodeVar = $var; + $nodeVar = $nodeVar->var; } return true; } - protected function insertParentNode(Node $node): ?Node + protected function insertParentNode(Node $node): Node { $statements = []; From b3a142e9ff77803cb92ffbaa05ce99c13c26f21d Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:01:44 +0500 Subject: [PATCH 05/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/PHPFileBuilder.php | 9 ++++-- src/Printer.php | 31 +++++++++++++++++-- .../AddScheduleCommand.php | 7 +++-- .../Traits/PHPFileBuilderTestMockTrait.php | 4 ++- tests/TestCase.php | 17 ++++++++-- .../results/combine_render.php | 11 ++++--- .../results/schedule.php | 9 +++--- 7 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 3f4cc5e..7c60a20 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -67,14 +67,17 @@ public function addImports(array $imports): self } public function save(): void + { + file_put_contents($this->filePath, $this->toSting()); + } + + public function toSting(): string { $this->traverser->addVisitor(new CloningVisitor()); $oldSyntaxTree = $this->syntaxTree; $newSyntaxTree = $this->traverser->traverse($this->syntaxTree); - $fileContent = (new Printer())->printFormatPreserving($newSyntaxTree, $oldSyntaxTree, $this->oldTokens); - - file_put_contents($this->filePath, $fileContent); + return (new Printer())->printFormatPreserving($newSyntaxTree, $oldSyntaxTree, $this->oldTokens); } } diff --git a/src/Printer.php b/src/Printer.php index 3f1cd96..7adc263 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\PropertyItem; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Property; @@ -86,7 +88,7 @@ protected function pStmt_PreformattedCode(PreformattedCode $node): string $lines = explode("\n", $value); $lines = array_map( - callback: fn (string $line) => (str_starts_with($line, $indent)) ? substr($line, $indentLength) : $line, + callback: fn(string $line) => (str_starts_with($line, $indent)) ? substr($line, $indentLength) : $line, array: $lines, ); @@ -100,4 +102,29 @@ protected function preparePreformattedCode(string $value): string return rtrim($value); } -} + + protected function pExpr_MethodCall(MethodCall $node): string + { + if ($node->getAttribute('isNewCall')) { + return $this->pDereferenceLhs($node->var) + . $this->nl + . "\t->" + . $this->pObjectProperty($node->name) + . '(' . $this->pMaybeMultiline($node->args) . ')'; + } + + return parent::pExpr_MethodCall($node); + } + + protected function pExpr_Closure(Closure $node): string + { + $tab = $node + ->getAttribute('parent') + ?->getAttribute('isNewCall'); + + if (!empty($tab)) { + $this->indent(); + } + + return parent::pExpr_Closure($node); + }} diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 5f8b49d..43840cf 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -141,8 +141,11 @@ protected function insertParentNode(Node $node): Node 'stmts' => $statements, ]); - $withScheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); + $scheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); - return new MethodCall($withScheduleCall, new Identifier('create')); + $scheduleCall->setAttribute('isNewCall', true); + $scheduleCall->args[0]->value->setAttribute('parent', $scheduleCall); + + return new MethodCall($scheduleCall, new Identifier('create')); } } diff --git a/tests/Support/Traits/PHPFileBuilderTestMockTrait.php b/tests/Support/Traits/PHPFileBuilderTestMockTrait.php index 80370dc..960f7e6 100644 --- a/tests/Support/Traits/PHPFileBuilderTestMockTrait.php +++ b/tests/Support/Traits/PHPFileBuilderTestMockTrait.php @@ -15,6 +15,8 @@ protected function callFileGetContent(string $fileName, string $originalFixture) protected function callFilePutContent(string $fileName, string $resultFixture, int $flags = 0): array { - return $this->functionCall('file_put_contents', [$fileName, $this->getFixture("results/{$resultFixture}"), $flags]); + $original = $this->getOriginalFixture($fileName); + + return $this->functionCall('file_put_contents', [$original, $this->getFixture("results/{$resultFixture}"), $flags]); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 64822ea..633ba90 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,11 +11,24 @@ class TestCase extends BaseTestCase use TestingTrait; public function getFixturePath(string $fixtureName): string + { + $className = $this->getTestName(); + + return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; + } + + protected function getOriginalFixture(string $fileName): string + { + $className = $this->getTestName(); + + return __DIR__ . "/fixtures/{$className}/original/{$fileName}.php"; + } + + protected function getTestName(): string { $class = get_class($this); $explodedClass = explode('\\', $class); - $className = Arr::last($explodedClass); - return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; + return Arr::last($explodedClass); } } diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php index b465441..3d3e260 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php @@ -19,10 +19,11 @@ $exceptions->render(function (HttpException $exception) { return; }); - })->withSchedule(function (): void { - Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + }) + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); - Schedule::command('telescope:prune --set-hours=resolved_exception_2'); + Schedule::command('telescope:prune --set-hours=resolved_exception_2'); - Schedule::command('telescope:prune --set-hours=resolved_exception_3'); -})->create(); + Schedule::command('telescope:prune --set-hours=resolved_exception_3'); + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php index a9f24e9..dee246b 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -16,8 +16,9 @@ }) ->withExceptions(function (Exceptions $exceptions): void { // - })->withSchedule(function (): void { - Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->daily()->timezone('America/New_York'); + }) + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->daily()->timezone('America/New_York'); - Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); -})->create(); + Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); + })->create(); From 08bb80f8015798be85df8e8b987e97c014ec45bb Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:02:38 +0500 Subject: [PATCH 06/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/PHPFileBuilder.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 7c60a20..3f4cc5e 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -67,17 +67,14 @@ public function addImports(array $imports): self } public function save(): void - { - file_put_contents($this->filePath, $this->toSting()); - } - - public function toSting(): string { $this->traverser->addVisitor(new CloningVisitor()); $oldSyntaxTree = $this->syntaxTree; $newSyntaxTree = $this->traverser->traverse($this->syntaxTree); - return (new Printer())->printFormatPreserving($newSyntaxTree, $oldSyntaxTree, $this->oldTokens); + $fileContent = (new Printer())->printFormatPreserving($newSyntaxTree, $oldSyntaxTree, $this->oldTokens); + + file_put_contents($this->filePath, $fileContent); } } From a47a8616a68276f730da61e800ed9c897ffe0861 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:04:42 +0500 Subject: [PATCH 07/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Printer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Printer.php b/src/Printer.php index 7adc263..e841150 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -88,7 +88,7 @@ protected function pStmt_PreformattedCode(PreformattedCode $node): string $lines = explode("\n", $value); $lines = array_map( - callback: fn(string $line) => (str_starts_with($line, $indent)) ? substr($line, $indentLength) : $line, + callback: fn (string $line) => (str_starts_with($line, $indent)) ? substr($line, $indentLength) : $line, array: $lines, ); From d3835da5742cc7ea16a9798c4d51c3fcf0fb56b3 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:07:07 +0500 Subject: [PATCH 08/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- tests/Support/Traits/PHPFileBuilderTestMockTrait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Support/Traits/PHPFileBuilderTestMockTrait.php b/tests/Support/Traits/PHPFileBuilderTestMockTrait.php index 960f7e6..80370dc 100644 --- a/tests/Support/Traits/PHPFileBuilderTestMockTrait.php +++ b/tests/Support/Traits/PHPFileBuilderTestMockTrait.php @@ -15,8 +15,6 @@ protected function callFileGetContent(string $fileName, string $originalFixture) protected function callFilePutContent(string $fileName, string $resultFixture, int $flags = 0): array { - $original = $this->getOriginalFixture($fileName); - - return $this->functionCall('file_put_contents', [$original, $this->getFixture("results/{$resultFixture}"), $flags]); + return $this->functionCall('file_put_contents', [$fileName, $this->getFixture("results/{$resultFixture}"), $flags]); } } From 58ade355fbd0789b7d1d4f27432c4df8455537cf Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:07:48 +0500 Subject: [PATCH 09/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- tests/TestCase.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 633ba90..64822ea 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,24 +11,11 @@ class TestCase extends BaseTestCase use TestingTrait; public function getFixturePath(string $fixtureName): string - { - $className = $this->getTestName(); - - return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; - } - - protected function getOriginalFixture(string $fileName): string - { - $className = $this->getTestName(); - - return __DIR__ . "/fixtures/{$className}/original/{$fileName}.php"; - } - - protected function getTestName(): string { $class = get_class($this); $explodedClass = explode('\\', $class); + $className = Arr::last($explodedClass); - return Arr::last($explodedClass); + return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; } } From b307beb0a8cafa9f70e0a8d52758954b31d4440e Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:10:31 +0500 Subject: [PATCH 10/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Traits/AstValueBuilderTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/AstValueBuilderTrait.php b/src/Traits/AstValueBuilderTrait.php index 9fbf717..7c65238 100644 --- a/src/Traits/AstValueBuilderTrait.php +++ b/src/Traits/AstValueBuilderTrait.php @@ -56,4 +56,4 @@ protected function makeArrayValue(array $values): Array_ return new Array_($items); } -} \ No newline at end of file +} From bfd5cb5d73d84228259f4310d8993bb5545e5761 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 14:12:29 +0500 Subject: [PATCH 11/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 43840cf..e529d9e 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -29,7 +29,7 @@ public function __construct( $this->renderStatement = $this->buildRenderCall(); - self::$statements[] = clone $this->renderStatement; + self::$statements[] = $this->renderStatement; parent::__construct( parentMethod: 'withSchedule', From 57580d9243480bcdeee10e3ba242d0cd460598b8 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 21:51:33 +0500 Subject: [PATCH 12/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php index dad9e46..4f828b3 100644 --- a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php +++ b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php @@ -6,6 +6,8 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; +use RonasIT\Larabuilder\Traits\AstValueBuilderTrait; +use RonasIT\Larabuilder\Visitors\InsertOrUpdateNodeAbstractVisitor; abstract class AbstractPropertyVisitor extends InsertOrUpdateNodeAbstractVisitor { From 7c35d34557fb5608e218fe55cc247bee74df9754 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 21:54:06 +0500 Subject: [PATCH 13/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Printer.php | 3 ++- src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Printer.php b/src/Printer.php index e841150..e93a124 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -127,4 +127,5 @@ protected function pExpr_Closure(Closure $node): string } return parent::pExpr_Closure($node); - }} + } +} diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index e529d9e..b206328 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -113,8 +113,7 @@ protected function isParentMethodMissing(Node $node): bool { $nodeVar = $node->var; - while ($nodeVar instanceof MethodCall) - { + while ($nodeVar instanceof MethodCall) { if ($nodeVar->name->toString() === $this->parentMethod) { return false; } From e12f780cb7668b9985e967e4cf24c0e4c45d0b70 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 22:03:54 +0500 Subject: [PATCH 14/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Printer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Printer.php b/src/Printer.php index e93a124..5111706 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -108,7 +108,7 @@ protected function pExpr_MethodCall(MethodCall $node): string if ($node->getAttribute('isNewCall')) { return $this->pDereferenceLhs($node->var) . $this->nl - . "\t->" + . " ->" . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; } From b08bfbeaebae8f8b92620c01de5c695c8adb7456 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 22:06:13 +0500 Subject: [PATCH 15/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Printer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Printer.php b/src/Printer.php index 5111706..42b21c1 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -108,7 +108,7 @@ protected function pExpr_MethodCall(MethodCall $node): string if ($node->getAttribute('isNewCall')) { return $this->pDereferenceLhs($node->var) . $this->nl - . " ->" + . ' ->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; } From 1bb478b0c6726d5b389ad0311aafc477d6a7578f Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Mon, 26 Jan 2026 22:24:31 +0500 Subject: [PATCH 16/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- tests/AppBootstrapBuilderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 7025e9e..28d2414 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -122,7 +122,7 @@ public function testInvalidBootstrapAppFileException(string $fixture, string $ty ->save(); } - public function testAddScheduleRenderEmpty(): void + public function testAddScheduleCommandEmpty(): void { $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', @@ -148,7 +148,7 @@ public function testAddScheduleRenderEmpty(): void ->save(); } - public function testAddScheduleRenderWithScheduleExists(): void + public function testAddScheduleCommandWithScheduleExists(): void { $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', @@ -167,7 +167,7 @@ public function testAddScheduleRenderWithScheduleExists(): void ->save(); } - public function testCombineRenderEmpty(): void + public function testCombineScheduleAndExceptionRenders(): void { $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', From b2f486c6d7da74430e7b7c492b985795fa24a4df Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 27 Jan 2026 15:49:21 +0500 Subject: [PATCH 17/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 4 +- src/Printer.php | 25 ++++-------- .../AddScheduleCommand.php | 38 ++++++++----------- 3 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 140b708..4f5ee6b 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -20,7 +20,7 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, $imports = [$exceptionClass]; if ($includeRequestArg) { - $imports[] = 'Illuminate\\Http\\Request'; + $imports[] = 'Illuminate\Http\Request'; } $this->addImports($imports); @@ -33,7 +33,7 @@ public function addScheduleCommand(string $command, ?string $environment = null, $this->traverser->addVisitor(new AddScheduleCommand($command, $environment, ...$frequencyOptions)); $this->addImports([ - 'Illuminate\\Support\\Facades\\Schedule', + 'Illuminate\Support\Facades\Schedule', ]); return $this; diff --git a/src/Printer.php b/src/Printer.php index 42b21c1..e40e9f6 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\PropertyItem; use PhpParser\Node\Stmt\Expression; @@ -105,27 +104,17 @@ protected function preparePreformattedCode(string $value): string protected function pExpr_MethodCall(MethodCall $node): string { - if ($node->getAttribute('isNewCall')) { - return $this->pDereferenceLhs($node->var) - . $this->nl - . ' ->' - . $this->pObjectProperty($node->name) - . '(' . $this->pMaybeMultiline($node->args) . ')'; - } + if ($node->getAttribute('wasCreated')) { + $this->indent(); - return parent::pExpr_MethodCall($node); - } + $newCall = $this->nl . '->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; - protected function pExpr_Closure(Closure $node): string - { - $tab = $node - ->getAttribute('parent') - ?->getAttribute('isNewCall'); + $this->outdent(); + + return $this->pDereferenceLhs($node->var) . $newCall; - if (!empty($tab)) { - $this->indent(); } - return parent::pExpr_Closure($node); + return parent::pExpr_MethodCall($node); } } diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index b206328..ec9564e 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -17,7 +17,6 @@ class AddScheduleCommand extends AbstractAppBootstrapVisitor { protected Expression $renderStatement; - protected static array $statements = []; protected array $frequencyOptions; public function __construct( @@ -29,8 +28,6 @@ public function __construct( $this->renderStatement = $this->buildRenderCall(); - self::$statements[] = $this->renderStatement; - parent::__construct( parentMethod: 'withSchedule', targetMethod: 'command', @@ -39,8 +36,6 @@ public function __construct( protected function insertNode(MethodCall $node): MethodCall { - self::$statements = []; - $currentStatements = $node->args[0]->value->stmts; if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { @@ -95,8 +90,14 @@ class: new Name('Schedule'), public function leaveNode(Node $node): Node { - if ($this->shouldInsertParentNode($node)) { - $node = $this->insertParentNode($node); + if ($node instanceof MethodCall) { + if ($this->shouldInsertParentNode($node)) { + $node = $this->insertParentNode($node); + } + + if ($node->var->getAttribute('wasCreated')) { + $node->var = parent::leaveNode($node->var); + } } return parent::leaveNode($node); @@ -104,8 +105,7 @@ public function leaveNode(Node $node): Node protected function shouldInsertParentNode(Node $node): bool { - return ($node instanceof MethodCall) - && ($node->name->toString() === 'create') + return ($node->name->toString() === 'create') && $this->isParentMethodMissing($node); } @@ -126,25 +126,19 @@ protected function isParentMethodMissing(Node $node): bool protected function insertParentNode(Node $node): Node { - $statements = []; - - foreach (self::$statements as $statement) { - $statements[] = $statement; - $statements[] = new Nop(); - } - - array_pop($statements); - $closure = new Closure([ 'returnType' => new Identifier('void'), - 'stmts' => $statements, ]); $scheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); + $scheduleCall->setAttribute('wasCreated', true); + $scheduleCall + ->args[0] + ->value + ->setAttribute('parent', $scheduleCall); - $scheduleCall->setAttribute('isNewCall', true); - $scheduleCall->args[0]->value->setAttribute('parent', $scheduleCall); + $createCall = new MethodCall($scheduleCall, new Identifier('create')); - return new MethodCall($scheduleCall, new Identifier('create')); + return $createCall; } } From d6c0d719e433de0927a21786f2d868b5c667ce7e Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 27 Jan 2026 15:58:03 +0500 Subject: [PATCH 18/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Printer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Printer.php b/src/Printer.php index e40e9f6..cd85999 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -107,12 +107,11 @@ protected function pExpr_MethodCall(MethodCall $node): string if ($node->getAttribute('wasCreated')) { $this->indent(); - $newCall = $this->nl . '->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; + $newCall = $this->nl . '->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; $this->outdent(); return $this->pDereferenceLhs($node->var) . $newCall; - } return parent::pExpr_MethodCall($node); From 0cebe130a9bd42fbf1406bd84bac8b59372edff3 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 27 Jan 2026 16:18:17 +0500 Subject: [PATCH 19/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- .../AppBootstrapVisitors/AddScheduleCommand.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index ec9564e..e0e2e43 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -16,7 +16,7 @@ class AddScheduleCommand extends AbstractAppBootstrapVisitor { - protected Expression $renderStatement; + protected Expression $scheduleStatement; protected array $frequencyOptions; public function __construct( @@ -26,7 +26,7 @@ public function __construct( ) { $this->frequencyOptions = $frequencyOptions; - $this->renderStatement = $this->buildRenderCall(); + $this->scheduleStatement = $this->buildScheduleCall(); parent::__construct( parentMethod: 'withSchedule', @@ -39,21 +39,21 @@ protected function insertNode(MethodCall $node): MethodCall $currentStatements = $node->args[0]->value->stmts; if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { - $node->args[0]->value->stmts = [$this->renderStatement]; + $node->args[0]->value->stmts = [$this->scheduleStatement]; return $node; } $lastExistingStatement = end($currentStatements); - $this->renderStatement->setAttribute('previous', $lastExistingStatement); + $this->scheduleStatement->setAttribute('previous', $lastExistingStatement); - $node->args[0]->value->stmts[] = $this->renderStatement; + $node->args[0]->value->stmts[] = $this->scheduleStatement; return $node; } - protected function buildRenderCall(): Expression + protected function buildScheduleCall(): Expression { $call = new StaticCall( class: new Name('Schedule'), From 6da6f6da6d102d7276f2948292c77ef2ca93bf7a Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 27 Jan 2026 16:40:54 +0500 Subject: [PATCH 20/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index e0e2e43..c735610 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -137,8 +137,6 @@ protected function insertParentNode(Node $node): Node ->value ->setAttribute('parent', $scheduleCall); - $createCall = new MethodCall($scheduleCall, new Identifier('create')); - - return $createCall; + return new MethodCall($scheduleCall, new Identifier('create')); } } From 50acbe6e88d0fe183466ae914c75a2904686cc48 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 30 Jan 2026 10:33:05 +0500 Subject: [PATCH 21/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 8 +-- src/Traits/AstValueBuilderTrait.php | 42 --------------- .../AbstractAppBootstrapVisitor.php | 7 +-- ...AddScheduleCommand.php => AddSchedule.php} | 6 +-- src/Visitors/BaseNodeVisitorAbstract.php | 51 +++++++++++++++++++ .../AbstractPropertyVisitor.php | 3 -- tests/AppBootstrapBuilderTest.php | 14 ++--- .../results/combine_render.php | 2 +- .../results/schedule.php | 2 +- .../results/schedule_exists.php | 2 +- 10 files changed, 68 insertions(+), 69 deletions(-) rename src/Visitors/AppBootstrapVisitors/{AddScheduleCommand.php => AddSchedule.php} (95%) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 4f5ee6b..3800a68 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -4,7 +4,7 @@ use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; -use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand; +use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddSchedule; class AppBootstrapBuilder extends PHPFileBuilder { @@ -28,12 +28,12 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } - public function addScheduleCommand(string $command, ?string $environment = null, ScheduleFrequencyOptionsDTO ...$frequencyOptions): self + public function addSchedule(string $command, ?string $environment = null, ScheduleFrequencyOptionsDTO ...$frequencyOptions): self { - $this->traverser->addVisitor(new AddScheduleCommand($command, $environment, ...$frequencyOptions)); + $this->traverser->addVisitor(new AddSchedule($command, $environment, ...$frequencyOptions)); $this->addImports([ - 'Illuminate\Support\Facades\Schedule', + 'Illuminate\Console\Scheduling\Schedule', ]); return $this; diff --git a/src/Traits/AstValueBuilderTrait.php b/src/Traits/AstValueBuilderTrait.php index 7c65238..b17d77a 100644 --- a/src/Traits/AstValueBuilderTrait.php +++ b/src/Traits/AstValueBuilderTrait.php @@ -13,47 +13,5 @@ trait AstValueBuilderTrait { - protected function makeArg(mixed $value): Arg - { - list($value) = $this->getPropertyValue($value); - return new Arg($value); - } - - protected function getPropertyValue(mixed $value): array - { - $type = get_debug_type($value); - - $value = match ($type) { - 'int' => new Int_($value), - 'array' => $this->makeArrayValue($value), - 'string' => new String_($value), - 'float' => new Float_($value), - 'bool' => $this->makeBoolValue($value), - 'null' => new ConstFetch(new Name('null')), - }; - - return [$value, $type]; - } - - protected function makeBoolValue(bool $value): ConstFetch - { - $name = new Name(($value) ? 'true' : 'false'); - - return new ConstFetch($name); - } - - protected function makeArrayValue(array $values): Array_ - { - $items = []; - - foreach ($values as $key => $val) { - list($val) = $this->getPropertyValue($val); - list($key) = $this->getPropertyValue($key); - - $items[] = new ArrayItem($val, $key); - } - - return new Array_($items); - } } diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 1de3a23..8f62a17 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -9,15 +9,12 @@ use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; -use PhpParser\NodeVisitorAbstract; use PhpParser\ParserFactory; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; -use RonasIT\Larabuilder\Traits\AstValueBuilderTrait; +use RonasIT\Larabuilder\Visitors\BaseNodeVisitorAbstract; -abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract +abstract class AbstractAppBootstrapVisitor extends BaseNodeVisitorAbstract { - use AstValueBuilderTrait; - protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddSchedule.php similarity index 95% rename from src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php rename to src/Visitors/AppBootstrapVisitors/AddSchedule.php index c735610..062872d 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddSchedule.php @@ -14,7 +14,7 @@ use PhpParser\Node\Stmt\Nop; use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; -class AddScheduleCommand extends AbstractAppBootstrapVisitor +class AddSchedule extends AbstractAppBootstrapVisitor { protected Expression $scheduleStatement; protected array $frequencyOptions; @@ -132,10 +132,6 @@ protected function insertParentNode(Node $node): Node $scheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); $scheduleCall->setAttribute('wasCreated', true); - $scheduleCall - ->args[0] - ->value - ->setAttribute('parent', $scheduleCall); return new MethodCall($scheduleCall, new Identifier('create')); } diff --git a/src/Visitors/BaseNodeVisitorAbstract.php b/src/Visitors/BaseNodeVisitorAbstract.php index 2dd3cce..adc5009 100644 --- a/src/Visitors/BaseNodeVisitorAbstract.php +++ b/src/Visitors/BaseNodeVisitorAbstract.php @@ -3,7 +3,14 @@ namespace RonasIT\Larabuilder\Visitors; use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\Float_; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; @@ -79,4 +86,48 @@ protected function setParentForNode(Node $child, Node $parent): void } } } + + protected function makeArg(mixed $value): Arg + { + list($value) = $this->getPropertyValue($value); + + return new Arg($value); + } + + protected function getPropertyValue(mixed $value): array + { + $type = get_debug_type($value); + + $value = match ($type) { + 'int' => new Int_($value), + 'array' => $this->makeArrayValue($value), + 'string' => new String_($value), + 'float' => new Float_($value), + 'bool' => $this->makeBoolValue($value), + 'null' => new ConstFetch(new Name('null')), + }; + + return [$value, $type]; + } + + protected function makeBoolValue(bool $value): ConstFetch + { + $name = new Name(($value) ? 'true' : 'false'); + + return new ConstFetch($name); + } + + protected function makeArrayValue(array $values): Array_ + { + $items = []; + + foreach ($values as $key => $val) { + list($val) = $this->getPropertyValue($val); + list($key) = $this->getPropertyValue($key); + + $items[] = new ArrayItem($val, $key); + } + + return new Array_($items); + } } diff --git a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php index 4f828b3..90ffad2 100644 --- a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php +++ b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php @@ -6,13 +6,10 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; -use RonasIT\Larabuilder\Traits\AstValueBuilderTrait; use RonasIT\Larabuilder\Visitors\InsertOrUpdateNodeAbstractVisitor; abstract class AbstractPropertyVisitor extends InsertOrUpdateNodeAbstractVisitor { - use AstValueBuilderTrait; - public function __construct( protected string $name, ) { diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 28d2414..fb18428 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -131,7 +131,7 @@ public function testAddScheduleCommandEmpty(): void ); new AppBootstrapBuilder() - ->addScheduleCommand( + ->addSchedule( 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', 'production', new ScheduleFrequencyOptionsDTO( @@ -142,7 +142,7 @@ public function testAddScheduleCommandEmpty(): void attributes: ['America/New_York'], ), ) - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:12222', ) ->save(); @@ -157,11 +157,11 @@ public function testAddScheduleCommandWithScheduleExists(): void ); new AppBootstrapBuilder() - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', environment: 'production', ) - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:12222', ) ->save(); @@ -176,7 +176,7 @@ public function testCombineScheduleAndExceptionRenders(): void ); new AppBootstrapBuilder() - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', environment: 'production', ) @@ -184,14 +184,14 @@ public function testCombineScheduleAndExceptionRenders(): void exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception_2', ) ->addExceptionsRender( exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addScheduleCommand( + ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception_3', ) ->save(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php index 3d3e260..bec0e04 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php @@ -3,7 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Support\Facades\Schedule; +use Illuminate\Console\Scheduling\Schedule; use Symfony\Component\HttpKernel\Exception\HttpException; return Application::configure(basePath: dirname(__DIR__)) diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php index dee246b..5526a65 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -3,7 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Support\Facades\Schedule; +use Illuminate\Console\Scheduling\Schedule; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php index 3d19318..313067b 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php @@ -2,7 +2,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Support\Facades\Schedule; +use Illuminate\Console\Scheduling\Schedule; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( From 769d14e4ed6901771f1aa4dcd11b116b1c9a1f93 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 30 Jan 2026 10:43:56 +0500 Subject: [PATCH 22/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Traits/AstValueBuilderTrait.php | 17 ----------------- .../AbstractAppBootstrapVisitor.php | 1 - 2 files changed, 18 deletions(-) delete mode 100644 src/Traits/AstValueBuilderTrait.php diff --git a/src/Traits/AstValueBuilderTrait.php b/src/Traits/AstValueBuilderTrait.php deleted file mode 100644 index b17d77a..0000000 --- a/src/Traits/AstValueBuilderTrait.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Fri, 30 Jan 2026 11:49:10 +0500 Subject: [PATCH 23/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 6 +- src/DTO/ScheduleFrequencyOptionsDTO.php | 14 ----- src/DTO/ScheduleOptionDTO.php | 40 ++++++++++++ src/Enums/ScheduleFrequencyMethodEnum.php | 55 ---------------- .../AppBootstrapVisitors/AddSchedule.php | 25 +++----- tests/AppBootstrapBuilderTest.php | 42 ++++++------- tests/TestCase.php | 11 ++++ .../exceptions/invalid_schedule_option.txt | 63 +++++++++++++++++++ .../results/schedule.php | 2 +- 9 files changed, 145 insertions(+), 113 deletions(-) delete mode 100644 src/DTO/ScheduleFrequencyOptionsDTO.php create mode 100644 src/DTO/ScheduleOptionDTO.php delete mode 100644 src/Enums/ScheduleFrequencyMethodEnum.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 3800a68..5f0b046 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -2,7 +2,7 @@ namespace RonasIT\Larabuilder\Builders; -use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; +use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddSchedule; @@ -28,9 +28,9 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } - public function addSchedule(string $command, ?string $environment = null, ScheduleFrequencyOptionsDTO ...$frequencyOptions): self + public function addSchedule(string $command, ScheduleOptionDTO ...$options): self { - $this->traverser->addVisitor(new AddSchedule($command, $environment, ...$frequencyOptions)); + $this->traverser->addVisitor(new AddSchedule($command, ...$options)); $this->addImports([ 'Illuminate\Console\Scheduling\Schedule', diff --git a/src/DTO/ScheduleFrequencyOptionsDTO.php b/src/DTO/ScheduleFrequencyOptionsDTO.php deleted file mode 100644 index cf82453..0000000 --- a/src/DTO/ScheduleFrequencyOptionsDTO.php +++ /dev/null @@ -1,14 +0,0 @@ -validateMethod($this->method); + } + + private function validateMethod(string $method): void + { + $methods = array_merge( + $this->getMethods(ManagesAttributes::class), + $this->getMethods(ManagesFrequencies::class), + ); + + if (!in_array($method, $methods, true)) { + $methods = implode("\n", $methods); + + throw new InvalidArgumentException("Unknown schedule method `{$method}`.\nAllowed methods:\n{$methods}"); + } + } + + private function getMethods(string $class): array + { + $schedulePublicMethods = new ReflectionClass($class)->getMethods(ReflectionMethod::IS_PUBLIC); + + return array_map(fn (ReflectionMethod $method) => $method->getName(), $schedulePublicMethods); + } +} diff --git a/src/Enums/ScheduleFrequencyMethodEnum.php b/src/Enums/ScheduleFrequencyMethodEnum.php deleted file mode 100644 index 9a4381c..0000000 --- a/src/Enums/ScheduleFrequencyMethodEnum.php +++ /dev/null @@ -1,55 +0,0 @@ -frequencyOptions = $frequencyOptions; + $this->options = $options; $this->scheduleStatement = $this->buildScheduleCall(); @@ -63,23 +62,13 @@ class: new Name('Schedule'), ], ); - if ($this->environment) { - $call = new MethodCall( - var: $call, - name: new Identifier('environments'), - args: [ - new Arg(new String_($this->environment)), - ], - ); - } - - if ($this->frequencyOptions) { - foreach ($this->frequencyOptions as $option) { + if ($this->options) { + foreach ($this->options as $option) { $args = array_map(fn ($arg) => $this->makeArg($arg), $option->attributes); $call = new MethodCall( var: $call, - name: new Identifier($option->method->value), + name: new Identifier($option->method), args: $args, ); } diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index fb18428..924c91e 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -2,11 +2,11 @@ namespace RonasIT\Larabuilder\Tests; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use RonasIT\Larabuilder\Builders\AppBootstrapBuilder; -use RonasIT\Larabuilder\DTO\ScheduleFrequencyOptionsDTO; -use RonasIT\Larabuilder\Enums\ScheduleFrequencyMethodEnum; +use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -133,18 +133,15 @@ public function testAddScheduleCommandEmpty(): void new AppBootstrapBuilder() ->addSchedule( 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - 'production', - new ScheduleFrequencyOptionsDTO( - method: ScheduleFrequencyMethodEnum::Daily, - ), - new ScheduleFrequencyOptionsDTO( - method: ScheduleFrequencyMethodEnum::Timezone, + new ScheduleOptionDTO('environments', ['production']), + new ScheduleOptionDTO('evenInMaintenanceMode'), + new ScheduleOptionDTO('daily'), + new ScheduleOptionDTO( + method: 'timezone', attributes: ['America/New_York'], ), ) - ->addSchedule( - command: 'telescope:prune --set-hours=resolved_exception:12222', - ) + ->addSchedule('telescope:prune --set-hours=resolved_exception:12222') ->save(); } @@ -159,11 +156,9 @@ public function testAddScheduleCommandWithScheduleExists(): void new AppBootstrapBuilder() ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - environment: 'production', - ) - ->addSchedule( - command: 'telescope:prune --set-hours=resolved_exception:12222', + options: new ScheduleOptionDTO('environments', ['production']), ) + ->addSchedule('telescope:prune --set-hours=resolved_exception:12222') ->save(); } @@ -178,22 +173,25 @@ public function testCombineScheduleAndExceptionRenders(): void new AppBootstrapBuilder() ->addSchedule( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - environment: 'production', + options: new ScheduleOptionDTO('environments', ['production']), ) ->addExceptionsRender( exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addSchedule( - command: 'telescope:prune --set-hours=resolved_exception_2', - ) + ->addSchedule('telescope:prune --set-hours=resolved_exception_2') ->addExceptionsRender( exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addSchedule( - command: 'telescope:prune --set-hours=resolved_exception_3', - ) + ->addSchedule('telescope:prune --set-hours=resolved_exception_3') ->save(); } + + public function testScheduleOptionDTOInvalidMethod(): void + { + $this->threwException(InvalidArgumentException::class, 'invalid_schedule_option'); + + new ScheduleOptionDTO('invalid_frequency'); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 64822ea..7c494ff 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,4 +18,15 @@ public function getFixturePath(string $fixtureName): string return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; } + + public function threwException(string $class, string $fixtureName): void + { + $this->expectException($class); + + $fixtureName = (str_contains($fixtureName, '.')) ? $fixtureName : "{$fixtureName}.txt"; + + $message = $this->getFixture("exceptions/{$fixtureName}"); + + $this->expectExceptionMessage($message); + } } diff --git a/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt new file mode 100644 index 0000000..55c71dd --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt @@ -0,0 +1,63 @@ +Unknown schedule method `invalid_frequency`. +Allowed methods: +user +environments +evenInMaintenanceMode +withoutOverlapping +onOneServer +runInBackground +when +skip +name +description +cron +between +unlessBetween +everySecond +everyTwoSeconds +everyFiveSeconds +everyTenSeconds +everyFifteenSeconds +everyTwentySeconds +everyThirtySeconds +everyMinute +everyTwoMinutes +everyThreeMinutes +everyFourMinutes +everyFiveMinutes +everyTenMinutes +everyFifteenMinutes +everyThirtyMinutes +hourly +hourlyAt +everyOddHour +everyTwoHours +everyThreeHours +everyFourHours +everySixHours +daily +at +dailyAt +twiceDaily +twiceDailyAt +weekdays +weekends +mondays +tuesdays +wednesdays +thursdays +fridays +saturdays +sundays +weekly +weeklyOn +monthly +monthlyOn +twiceMonthly +lastDayOfMonth +quarterly +quarterlyOn +yearly +yearlyOn +days +timezone \ No newline at end of file diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php index 5526a65..cf6bd92 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -18,7 +18,7 @@ // }) ->withSchedule(function (): void { - Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->daily()->timezone('America/New_York'); + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->evenInMaintenanceMode()->daily()->timezone('America/New_York'); Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); })->create(); From 13a8f091e7ef0946cdf980f98c5f571fd5ddce89 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 30 Jan 2026 11:53:25 +0500 Subject: [PATCH 24/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 6 +++--- .../{AddSchedule.php => AddScheduleCommand.php} | 2 +- tests/AppBootstrapBuilderTest.php | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/Visitors/AppBootstrapVisitors/{AddSchedule.php => AddScheduleCommand.php} (98%) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 5f0b046..3a3d352 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -4,7 +4,7 @@ use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; -use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddSchedule; +use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand; class AppBootstrapBuilder extends PHPFileBuilder { @@ -28,9 +28,9 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } - public function addSchedule(string $command, ScheduleOptionDTO ...$options): self + public function addScheduleCommand(string $command, ScheduleOptionDTO ...$options): self { - $this->traverser->addVisitor(new AddSchedule($command, ...$options)); + $this->traverser->addVisitor(new AddScheduleCommand($command, ...$options)); $this->addImports([ 'Illuminate\Console\Scheduling\Schedule', diff --git a/src/Visitors/AppBootstrapVisitors/AddSchedule.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php similarity index 98% rename from src/Visitors/AppBootstrapVisitors/AddSchedule.php rename to src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 7357a9b..177ea5b 100644 --- a/src/Visitors/AppBootstrapVisitors/AddSchedule.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -14,7 +14,7 @@ use PhpParser\Node\Stmt\Nop; use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; -class AddSchedule extends AbstractAppBootstrapVisitor +class AddScheduleCommand extends AbstractAppBootstrapVisitor { protected Expression $scheduleStatement; protected array $options; diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 924c91e..6d959ce 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -131,7 +131,7 @@ public function testAddScheduleCommandEmpty(): void ); new AppBootstrapBuilder() - ->addSchedule( + ->addScheduleCommand( 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', new ScheduleOptionDTO('environments', ['production']), new ScheduleOptionDTO('evenInMaintenanceMode'), @@ -141,7 +141,7 @@ public function testAddScheduleCommandEmpty(): void attributes: ['America/New_York'], ), ) - ->addSchedule('telescope:prune --set-hours=resolved_exception:12222') + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') ->save(); } @@ -154,11 +154,11 @@ public function testAddScheduleCommandWithScheduleExists(): void ); new AppBootstrapBuilder() - ->addSchedule( + ->addScheduleCommand( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', options: new ScheduleOptionDTO('environments', ['production']), ) - ->addSchedule('telescope:prune --set-hours=resolved_exception:12222') + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') ->save(); } @@ -171,7 +171,7 @@ public function testCombineScheduleAndExceptionRenders(): void ); new AppBootstrapBuilder() - ->addSchedule( + ->addScheduleCommand( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', options: new ScheduleOptionDTO('environments', ['production']), ) @@ -179,12 +179,12 @@ public function testCombineScheduleAndExceptionRenders(): void exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addSchedule('telescope:prune --set-hours=resolved_exception_2') + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception_2') ->addExceptionsRender( exceptionClass: HttpException::class, renderBody: 'return;', ) - ->addSchedule('telescope:prune --set-hours=resolved_exception_3') + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception_3') ->save(); } From 5e4197981894df06f1936ec5c77a85d228866c3f Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov <110885041+AZabolotnikov@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:35:53 +0500 Subject: [PATCH 25/46] Update tests/AppBootstrapBuilderTest.php Co-authored-by: Artyom Osepyan <152782500+artengin@users.noreply.github.com> --- tests/AppBootstrapBuilderTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 6d959ce..9b35564 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -136,10 +136,7 @@ public function testAddScheduleCommandEmpty(): void new ScheduleOptionDTO('environments', ['production']), new ScheduleOptionDTO('evenInMaintenanceMode'), new ScheduleOptionDTO('daily'), - new ScheduleOptionDTO( - method: 'timezone', - attributes: ['America/New_York'], - ), + new ScheduleOptionDTO('timezone', ['America/New_York']), ) ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') ->save(); From dd730fdd1eeb5b8a40fd11858cd32f6919b3e4e7 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 30 Jan 2026 12:36:10 +0500 Subject: [PATCH 26/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- README.md | 20 ++++++++++++++++++++ tests/AppBootstrapBuilderTest.php | 5 ++++- tests/TestCase.php | 8 ++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 65c4a22..4381d78 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,23 @@ Adds a new exception render to the `withExceptions` called method in case it doe render for the passed exception class. **Note** Need to provide the full exception class name (FQCN) to the method, it automatically imports it. + +#### addScheduleCommand + +Adds a scheduled command to the `withSchedule` method. If `withSchedule` does not exist, it will be created first; +the new scheduled command will then be inserted into its closure. + +Example usage: +```php +new AppBootstrapBuilder() + ->addScheduleCommand( + 'command', + new ScheduleOptionDTO('environments', ['production']), + new ScheduleOptionDTO('daily'), + new ScheduleOptionDTO( + method: 'timezone', + attributes: ['America/New_York'], + ), + ) + ->save(); +``` \ No newline at end of file diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 6d959ce..ff9e117 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -190,7 +190,10 @@ public function testCombineScheduleAndExceptionRenders(): void public function testScheduleOptionDTOInvalidMethod(): void { - $this->threwException(InvalidArgumentException::class, 'invalid_schedule_option'); + $this->assertExceptionThrew( + expectedClassName: InvalidArgumentException::class, + expectedMessage: $this->getExceptionFixture('invalid_schedule_option'), + ); new ScheduleOptionDTO('invalid_frequency'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 7c494ff..0d5ac13 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -19,14 +19,10 @@ public function getFixturePath(string $fixtureName): string return __DIR__ . "/fixtures/{$className}/{$fixtureName}"; } - public function threwException(string $class, string $fixtureName): void + public function getExceptionFixture(string $fixtureName): string { - $this->expectException($class); - $fixtureName = (str_contains($fixtureName, '.')) ? $fixtureName : "{$fixtureName}.txt"; - $message = $this->getFixture("exceptions/{$fixtureName}"); - - $this->expectExceptionMessage($message); + return $this->getFixture("exceptions/{$fixtureName}"); } } From 3445a5fb00adccf9562c16f8f6a2b926cd8b4df6 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 30 Jan 2026 14:31:51 +0500 Subject: [PATCH 27/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4381d78..f3a6f10 100644 --- a/README.md +++ b/README.md @@ -70,20 +70,20 @@ render for the passed exception class. #### addScheduleCommand -Adds a scheduled command to the `withSchedule` method. If `withSchedule` does not exist, it will be created first; -the new scheduled command will then be inserted into its closure. +Adds a scheduled command into the `withSchedule` method closure. +If `withSchedule` does not exist, it will be created first; the new scheduled command will then be inserted into its closure. Example usage: ```php new AppBootstrapBuilder() ->addScheduleCommand( 'command', - new ScheduleOptionDTO('environments', ['production']), - new ScheduleOptionDTO('daily'), - new ScheduleOptionDTO( - method: 'timezone', - attributes: ['America/New_York'], - ), + new ScheduleOptionDTO('environments', ['production']), + new ScheduleOptionDTO('daily'), + new ScheduleOptionDTO( + method: 'timezone', + attributes: ['America/New_York'], + ), ) ->save(); ``` \ No newline at end of file From 0f2c548a878d3042a103e5e17291f3b48dd33ef2 Mon Sep 17 00:00:00 2001 From: DenTray Date: Thu, 5 Feb 2026 15:46:39 +0600 Subject: [PATCH 28/46] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f3a6f10..428c47b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Adds a scheduled command into the `withSchedule` method closure. If `withSchedule` does not exist, it will be created first; the new scheduled command will then be inserted into its closure. Example usage: + ```php new AppBootstrapBuilder() ->addScheduleCommand( From 3092f2d8dae431d399df3157acef9f2306deb47a Mon Sep 17 00:00:00 2001 From: DenTray Date: Thu, 5 Feb 2026 15:47:16 +0600 Subject: [PATCH 29/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 428c47b..f26097a 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ render for the passed exception class. #### addScheduleCommand Adds a scheduled command into the `withSchedule` method closure. -If `withSchedule` does not exist, it will be created first; the new scheduled command will then be inserted into its closure. +If `withSchedule` does not exist, it will be automatically created. The new scheduled command will then be inserted into its closure. Example usage: From ecd449fbf8695d24d33a8f0c28735ac4f0184acd Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 10 Feb 2026 10:48:48 +0500 Subject: [PATCH 30/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 4 +-- .../ScheduleOption.php} | 4 +-- .../AddScheduleCommand.php | 25 ++++++++----------- tests/AppBootstrapBuilderTest.php | 16 ++++++------ 4 files changed, 23 insertions(+), 26 deletions(-) rename src/{DTO/ScheduleOptionDTO.php => ValueOptions/ScheduleOption.php} (93%) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 3a3d352..b2e0602 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -2,7 +2,7 @@ namespace RonasIT\Larabuilder\Builders; -use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; +use RonasIT\Larabuilder\ValueOptions\ScheduleOption; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender; use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand; @@ -28,7 +28,7 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } - public function addScheduleCommand(string $command, ScheduleOptionDTO ...$options): self + public function addScheduleCommand(string $command, ScheduleOption ...$options): self { $this->traverser->addVisitor(new AddScheduleCommand($command, ...$options)); diff --git a/src/DTO/ScheduleOptionDTO.php b/src/ValueOptions/ScheduleOption.php similarity index 93% rename from src/DTO/ScheduleOptionDTO.php rename to src/ValueOptions/ScheduleOption.php index a89769a..6cdb5d9 100644 --- a/src/DTO/ScheduleOptionDTO.php +++ b/src/ValueOptions/ScheduleOption.php @@ -1,6 +1,6 @@ options = $options; @@ -58,20 +57,18 @@ protected function buildScheduleCall(): Expression class: new Name('Schedule'), name: new Identifier('command'), args: [ - new Arg(new String_($this->command)), + $this->makeArg($this->command), ], ); - if ($this->options) { - foreach ($this->options as $option) { - $args = array_map(fn ($arg) => $this->makeArg($arg), $option->attributes); + foreach ($this->options as $option) { + $args = array_map(fn ($arg) => $this->makeArg($arg), $option->attributes); - $call = new MethodCall( - var: $call, - name: new Identifier($option->method), - args: $args, - ); - } + $call = new MethodCall( + var: $call, + name: new Identifier($option->method), + args: $args, + ); } return new Expression($call); diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 39b26a8..7275d25 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use RonasIT\Larabuilder\Builders\AppBootstrapBuilder; -use RonasIT\Larabuilder\DTO\ScheduleOptionDTO; +use RonasIT\Larabuilder\ValueOptions\ScheduleOption; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -133,10 +133,10 @@ public function testAddScheduleCommandEmpty(): void new AppBootstrapBuilder() ->addScheduleCommand( 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - new ScheduleOptionDTO('environments', ['production']), - new ScheduleOptionDTO('evenInMaintenanceMode'), - new ScheduleOptionDTO('daily'), - new ScheduleOptionDTO('timezone', ['America/New_York']), + new ScheduleOption('environments', ['production']), + new ScheduleOption('evenInMaintenanceMode'), + new ScheduleOption('daily'), + new ScheduleOption('timezone', ['America/New_York']), ) ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') ->save(); @@ -153,7 +153,7 @@ public function testAddScheduleCommandWithScheduleExists(): void new AppBootstrapBuilder() ->addScheduleCommand( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - options: new ScheduleOptionDTO('environments', ['production']), + options: new ScheduleOption('environments', ['production']), ) ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') ->save(); @@ -170,7 +170,7 @@ public function testCombineScheduleAndExceptionRenders(): void new AppBootstrapBuilder() ->addScheduleCommand( command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', - options: new ScheduleOptionDTO('environments', ['production']), + options: new ScheduleOption('environments', ['production']), ) ->addExceptionsRender( exceptionClass: HttpException::class, @@ -192,6 +192,6 @@ public function testScheduleOptionDTOInvalidMethod(): void expectedMessage: $this->getExceptionFixture('invalid_schedule_option'), ); - new ScheduleOptionDTO('invalid_frequency'); + new ScheduleOption('invalid_frequency'); } } From f3a0605db6ea97f4c64fda2002ac0703198799f5 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 10 Feb 2026 11:12:52 +0500 Subject: [PATCH 31/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- .../AppBootstrapVisitors/AddScheduleCommand.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 5471152..89e0dc8 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -76,8 +76,8 @@ class: new Name('Schedule'), public function leaveNode(Node $node): Node { - if ($node instanceof MethodCall) { - if ($this->shouldInsertParentNode($node)) { + if ($node instanceof MethodCall && $node->name->toString() === 'create') { + if ($this->isParentMethodMissing($node)) { $node = $this->insertParentNode($node); } @@ -89,12 +89,6 @@ public function leaveNode(Node $node): Node return parent::leaveNode($node); } - protected function shouldInsertParentNode(Node $node): bool - { - return ($node->name->toString() === 'create') - && $this->isParentMethodMissing($node); - } - protected function isParentMethodMissing(Node $node): bool { $nodeVar = $node->var; From 9bba11bc6f68b50cd305fe3db8bfbac03edb64e3 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Tue, 10 Feb 2026 11:15:08 +0500 Subject: [PATCH 32/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- tests/AppBootstrapBuilderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 7275d25..2d76ab0 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -6,9 +6,9 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use RonasIT\Larabuilder\Builders\AppBootstrapBuilder; -use RonasIT\Larabuilder\ValueOptions\ScheduleOption; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; +use RonasIT\Larabuilder\ValueOptions\ScheduleOption; use Symfony\Component\HttpKernel\Exception\HttpException; class AppBootstrapBuilderTest extends TestCase From 7aaa5250cf7f3c289c2cbeefc6126e4f739f4678 Mon Sep 17 00:00:00 2001 From: Anton Zabolotnikov Date: Fri, 13 Mar 2026 13:06:49 +0500 Subject: [PATCH 33/46] feat: ability generate withSchedule for boostrap/app.php file refs: https://app.clickup.com/t/86c4eft6r --- src/Builders/AppBootstrapBuilder.php | 2 +- .../fixtures/AppBootstrapBuilderTest/results/combine_render.php | 2 +- tests/fixtures/AppBootstrapBuilderTest/results/schedule.php | 2 +- .../AppBootstrapBuilderTest/results/schedule_exists.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index b2e0602..07d7faa 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -33,7 +33,7 @@ public function addScheduleCommand(string $command, ScheduleOption ...$options): $this->traverser->addVisitor(new AddScheduleCommand($command, ...$options)); $this->addImports([ - 'Illuminate\Console\Scheduling\Schedule', + 'Illuminate\Support\Facades\Schedule', ]); return $this; diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php index bec0e04..3d3e260 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/combine_render.php @@ -3,7 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Support\Facades\Schedule; use Symfony\Component\HttpKernel\Exception\HttpException; return Application::configure(basePath: dirname(__DIR__)) diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php index cf6bd92..a603078 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule.php @@ -3,7 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Support\Facades\Schedule; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php index 313067b..3d19318 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php +++ b/tests/fixtures/AppBootstrapBuilderTest/results/schedule_exists.php @@ -2,7 +2,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Support\Facades\Schedule; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( From a57198d3f64ea0d2bbce2e08f213cae590b82409 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 23 Mar 2026 20:13:21 +0300 Subject: [PATCH 34/46] fix: remarks from reviewer --- src/ValueOptions/ScheduleOption.php | 2 +- .../AbstractAppBootstrapVisitor.php | 72 ++++++++++++++++++- .../AddExceptionsRender.php | 51 +++++-------- .../AddScheduleCommand.php | 70 ++---------------- src/Visitors/BaseNodeVisitorAbstract.php | 2 +- 5 files changed, 96 insertions(+), 101 deletions(-) diff --git a/src/ValueOptions/ScheduleOption.php b/src/ValueOptions/ScheduleOption.php index 6cdb5d9..95f21be 100644 --- a/src/ValueOptions/ScheduleOption.php +++ b/src/ValueOptions/ScheduleOption.php @@ -12,7 +12,7 @@ { public function __construct( public string $method, - public array $attributes = [], + public array $arguments = [], ) { $this->validateMethod($this->method); } diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index bee036f..09d80ae 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -3,11 +3,15 @@ namespace RonasIT\Larabuilder\Visitors\AppBootstrapVisitors; use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Interface_; +use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Trait_; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Visitors\BaseNodeVisitorAbstract; @@ -21,7 +25,7 @@ abstract class AbstractAppBootstrapVisitor extends BaseNodeVisitorAbstract Enum_::class, ]; - abstract protected function insertNode(MethodCall $node): MethodCall; + abstract protected function getInsertableNode(): Expression; public function __construct( protected string $parentMethod, @@ -40,6 +44,16 @@ public function enterNode(Node $node): void public function leaveNode(Node $node): Node { + if ($node instanceof MethodCall && $node->name->toString() === 'create') { + if ($this->isParentMethodMissing($node)) { + $node = $this->insertParentNode($node); + } + + if ($node->var->getAttribute('wasCreated')) { + $node->var = $this->handleParentNode($node->var); + } + } + if (!$node instanceof MethodCall) { return $node; } @@ -51,6 +65,42 @@ public function leaveNode(Node $node): Node return $node; } + protected function isParentMethodMissing(Node $node): bool + { + $nodeVar = $node->var; + + while ($nodeVar instanceof MethodCall) { + if ($nodeVar->name->toString() === $this->parentMethod) { + return false; + } + + $nodeVar = $nodeVar->var; + } + + return true; + } + + protected function insertParentNode(Node $node): Node + { + $closure = new Closure([ + 'returnType' => new Identifier('void'), + ]); + + $parentCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); + $parentCall->setAttribute('wasCreated', true); + + return new MethodCall($parentCall, new Identifier('create')); + } + + protected function handleParentNode(MethodCall $node): Node + { + if ($this->isParentNode($node) && $this->shouldInsertNode($node)) { + return $this->insertNode($node); + } + + return $node; + } + protected function isParentNode(Node $node): bool { return $node instanceof MethodCall && $node->name->toString() === $this->parentMethod; @@ -75,6 +125,26 @@ protected function shouldInsertNode(MethodCall $node): bool return true; } + protected function insertNode(MethodCall $node): MethodCall + { + $currentStatements = $node->args[0]->value->stmts; + $statement = $this->getInsertableNode(); + + if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { + $node->args[0]->value->stmts = [$statement]; + + return $node; + } + + $lastExistingStatement = end($currentStatements); + + $statement->setAttribute('previous', $lastExistingStatement); + + $node->args[0]->value->stmts[] = $statement; + + return $node; + } + protected function matchesCustomCriteria(Expression $statement): bool { return false; diff --git a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php index 409564d..cfa3620 100644 --- a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php +++ b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php @@ -10,7 +10,6 @@ use PhpParser\Node\Name; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Nop; use RonasIT\Larabuilder\Nodes\PreformattedCode; class AddExceptionsRender extends AbstractAppBootstrapVisitor @@ -30,38 +29,6 @@ public function __construct( ); } - protected function matchesCustomCriteria(Expression $stmt): bool - { - $paramType = $stmt->expr->args[0]?->value?->params[0]?->type ?? null; - - if (!($paramType instanceof Name)) { - return false; - } - - $typeName = $paramType->toString(); - - return $typeName === $this->exceptionClass || $typeName === class_basename($this->exceptionClass); - } - - protected function insertNode(MethodCall $node): MethodCall - { - $currentStatements = $node->args[0]->value->stmts; - - if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { - $node->args[0]->value->stmts = [$this->renderStatement]; - - return $node; - } - - $lastExistingStatement = end($currentStatements); - - $this->renderStatement->setAttribute('previous', $lastExistingStatement); - - $node->args[0]->value->stmts[] = $this->renderStatement; - - return $node; - } - protected function buildRenderCall(): Expression { return new Expression( @@ -96,4 +63,22 @@ protected function buildClosure(): Closure 'stmts' => [new PreformattedCode($this->renderBody)], ]); } + + protected function matchesCustomCriteria(Expression $stmt): bool + { + $paramType = $stmt->expr->args[0]?->value?->params[0]?->type ?? null; + + if (!($paramType instanceof Name)) { + return false; + } + + $typeName = $paramType->toString(); + + return $typeName === $this->exceptionClass || $typeName === class_basename($this->exceptionClass); + } + + protected function getInsertableNode(): Expression + { + return $this->renderStatement; + } } diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 89e0dc8..8ea4519 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -2,15 +2,11 @@ namespace RonasIT\Larabuilder\Visitors\AppBootstrapVisitors; -use PhpParser\Node; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Nop; use RonasIT\Larabuilder\ValueOptions\ScheduleOption; class AddScheduleCommand extends AbstractAppBootstrapVisitor @@ -32,87 +28,31 @@ public function __construct( ); } - protected function insertNode(MethodCall $node): MethodCall - { - $currentStatements = $node->args[0]->value->stmts; - - if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { - $node->args[0]->value->stmts = [$this->scheduleStatement]; - - return $node; - } - - $lastExistingStatement = end($currentStatements); - - $this->scheduleStatement->setAttribute('previous', $lastExistingStatement); - - $node->args[0]->value->stmts[] = $this->scheduleStatement; - - return $node; - } - protected function buildScheduleCall(): Expression { $call = new StaticCall( class: new Name('Schedule'), name: new Identifier('command'), args: [ - $this->makeArg($this->command), + $this->makeArgument($this->command), ], ); foreach ($this->options as $option) { - $args = array_map(fn ($arg) => $this->makeArg($arg), $option->attributes); + $arguments = array_map(fn ($argument) => $this->makeArgument($argument), $option->arguments); $call = new MethodCall( var: $call, name: new Identifier($option->method), - args: $args, + args: $arguments, ); } return new Expression($call); } - public function leaveNode(Node $node): Node + protected function getInsertableNode(): Expression { - if ($node instanceof MethodCall && $node->name->toString() === 'create') { - if ($this->isParentMethodMissing($node)) { - $node = $this->insertParentNode($node); - } - - if ($node->var->getAttribute('wasCreated')) { - $node->var = parent::leaveNode($node->var); - } - } - - return parent::leaveNode($node); - } - - protected function isParentMethodMissing(Node $node): bool - { - $nodeVar = $node->var; - - while ($nodeVar instanceof MethodCall) { - if ($nodeVar->name->toString() === $this->parentMethod) { - return false; - } - - $nodeVar = $nodeVar->var; - } - - return true; - } - - protected function insertParentNode(Node $node): Node - { - $closure = new Closure([ - 'returnType' => new Identifier('void'), - ]); - - $scheduleCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); - $scheduleCall->setAttribute('wasCreated', true); - - return new MethodCall($scheduleCall, new Identifier('create')); + return $this->scheduleStatement; } } diff --git a/src/Visitors/BaseNodeVisitorAbstract.php b/src/Visitors/BaseNodeVisitorAbstract.php index adc5009..41a8477 100644 --- a/src/Visitors/BaseNodeVisitorAbstract.php +++ b/src/Visitors/BaseNodeVisitorAbstract.php @@ -87,7 +87,7 @@ protected function setParentForNode(Node $child, Node $parent): void } } - protected function makeArg(mixed $value): Arg + protected function makeArgument(mixed $value): Arg { list($value) = $this->getPropertyValue($value); From 94a51ea2a8b016a7fc824b59613b180caf604332 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 23 Mar 2026 20:54:20 +0300 Subject: [PATCH 35/46] fix: remarks from reviewer --- src/Builders/AppBootstrapBuilder.php | 2 +- .../AbstractAppBootstrapVisitor.php | 2 ++ .../AddExceptionsRender.php | 6 +++++ tests/AppBootstrapBuilderTest.php | 17 ++++++++++++++ .../original/without_exceptions.php | 14 ++++++++++++ .../results/without_exceptions.php | 22 +++++++++++++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/AppBootstrapBuilderTest/original/without_exceptions.php create mode 100644 tests/fixtures/AppBootstrapBuilderTest/results/without_exceptions.php diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index 07d7faa..abf02a6 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -17,7 +17,7 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, { $this->traverser->addVisitor(new AddExceptionsRender($exceptionClass, $renderBody, $includeRequestArg)); - $imports = [$exceptionClass]; + $imports = ['Illuminate\Foundation\Configuration\Exceptions', $exceptionClass]; if ($includeRequestArg) { $imports[] = 'Illuminate\Http\Request'; diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 09d80ae..2e019bc 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -30,6 +30,7 @@ abstract protected function getInsertableNode(): Expression; public function __construct( protected string $parentMethod, protected string $targetMethod, + protected array $closureParams = [], ) { } @@ -83,6 +84,7 @@ protected function isParentMethodMissing(Node $node): bool protected function insertParentNode(Node $node): Node { $closure = new Closure([ + 'params' => $this->closureParams, 'returnType' => new Identifier('void'), ]); diff --git a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php index cfa3620..1488295 100644 --- a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php +++ b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php @@ -26,6 +26,12 @@ public function __construct( parent::__construct( parentMethod: 'withExceptions', targetMethod: 'render', + closureParams: [ + new Param( + var: new Variable('exceptions'), + type: new Name('Exceptions'), + ), + ], ); } diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 2d76ab0..68db047 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -38,6 +38,23 @@ public function testAddExceptionsRenderEmpty(): void ->save(); } + public function testAddExceptionsRenderMissingWithExceptions(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('bootstrap/app.php', 'without_exceptions.php'), + $this->callFilePutContent('bootstrap/app.php', 'without_exceptions.php'), + ); + + new AppBootstrapBuilder() + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: $this->getJsonFixture('render_body'), + includeRequestArg: true, + ) + ->save(); + } + public function testAddExceptionsRenderCustom(): void { $this->mockNativeFunction( diff --git a/tests/fixtures/AppBootstrapBuilderTest/original/without_exceptions.php b/tests/fixtures/AppBootstrapBuilderTest/original/without_exceptions.php new file mode 100644 index 0000000..3e9a147 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/original/without_exceptions.php @@ -0,0 +1,14 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/results/without_exceptions.php b/tests/fixtures/AppBootstrapBuilderTest/results/without_exceptions.php new file mode 100644 index 0000000..5359cf0 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/results/without_exceptions.php @@ -0,0 +1,22 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->render(function (HttpException $exception, Request $request) { + return ($request->expectsJson()) ? response()->json(['error' => $exception->getMessage()], $exception->getStatusCode()) : null; + }); + })->create(); From 78d54df060a31d3744acc50dcb979ab3065bdb23 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 23 Mar 2026 22:05:30 +0300 Subject: [PATCH 36/46] fix: remarks --- .../AbstractAppBootstrapVisitor.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 2e019bc..1655dbd 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -25,6 +25,8 @@ abstract class AbstractAppBootstrapVisitor extends BaseNodeVisitorAbstract Enum_::class, ]; + protected static array $existingParentNodes = []; + abstract protected function getInsertableNode(): Expression; public function __construct( @@ -34,6 +36,13 @@ public function __construct( ) { } + public function afterTraverse(array $nodes): ?array + { + static::$existingParentNodes = []; + + return null; + } + public function enterNode(Node $node): void { $isBootstrapAppFile = array_any(self::FORBIDDEN_NODES, fn ($type) => $node instanceof $type); @@ -41,12 +50,16 @@ public function enterNode(Node $node): void if ($isBootstrapAppFile) { throw new InvalidBootstrapAppFileException(class_basename($node)); } + + if ($node instanceof MethodCall && $node->name->toString() === $this->parentMethod) { + static::$existingParentNodes[] = $this->parentMethod; + } } public function leaveNode(Node $node): Node { if ($node instanceof MethodCall && $node->name->toString() === 'create') { - if ($this->isParentMethodMissing($node)) { + if (!in_array($this->parentMethod, static::$existingParentNodes)) { $node = $this->insertParentNode($node); } @@ -66,23 +79,10 @@ public function leaveNode(Node $node): Node return $node; } - protected function isParentMethodMissing(Node $node): bool - { - $nodeVar = $node->var; - - while ($nodeVar instanceof MethodCall) { - if ($nodeVar->name->toString() === $this->parentMethod) { - return false; - } - - $nodeVar = $nodeVar->var; - } - - return true; - } - protected function insertParentNode(Node $node): Node { + static::$existingParentNodes[] = $this->parentMethod; + $closure = new Closure([ 'params' => $this->closureParams, 'returnType' => new Identifier('void'), From 1427f863acdc3a3c14e334b62ea493738e811911 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 27 Mar 2026 12:02:49 +0300 Subject: [PATCH 37/46] fix: remarks from reviewer --- .../AppBootstrapVisitors/AbstractAppBootstrapVisitor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 1655dbd..82b896c 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -45,9 +45,9 @@ public function afterTraverse(array $nodes): ?array public function enterNode(Node $node): void { - $isBootstrapAppFile = array_any(self::FORBIDDEN_NODES, fn ($type) => $node instanceof $type); + $isNotBootstrapAppFile = array_any(self::FORBIDDEN_NODES, fn ($type) => $node instanceof $type); - if ($isBootstrapAppFile) { + if ($isNotBootstrapAppFile) { throw new InvalidBootstrapAppFileException(class_basename($node)); } From b9002a1c04ddd403f417a3311ad5e73daa731ed5 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Thu, 2 Apr 2026 23:29:23 +0300 Subject: [PATCH 38/46] fix: update invalid_schedule_option fixture with daysOfMonth method --- .../exceptions/invalid_schedule_option.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt index 55c71dd..a0117c0 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt +++ b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt @@ -55,6 +55,7 @@ monthly monthlyOn twiceMonthly lastDayOfMonth +daysOfMonth quarterly quarterlyOn yearly From 235bed2ba8cef47b9091b926d27e3c2d78ec2d8c Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 10 Apr 2026 15:04:23 +0300 Subject: [PATCH 39/46] Merge remote-tracking branch 'origin/master' into generate-withSchedule --- README.md | 6 + src/Builders/PHPFileBuilder.php | 4 +- src/Exceptions/InvalidNodeTypeException.php | 15 -- .../InvalidStructureTypeException.php | 15 ++ src/Traits/VisitorHelperTrait.php | 175 ++++++++++++++++++ src/Visitors/AddImports.php | 2 + src/Visitors/AddTraits.php | 21 +-- .../AbstractAppBootstrapVisitor.php | 7 +- src/Visitors/BaseNodeVisitorAbstract.php | 168 +++-------------- src/Visitors/InsertCodeToMethod.php | 17 +- src/Visitors/InsertNodesAbstractVisitor.php | 7 + .../InsertOrUpdateNodeAbstractVisitor.php | 39 +--- .../AbstractPropertyVisitor.php | 5 + .../PropertyVisitors/AddArrayPropertyItem.php | 2 +- .../{SetPropertyValue.php => SetProperty.php} | 2 +- tests/PHPFileBuilderTest.php | 61 +++++- .../PHPFileBuilderTest/original/enum.php | 8 + 17 files changed, 330 insertions(+), 224 deletions(-) delete mode 100644 src/Exceptions/InvalidNodeTypeException.php create mode 100644 src/Exceptions/InvalidStructureTypeException.php create mode 100644 src/Traits/VisitorHelperTrait.php rename src/Visitors/PropertyVisitors/{SetPropertyValue.php => SetProperty.php} (96%) create mode 100644 tests/fixtures/PHPFileBuilderTest/original/enum.php diff --git a/README.md b/README.md index f26097a..d2c3644 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ Insert the provided code into the specified method body at the desired position Add new imports to the file. This method will add a new import only in case it does not exist yet, preventing duplicate `use` statements. +#### addTraits + +Add new `use TraitName;` statements to a class, trait, or enum. This method automatically adds the corresponding `use` imports at the top of the file and prevents duplicate trait usages. + +**Note:** Need to provide the full trait class name (FQCN); the method will import it automatically. + ## Special Laravel structure builders ### Bootstrap app diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index c21ce1b..0f9128e 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -15,7 +15,7 @@ use RonasIT\Larabuilder\Visitors\InsertCodeToMethod; use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem; -use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetPropertyValue; +use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty; class PHPFileBuilder { @@ -43,7 +43,7 @@ public function __construct( public function setProperty(string $name, mixed $value, AccessModifierEnum $accessModifier = AccessModifierEnum::Public): self { - $this->traverser->addVisitor(new SetPropertyValue($name, $value, $accessModifier)); + $this->traverser->addVisitor(new SetProperty($name, $value, $accessModifier)); return $this; } diff --git a/src/Exceptions/InvalidNodeTypeException.php b/src/Exceptions/InvalidNodeTypeException.php deleted file mode 100644 index 24575ce..0000000 --- a/src/Exceptions/InvalidNodeTypeException.php +++ /dev/null @@ -1,15 +0,0 @@ - $statement) { + foreach (self::TYPE_ORDER as $currentTypeIndex => $type) { + if ($statement instanceof $type && $currentTypeIndex <= $insertTypeOrder) { + $insertIndex = $index + 1; + } + } + } + + return $insertIndex; + } + + protected function shouldAddEmptyLine(array $stmts, int $index, string $type): bool + { + return (isset($stmts[$index])) + && !($stmts[$index] instanceof Nop) + && !($stmts[$index] instanceof $type); + } + + protected function addEmptyLine(array &$nodes, int $index): void + { + array_splice($nodes, $index, 0, [new Nop()]); + } + + protected function prepareNewNode(mixed $parent, mixed $child): mixed + { + $this->setParentForNode($child, $parent); + + return $parent; + } + + protected function setParentForNode(Node $child, Node $parent): void + { + $child->setAttribute(StatementAttributeEnum::Parent->value, $parent); + + if ($child instanceof Array_) { + foreach ($child->items as $item) { + $item->setAttribute(StatementAttributeEnum::Parent->value, $child); + + if ($item->value instanceof Array_) { + $this->setParentForNode($item->value, $item); + } + } + } + } + + protected function isCodeDuplicated(array $existingStatements, array $statementsToCheck): bool + { + if (empty($existingStatements) || empty($statementsToCheck)) { + return false; + } + + $haystack = $this->normalizeStatements($existingStatements); + $needle = $this->normalizeStatements($statementsToCheck); + + return $this->isSubsequence($haystack, $needle); + } + + protected function normalizeStatements(array $statements): array + { + $printer = new Standard(); + + return Arr::map($statements, function (Stmt $statement) use ($printer) { + $stmtCopy = clone $statement; + + $stmtCopy->setAttribute(StatementAttributeEnum::Comments->value, []); + + return $printer->prettyPrint([$stmtCopy]); + }); + } + + protected function isSubsequence(array $haystackStatements, array $needleStatements): bool + { + $needleCount = count($needleStatements); + $haystackCount = count($haystackStatements); + + for ($i = 0; $i <= $haystackCount - $needleCount; $i++) { + if (array_slice($haystackStatements, $i, $needleCount) === $needleStatements) { + return true; + } + } + + return false; + } + + protected function makeArgument(mixed $value): Arg + { + list($value) = $this->getPropertyValue($value); + + return new Arg($value); + } + + protected function getPropertyValue(mixed $value): array + { + $type = get_debug_type($value); + + $value = match ($type) { + 'int' => new Int_($value), + 'array' => $this->makeArrayValue($value), + 'string' => new String_($value), + 'float' => new Float_($value), + 'bool' => $this->makeBoolValue($value), + 'null' => new ConstFetch(new Name('null')), + }; + + return [$value, $type]; + } + + protected function makeBoolValue(bool $value): ConstFetch + { + $name = new Name(($value) ? 'true' : 'false'); + + return new ConstFetch($name); + } + + protected function makeArrayValue(array $values): Array_ + { + $items = []; + + foreach ($values as $key => $val) { + list($val) = $this->getPropertyValue($val); + list($key) = $this->getPropertyValue($key); + + $items[] = new ArrayItem($val, $key); + } + + return new Array_($items); + } +} diff --git a/src/Visitors/AddImports.php b/src/Visitors/AddImports.php index a758e0a..2da2cff 100644 --- a/src/Visitors/AddImports.php +++ b/src/Visitors/AddImports.php @@ -10,6 +10,8 @@ class AddImports extends InsertNodesAbstractVisitor { + protected array $allowedParentNodesTypes = self::ANY_TYPE; + public function __construct(array $imports) { $nodesToInsert = collect($imports) diff --git a/src/Visitors/AddTraits.php b/src/Visitors/AddTraits.php index 05bd787..c743372 100644 --- a/src/Visitors/AddTraits.php +++ b/src/Visitors/AddTraits.php @@ -11,6 +11,12 @@ class AddTraits extends InsertNodesAbstractVisitor { + protected array $allowedParentNodesTypes = [ + Class_::class, + Trait_::class, + Enum_::class, + ]; + public function __construct(array $traits) { $nodesToInsert = collect($traits) @@ -24,21 +30,6 @@ public function __construct(array $traits) ); } - public function leaveNode(Node $node): Node - { - if ($this->isParentNode($node)) { - /** @var Class_|Enum_|Trait_ $node */ - $this->insertNodes($node->stmts); - } - - return $node; - } - - protected function isParentNode(Node $node): bool - { - return $node instanceof Class_ || $node instanceof Trait_ || $node instanceof Enum_; - } - /** @param TraitUse $node */ protected function getChildNodes(Node $node): array { diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 82b896c..ac1f993 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -13,11 +13,14 @@ use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Trait_; +use PhpParser\NodeVisitorAbstract; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; -use RonasIT\Larabuilder\Visitors\BaseNodeVisitorAbstract; +use RonasIT\Larabuilder\Traits\VisitorHelperTrait; -abstract class AbstractAppBootstrapVisitor extends BaseNodeVisitorAbstract +abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { + use VisitorHelperTrait; + protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, diff --git a/src/Visitors/BaseNodeVisitorAbstract.php b/src/Visitors/BaseNodeVisitorAbstract.php index e54d0ed..4e213c1 100644 --- a/src/Visitors/BaseNodeVisitorAbstract.php +++ b/src/Visitors/BaseNodeVisitorAbstract.php @@ -2,175 +2,55 @@ namespace RonasIT\Larabuilder\Visitors; -use Illuminate\Support\Arr; use PhpParser\Node; -use PhpParser\Node\Arg; -use PhpParser\Node\ArrayItem; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Name; -use PhpParser\Node\Scalar\Float_; -use PhpParser\Node\Scalar\Int_; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Stmt; -use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassConst; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Enum_; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\Nop; -use PhpParser\Node\Stmt\Property; -use PhpParser\Node\Stmt\Trait_; -use PhpParser\Node\Stmt\TraitUse; -use PhpParser\Node\Stmt\Use_; use PhpParser\NodeVisitorAbstract; -use PhpParser\PrettyPrinter\Standard; -use RonasIT\Larabuilder\Enums\StatementAttributeEnum; +use RonasIT\Larabuilder\Exceptions\InvalidStructureTypeException; +use RonasIT\Larabuilder\Traits\VisitorHelperTrait; abstract class BaseNodeVisitorAbstract extends NodeVisitorAbstract { - protected const TYPE_ORDER = [ - Namespace_::class, - Use_::class, - Class_::class, - Trait_::class, - Enum_::class, - TraitUse::class, - ClassConst::class, - Property::class, - ClassMethod::class, - ]; + use VisitorHelperTrait; - protected function getInsertIndex(array $statements, string $insertType): int - { - $insertIndex = 0; - $insertTypeOrder = array_search($insertType, self::TYPE_ORDER); - - foreach ($statements as $index => $statement) { - foreach (self::TYPE_ORDER as $currentTypeIndex => $type) { - if ($statement instanceof $type && $currentTypeIndex <= $insertTypeOrder) { - $insertIndex = $index + 1; - } - } - } + protected const array ANY_TYPE = []; - return $insertIndex; + abstract protected array $allowedParentNodesTypes { + get; } - protected function shouldAddEmptyLine(array $stmts, int $index, string $type): bool - { - return (isset($stmts[$index])) - && !($stmts[$index] instanceof Nop) - && !($stmts[$index] instanceof $type); - } + protected bool $hasParentNode = false; - protected function addEmptyLine(array &$nodes, int $index): void - { - array_splice($nodes, $index, 0, [new Nop()]); - } + abstract protected function modify(Node $node): Node; - protected function prepareNewNode(mixed $parent, mixed $child): mixed + public function leaveNode(Node $node): Node { - $this->setParentForNode($child, $parent); + if ($this->isParentNode($node)) { + $this->hasParentNode = true; - return $parent; - } - - protected function setParentForNode(Node $child, Node $parent): void - { - $child->setAttribute(StatementAttributeEnum::Parent->value, $parent); - - if ($child instanceof Array_) { - foreach ($child->items as $item) { - $item->setAttribute(StatementAttributeEnum::Parent->value, $child); - - if ($item->value instanceof Array_) { - $this->setParentForNode($item->value, $item); - } - } + return $this->modify($node); } - } - - protected function isCodeDuplicated(array $existingStatements, array $statementsToCheck): bool - { - if (empty($existingStatements) || empty($statementsToCheck)) { - return false; - } - - $haystack = $this->normalizeStatements($existingStatements); - $needle = $this->normalizeStatements($statementsToCheck); - - return $this->isSubsequence($haystack, $needle); - } - protected function normalizeStatements(array $statements): array - { - $printer = new Standard(); - - return Arr::map($statements, function (Stmt $statement) use ($printer) { - $stmtCopy = clone $statement; - - $stmtCopy->setAttribute(StatementAttributeEnum::Comments->value, []); - - return $printer->prettyPrint([$stmtCopy]); - }); + return $node; } - protected function isSubsequence(array $haystackStatements, array $needleStatements): bool + public function afterTraverse(array $nodes): ?array { - $needleCount = count($needleStatements); - $haystackCount = count($haystackStatements); - - for ($i = 0; $i <= $haystackCount - $needleCount; $i++) { - if (array_slice($haystackStatements, $i, $needleCount) === $needleStatements) { - return true; - } + if (!empty($this->allowedParentNodesTypes) && !$this->hasParentNode) { + throw new InvalidStructureTypeException(class_basename(get_called_class()), $this->getReadableAllowedParentNodesTypes()); } - return false; + return null; } - protected function makeArgument(mixed $value): Arg + protected function getReadableAllowedParentNodesTypes(): array { - list($value) = $this->getPropertyValue($value); - - return new Arg($value); + return array_map( + fn (string $class) => trim(class_basename($class), '_'), + $this->allowedParentNodesTypes, + ); } - protected function getPropertyValue(mixed $value): array + protected function isParentNode(Node $node): bool { - $type = get_debug_type($value); - - $value = match ($type) { - 'int' => new Int_($value), - 'array' => $this->makeArrayValue($value), - 'string' => new String_($value), - 'float' => new Float_($value), - 'bool' => $this->makeBoolValue($value), - 'null' => new ConstFetch(new Name('null')), - }; - - return [$value, $type]; - } - - protected function makeBoolValue(bool $value): ConstFetch - { - $name = new Name(($value) ? 'true' : 'false'); - - return new ConstFetch($name); - } - - protected function makeArrayValue(array $values): Array_ - { - $items = []; - - foreach ($values as $key => $val) { - list($val) = $this->getPropertyValue($val); - list($key) = $this->getPropertyValue($key); - - $items[] = new ArrayItem($val, $key); - } - - return new Array_($items); + return array_any($this->allowedParentNodesTypes, fn ($type) => $node instanceof $type); } } diff --git a/src/Visitors/InsertCodeToMethod.php b/src/Visitors/InsertCodeToMethod.php index 01fa098..ed9b9a6 100644 --- a/src/Visitors/InsertCodeToMethod.php +++ b/src/Visitors/InsertCodeToMethod.php @@ -9,12 +9,17 @@ use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Trait_; use RonasIT\Larabuilder\Enums\InsertPositionEnum; -use RonasIT\Larabuilder\Exceptions\InvalidNodeTypeException; use RonasIT\Larabuilder\Exceptions\NodeNotExistException; use RonasIT\Larabuilder\Nodes\PreformattedCode; class InsertCodeToMethod extends InsertOrUpdateNodeAbstractVisitor { + protected array $allowedParentNodesTypes = [ + Class_::class, + Trait_::class, + Enum_::class, + ]; + protected PreformattedCode $code; protected bool $hasTargetMethod = false; @@ -26,11 +31,6 @@ public function __construct( $this->code = new PreformattedCode($code); } - public function parentNodeNotFoundHook(): void - { - throw new InvalidNodeTypeException('Class', 'Trait', 'Enum'); - } - public function insertNode(Node $node): Node { if (!$this->hasTargetMethod) { @@ -53,11 +53,6 @@ protected function shouldUpdateNode(Node $node): bool && !$this->isCodeDuplicated($node->stmts ?? [], $this->code->code); } - protected function isParentNode(Node $node): bool - { - return $node instanceof Class_ || $node instanceof Trait_ || $node instanceof Enum_; - } - protected function updateNode(Node $node): void { $existingStmts = $node->stmts ?? []; diff --git a/src/Visitors/InsertNodesAbstractVisitor.php b/src/Visitors/InsertNodesAbstractVisitor.php index a749215..8b84b88 100644 --- a/src/Visitors/InsertNodesAbstractVisitor.php +++ b/src/Visitors/InsertNodesAbstractVisitor.php @@ -20,6 +20,13 @@ public function __construct( ) { } + protected function modify(Node $node): Node + { + $this->insertNodes($node->stmts); + + return $node; + } + protected function insertNodes(array &$nodes): void { $newNodes = $this->getNodesToAdd($nodes); diff --git a/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php b/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php index 37f4184..71c6b46 100644 --- a/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php +++ b/src/Visitors/InsertOrUpdateNodeAbstractVisitor.php @@ -9,49 +9,24 @@ abstract class InsertOrUpdateNodeAbstractVisitor extends BaseNodeVisitorAbstract { - public bool $hasParentNode = false; - abstract protected function shouldUpdateNode(Node $node): bool; - /** - * Determine the criteria for selecting the node to work with. - * If `shouldUpdateNode` does not find a matching node, a new node will be inserted under this one. - */ - abstract protected function isParentNode(Node $node): bool; - abstract protected function updateNode(Node $node): void; abstract protected function getInsertableNode(): Node; - public function parentNodeNotFoundHook(): void - { - } - - public function leaveNode(Node $node): Node + protected function modify(Node $node): Node { - if ($this->isParentNode($node)) { - $this->hasParentNode = true; - - /** @var Class_|Trait_ $node */ - foreach ($node->stmts as $stmt) { - if ($this->shouldUpdateNode($stmt)) { - $this->updateNode($stmt); + /** @var Class_|Trait_ $node */ + foreach ($node->stmts as $stmt) { + if ($this->shouldUpdateNode($stmt)) { + $this->updateNode($stmt); - return $node; - } + return $node; } - - return $this->insertNode($node); } - return $node; - } - - public function afterTraverse(array $nodes): void - { - if (!$this->hasParentNode) { - $this->parentNodeNotFoundHook(); - } + return $this->insertNode($node); } /** @param Class_|Trait_ $node */ diff --git a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php index 90ffad2..bf4e7f6 100644 --- a/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php +++ b/src/Visitors/PropertyVisitors/AbstractPropertyVisitor.php @@ -10,6 +10,11 @@ abstract class AbstractPropertyVisitor extends InsertOrUpdateNodeAbstractVisitor { + protected array $allowedParentNodesTypes = [ + Class_::class, + Trait_::class, + ]; + public function __construct( protected string $name, ) { diff --git a/src/Visitors/PropertyVisitors/AddArrayPropertyItem.php b/src/Visitors/PropertyVisitors/AddArrayPropertyItem.php index 56428f7..646266d 100644 --- a/src/Visitors/PropertyVisitors/AddArrayPropertyItem.php +++ b/src/Visitors/PropertyVisitors/AddArrayPropertyItem.php @@ -10,7 +10,7 @@ use PhpParser\Node\Stmt\PropertyProperty; use RonasIT\Larabuilder\Exceptions\UnexpectedPropertyTypeException; -class AddArrayPropertyItem extends SetPropertyValue +class AddArrayPropertyItem extends SetProperty { protected ArrayItem $arrayItem; diff --git a/src/Visitors/PropertyVisitors/SetPropertyValue.php b/src/Visitors/PropertyVisitors/SetProperty.php similarity index 96% rename from src/Visitors/PropertyVisitors/SetPropertyValue.php rename to src/Visitors/PropertyVisitors/SetProperty.php index 4160669..046f02f 100644 --- a/src/Visitors/PropertyVisitors/SetPropertyValue.php +++ b/src/Visitors/PropertyVisitors/SetProperty.php @@ -8,7 +8,7 @@ use PhpParser\Node\Stmt\Property; use RonasIT\Larabuilder\Enums\AccessModifierEnum; -class SetPropertyValue extends AbstractPropertyVisitor +class SetProperty extends AbstractPropertyVisitor { protected PropertyItem $propertyItem; protected Identifier $typeIdentifier; diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index d298a65..df42873 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -8,6 +8,7 @@ use RonasIT\Larabuilder\Enums\AccessModifierEnum; use RonasIT\Larabuilder\Enums\InsertPositionEnum; use RonasIT\Larabuilder\Exceptions\InvalidPHPFileException; +use RonasIT\Larabuilder\Exceptions\InvalidStructureTypeException; use RonasIT\Larabuilder\Exceptions\NodeNotExistException; use RonasIT\Larabuilder\Exceptions\UnexpectedPropertyTypeException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; @@ -54,6 +55,20 @@ public function testSetPropertyWithoutExistingProperties(): void ->save(); } + public function testSetPropertyNotClassTrait(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'enum.php'), + ); + + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'SetProperty' operation may only be applied to: Class, Trait."); + + new PHPFileBuilder('some_file_path.php') + ->setProperty('newString', 'some string') + ->save(); + } + public function testAddArrayPropertyItem(): void { $this->mockNativeFunction( @@ -88,6 +103,20 @@ public function testAddArrayPropertyItemThrowsException(): void ->save(); } + public function testAddArrayPropertyItemNotClassTrait(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'enum.php'), + ); + + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'AddArrayPropertyItem' operation may only be applied to: Class, Trait."); + + new PHPFileBuilder('some_file_path.php') + ->addArrayPropertyItem('fillable', 'age') + ->save(); + } + public function testInvalidPhpFileThrowsException(): void { $this->mockNativeFunction( @@ -178,6 +207,20 @@ public function testRemoveArrayPropertyUnexpectedPropertyExceptionNull(): void ->save(); } + public function testRemoveArrayPropertyNotClassTrait(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'enum.php'), + ); + + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'RemoveArrayPropertyItem' operation may only be applied to: Class, Trait."); + + new PHPFileBuilder('some_file_path.php') + ->removeArrayPropertyItem('fillable', ['name', 'age']) + ->save(); + } + public static function provideAddImportsFiles(): array { return [ @@ -348,6 +391,22 @@ public function testAddTraitsWithMultipleTraitUse(): void ->save(); } + public function testAddTraitsNotClassTraitEnum(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'add_imports_to_interface.php'), + ); + + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'AddTraits' operation may only be applied to: Class, Trait, Enum."); + + new PHPFileBuilder('some_file_path.php') + ->addTraits([ + 'RonasIT\Support\Traits\FirstTrait', + ]) + ->save(); + } + public function testInsertCodeToMethodToTheEndPosition(): void { $this->mockNativeFunction( @@ -440,7 +499,7 @@ public function testInsertCodeToMethodNotClassTraitEnum(): void $this->callFileGetContent('some_file_path.php', 'add_imports_to_interface.php'), ); - $this->assertExceptionThrew(Exception::class, 'Only nodes with the next types can be modified: Class, Trait, Enum'); + $this->assertExceptionThrew(InvalidStructureTypeException::class, "'InsertCodeToMethod' operation may only be applied to: Class, Trait, Enum."); new PHPFileBuilder('some_file_path.php') ->insertCodeToMethod('someMethod', '$this->name = $name;') diff --git a/tests/fixtures/PHPFileBuilderTest/original/enum.php b/tests/fixtures/PHPFileBuilderTest/original/enum.php new file mode 100644 index 0000000..644af69 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/original/enum.php @@ -0,0 +1,8 @@ + Date: Tue, 14 Apr 2026 21:26:11 +0300 Subject: [PATCH 40/46] fix: testScheduleOptionDTOInvalidMethod --- tests/AppBootstrapBuilderTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 52858a4..94f1efb 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -230,6 +230,7 @@ public function testScheduleOptionDTOInvalidMethod(): void $this->assertExceptionThrew( expectedClassName: InvalidArgumentException::class, expectedMessage: $this->getExceptionFixture('invalid_schedule_option'), + isStrict: false, ); new ScheduleOption('invalid_frequency'); From 7647f881b7e0ab4485154d88f3aac2e982b75f70 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Tue, 14 Apr 2026 21:34:29 +0300 Subject: [PATCH 41/46] fix: testScheduleOptionDTOInvalidMethod --- .../exceptions/invalid_schedule_option.txt | 64 +------------------ 1 file changed, 1 insertion(+), 63 deletions(-) diff --git a/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt index a0117c0..85e8ef3 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt +++ b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt @@ -1,64 +1,2 @@ Unknown schedule method `invalid_frequency`. -Allowed methods: -user -environments -evenInMaintenanceMode -withoutOverlapping -onOneServer -runInBackground -when -skip -name -description -cron -between -unlessBetween -everySecond -everyTwoSeconds -everyFiveSeconds -everyTenSeconds -everyFifteenSeconds -everyTwentySeconds -everyThirtySeconds -everyMinute -everyTwoMinutes -everyThreeMinutes -everyFourMinutes -everyFiveMinutes -everyTenMinutes -everyFifteenMinutes -everyThirtyMinutes -hourly -hourlyAt -everyOddHour -everyTwoHours -everyThreeHours -everyFourHours -everySixHours -daily -at -dailyAt -twiceDaily -twiceDailyAt -weekdays -weekends -mondays -tuesdays -wednesdays -thursdays -fridays -saturdays -sundays -weekly -weeklyOn -monthly -monthlyOn -twiceMonthly -lastDayOfMonth -daysOfMonth -quarterly -quarterlyOn -yearly -yearlyOn -days -timezone \ No newline at end of file +Allowed methods: \ No newline at end of file From 90dfe6380c099d517d2246a7f51691f382b4f213 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 15:29:49 +0300 Subject: [PATCH 42/46] fix: remarks --- tests/AppBootstrapBuilderTest.php | 5 +++++ .../fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 94f1efb..ee4543e 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -177,6 +177,11 @@ public function testAddScheduleCommandBootstrapEmpty(): void new ScheduleOption('timezone', ['America/New_York']), ) ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') + ->addScheduleCommand( + 'emails:send', + new ScheduleOption('daily'), + new ScheduleOption('when', ['fn () => User::count() > 0']), + ) ->save(); } diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php index c555b83..c27076d 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php @@ -8,4 +8,6 @@ Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production')->evenInMaintenanceMode()->daily()->timezone('America/New_York'); Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); + + Schedule::command('emails:send')->daily()->when('fn () => User::count() > 0'); })->create(); From 96d468aba3bd58d5c155b447f32da9b0bda4227f Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 15:42:51 +0300 Subject: [PATCH 43/46] feat: extend ScheduleOption validation to include Event scheduling methods --- src/ValueOptions/ScheduleOption.php | 2 ++ tests/AppBootstrapBuilderTest.php | 6 ++++++ .../fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/ValueOptions/ScheduleOption.php b/src/ValueOptions/ScheduleOption.php index 95f21be..0b00e60 100644 --- a/src/ValueOptions/ScheduleOption.php +++ b/src/ValueOptions/ScheduleOption.php @@ -2,6 +2,7 @@ namespace RonasIT\Larabuilder\ValueOptions; +use Illuminate\Console\Scheduling\Event; use Illuminate\Console\Scheduling\ManagesAttributes; use Illuminate\Console\Scheduling\ManagesFrequencies; use InvalidArgumentException; @@ -22,6 +23,7 @@ private function validateMethod(string $method): void $methods = array_merge( $this->getMethods(ManagesAttributes::class), $this->getMethods(ManagesFrequencies::class), + $this->getMethods(Event::class), ); if (!in_array($method, $methods, true)) { diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index ee4543e..bb37395 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -182,6 +182,12 @@ public function testAddScheduleCommandBootstrapEmpty(): void new ScheduleOption('daily'), new ScheduleOption('when', ['fn () => User::count() > 0']), ) + ->addScheduleCommand( + 'reports:generate', + new ScheduleOption('daily'), + new ScheduleOption('sendOutputTo', ['/var/log/reports.log']), + new ScheduleOption('pingOnFailure', ['https://example.com/ping']), + ) ->save(); } diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php index c27076d..7495cdd 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php @@ -10,4 +10,6 @@ Schedule::command('telescope:prune --set-hours=resolved_exception:12222'); Schedule::command('emails:send')->daily()->when('fn () => User::count() > 0'); + + Schedule::command('reports:generate')->daily()->sendOutputTo('/var/log/reports.log')->pingOnFailure('https://example.com/ping'); })->create(); From 821815bb17376ccaf8eb4833f2fc324fa130ec0f Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 15:59:35 +0300 Subject: [PATCH 44/46] docs: note automatic Schedule import in addScheduleCommand --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2c3644..034b23b 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,9 @@ render for the passed exception class. #### addScheduleCommand -Adds a scheduled command into the `withSchedule` method closure. +Adds a scheduled command into the `withSchedule` method closure. If `withSchedule` does not exist, it will be automatically created. The new scheduled command will then be inserted into its closure. +Automatically adds `use Illuminate\Support\Facades\Schedule;` to the file imports. Example usage: From 9ada00c972726f0bcbf87b36cccec4daf953aba2 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Thu, 7 May 2026 20:06:14 +0300 Subject: [PATCH 45/46] fix: remarks --- src/Traits/VisitorHelperTrait.php | 175 ------------------ .../AbstractAppBootstrapVisitor.php | 3 - .../AddScheduleCommand.php | 6 +- 3 files changed, 4 insertions(+), 180 deletions(-) delete mode 100644 src/Traits/VisitorHelperTrait.php diff --git a/src/Traits/VisitorHelperTrait.php b/src/Traits/VisitorHelperTrait.php deleted file mode 100644 index 42ea3ec..0000000 --- a/src/Traits/VisitorHelperTrait.php +++ /dev/null @@ -1,175 +0,0 @@ - $statement) { - foreach (self::TYPE_ORDER as $currentTypeIndex => $type) { - if ($statement instanceof $type && $currentTypeIndex <= $insertTypeOrder) { - $insertIndex = $index + 1; - } - } - } - - return $insertIndex; - } - - protected function shouldAddEmptyLine(array $stmts, int $index, string $type): bool - { - return (isset($stmts[$index])) - && !($stmts[$index] instanceof Nop) - && !($stmts[$index] instanceof $type); - } - - protected function addEmptyLine(array &$nodes, int $index): void - { - array_splice($nodes, $index, 0, [new Nop()]); - } - - protected function prepareNewNode(mixed $parent, mixed $child): mixed - { - $this->setParentForNode($child, $parent); - - return $parent; - } - - protected function setParentForNode(Node $child, Node $parent): void - { - $child->setAttribute(StatementAttributeEnum::Parent->value, $parent); - - if ($child instanceof Array_) { - foreach ($child->items as $item) { - $item->setAttribute(StatementAttributeEnum::Parent->value, $child); - - if ($item->value instanceof Array_) { - $this->setParentForNode($item->value, $item); - } - } - } - } - - protected function isCodeDuplicated(array $existingStatements, array $statementsToCheck): bool - { - if (empty($existingStatements) || empty($statementsToCheck)) { - return false; - } - - $haystack = $this->normalizeStatements($existingStatements); - $needle = $this->normalizeStatements($statementsToCheck); - - return $this->isSubsequence($haystack, $needle); - } - - protected function normalizeStatements(array $statements): array - { - $printer = new Standard(); - - return Arr::map($statements, function (Stmt $statement) use ($printer) { - $stmtCopy = clone $statement; - - $stmtCopy->setAttribute(StatementAttributeEnum::Comments->value, []); - - return $printer->prettyPrint([$stmtCopy]); - }); - } - - protected function isSubsequence(array $haystackStatements, array $needleStatements): bool - { - $needleCount = count($needleStatements); - $haystackCount = count($haystackStatements); - - for ($i = 0; $i <= $haystackCount - $needleCount; $i++) { - if (array_slice($haystackStatements, $i, $needleCount) === $needleStatements) { - return true; - } - } - - return false; - } - - protected function makeArgument(mixed $value): Arg - { - list($value) = $this->getPropertyValue($value); - - return new Arg($value); - } - - protected function getPropertyValue(mixed $value): array - { - $type = get_debug_type($value); - - $value = match ($type) { - 'int' => new Int_($value), - 'array' => $this->makeArrayValue($value), - 'string' => new String_($value), - 'float' => new Float_($value), - 'bool' => $this->makeBoolValue($value), - 'null' => new ConstFetch(new Name('null')), - }; - - return [$value, $type]; - } - - protected function makeBoolValue(bool $value): ConstFetch - { - $name = new Name(($value) ? 'true' : 'false'); - - return new ConstFetch($name); - } - - protected function makeArrayValue(array $values): Array_ - { - $items = []; - - foreach ($values as $key => $val) { - list($val) = $this->getPropertyValue($val); - list($key) = $this->getPropertyValue($key); - - $items[] = new ArrayItem($val, $key); - } - - return new Array_($items); - } -} diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index ac1f993..265de8d 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -15,12 +15,9 @@ use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeVisitorAbstract; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; -use RonasIT\Larabuilder\Traits\VisitorHelperTrait; abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { - use VisitorHelperTrait; - protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 8ea4519..6e2842a 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -2,11 +2,13 @@ namespace RonasIT\Larabuilder\Visitors\AppBootstrapVisitors; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Expression; +use RonasIT\Larabuilder\Support\NodeValueFactory; use RonasIT\Larabuilder\ValueOptions\ScheduleOption; class AddScheduleCommand extends AbstractAppBootstrapVisitor @@ -34,12 +36,12 @@ protected function buildScheduleCall(): Expression class: new Name('Schedule'), name: new Identifier('command'), args: [ - $this->makeArgument($this->command), + new Arg(NodeValueFactory::make($this->command)->node), ], ); foreach ($this->options as $option) { - $arguments = array_map(fn ($argument) => $this->makeArgument($argument), $option->arguments); + $arguments = array_map(fn ($argument) => new Arg(NodeValueFactory::make($argument)->node), $option->arguments); $call = new MethodCall( var: $call, From ab48bc16f2ed46c526bb435c46871329b2a21e41 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 5 Jun 2026 20:57:08 +0300 Subject: [PATCH 46/46] fix: remarks from reviewer --- src/Enums/StatementAttributeEnum.php | 1 + src/Printer.php | 2 +- .../AbstractAppBootstrapVisitor.php | 31 +++++++++++-------- .../AddExceptionsRender.php | 29 ++--------------- .../AddScheduleCommand.php | 13 +++----- 5 files changed, 27 insertions(+), 49 deletions(-) diff --git a/src/Enums/StatementAttributeEnum.php b/src/Enums/StatementAttributeEnum.php index 0e4301f..74404b0 100644 --- a/src/Enums/StatementAttributeEnum.php +++ b/src/Enums/StatementAttributeEnum.php @@ -7,4 +7,5 @@ enum StatementAttributeEnum: string case Parent = 'parent'; case Previous = 'previous'; case Comments = 'comments'; + case WasCreated = 'wasCreated'; } diff --git a/src/Printer.php b/src/Printer.php index 0110bc2..9c513a0 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -126,7 +126,7 @@ protected function preparePreformattedCode(string $value): string protected function pExpr_MethodCall(MethodCall $node): string { - if ($node->getAttribute('wasCreated')) { + if ($node->getAttribute(StatementAttributeEnum::WasCreated->value)) { $this->indent(); $newCall = $this->nl . '->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')'; diff --git a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 265de8d..ddf638a 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -14,10 +14,13 @@ use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeVisitorAbstract; +use RonasIT\Larabuilder\Enums\StatementAttributeEnum; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract { + protected const string LAST_BOOTSTRAP_METHOD_NAME = 'create'; + protected const array FORBIDDEN_NODES = [ Class_::class, Trait_::class, @@ -25,9 +28,8 @@ abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract Enum_::class, ]; - protected static array $existingParentNodes = []; - - abstract protected function getInsertableNode(): Expression; + // Store the list of existed key bootstrap methods like withExceptions, withSchedule, etc. + protected static array $existingKeyMethods = []; public function __construct( protected string $parentMethod, @@ -36,9 +38,12 @@ public function __construct( ) { } + abstract protected function getInsertableNode(): Expression; + + // Clear key nodes list after all visitors complete traverse, to keep unique state for each file public function afterTraverse(array $nodes): ?array { - static::$existingParentNodes = []; + static::$existingKeyMethods = []; return null; } @@ -52,18 +57,18 @@ public function enterNode(Node $node): void } if ($node instanceof MethodCall && $node->name->toString() === $this->parentMethod) { - static::$existingParentNodes[] = $this->parentMethod; + static::$existingKeyMethods[] = $this->parentMethod; } } public function leaveNode(Node $node): Node { - if ($node instanceof MethodCall && $node->name->toString() === 'create') { - if (!in_array($this->parentMethod, static::$existingParentNodes)) { + if ($node instanceof MethodCall && $node->name->toString() === self::LAST_BOOTSTRAP_METHOD_NAME) { + if (!in_array($this->parentMethod, static::$existingKeyMethods)) { $node = $this->insertParentNode($node); } - if ($node->var->getAttribute('wasCreated')) { + if ($node->var->getAttribute(StatementAttributeEnum::WasCreated->value)) { $node->var = $this->handleParentNode($node->var); } } @@ -81,7 +86,7 @@ public function leaveNode(Node $node): Node protected function insertParentNode(Node $node): Node { - static::$existingParentNodes[] = $this->parentMethod; + static::$existingKeyMethods[] = $this->parentMethod; $closure = new Closure([ 'params' => $this->closureParams, @@ -89,9 +94,9 @@ protected function insertParentNode(Node $node): Node ]); $parentCall = new MethodCall($node->var, new Identifier($this->parentMethod), [new Arg($closure)]); - $parentCall->setAttribute('wasCreated', true); + $parentCall->setAttribute(StatementAttributeEnum::WasCreated->value, true); - return new MethodCall($parentCall, new Identifier('create')); + return new MethodCall($parentCall, new Identifier(self::LAST_BOOTSTRAP_METHOD_NAME)); } protected function handleParentNode(MethodCall $node): Node @@ -132,7 +137,7 @@ protected function insertNode(MethodCall $node): MethodCall $currentStatements = $node->args[0]->value->stmts; $statement = $this->getInsertableNode(); - if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { + if (count($currentStatements) === 1 && head($currentStatements) instanceof Nop) { $node->args[0]->value->stmts = [$statement]; return $node; @@ -140,7 +145,7 @@ protected function insertNode(MethodCall $node): MethodCall $lastExistingStatement = end($currentStatements); - $statement->setAttribute('previous', $lastExistingStatement); + $statement->setAttribute(StatementAttributeEnum::Previous->value, $lastExistingStatement); $node->args[0]->value->stmts[] = $statement; diff --git a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php index 5022dc8..4ef57ef 100644 --- a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php +++ b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php @@ -10,21 +10,15 @@ use PhpParser\Node\Name; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Nop; -use RonasIT\Larabuilder\Enums\StatementAttributeEnum; use RonasIT\Larabuilder\Nodes\PreformattedCode; class AddExceptionsRender extends AbstractAppBootstrapVisitor { - protected Expression $renderStatement; - public function __construct( protected string $exceptionClass, protected string $renderBody, protected bool $includeRequestArg, ) { - $this->renderStatement = $this->buildRenderCall(); - parent::__construct( parentMethod: 'withExceptions', targetMethod: 'render', @@ -37,23 +31,9 @@ public function __construct( ); } - protected function insertNode(MethodCall $node): MethodCall + protected function getInsertableNode(): Expression { - $currentStatements = $node->args[0]->value->stmts; - - if (count($currentStatements) === 1 && $currentStatements[0] instanceof Nop) { - $node->args[0]->value->stmts = [$this->renderStatement]; - - return $node; - } - - $lastExistingStatement = end($currentStatements); - - $this->renderStatement->setAttribute(StatementAttributeEnum::Previous->value, $lastExistingStatement); - - $node->args[0]->value->stmts[] = $this->renderStatement; - - return $node; + return $this->buildRenderCall(); } protected function buildRenderCall(): Expression @@ -103,9 +83,4 @@ protected function matchesCustomCriteria(Expression $stmt): bool return $typeName === $this->exceptionClass || $typeName === class_basename($this->exceptionClass); } - - protected function getInsertableNode(): Expression - { - return $this->renderStatement; - } } diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php index 6e2842a..dfaf9dd 100644 --- a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -13,7 +13,6 @@ class AddScheduleCommand extends AbstractAppBootstrapVisitor { - protected Expression $scheduleStatement; protected array $options = []; public function __construct( @@ -22,14 +21,17 @@ public function __construct( ) { $this->options = $options; - $this->scheduleStatement = $this->buildScheduleCall(); - parent::__construct( parentMethod: 'withSchedule', targetMethod: 'command', ); } + protected function getInsertableNode(): Expression + { + return $this->buildScheduleCall(); + } + protected function buildScheduleCall(): Expression { $call = new StaticCall( @@ -52,9 +54,4 @@ class: new Name('Schedule'), return new Expression($call); } - - protected function getInsertableNode(): Expression - { - return $this->scheduleStatement; - } }