Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1a32fc3
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 22, 2026
1ec1f91
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 22, 2026
8071f7f
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 22, 2026
05bac0d
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 22, 2026
b3a142e
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
08bb80f
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
a47a861
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
d3835da
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
58ade35
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
b307beb
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
bfd5cb5
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
c513952
Merge branch 'master' into generate-withSchedule
AZabolotnikov Jan 26, 2026
57580d9
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
7c35d34
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
e12f780
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
b08bfbe
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
1bb478b
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 26, 2026
b2f486c
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 27, 2026
d6c0d71
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 27, 2026
0c71342
Merge branch 'master' into generate-withSchedule
AZabolotnikov Jan 27, 2026
0cebe13
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 27, 2026
d5b78c8
Merge remote-tracking branch 'origin/generate-withSchedule' into gene…
AZabolotnikov Jan 27, 2026
6da6f6d
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 27, 2026
37a62d3
Merge branch 'master' into generate-withSchedule
AZabolotnikov Jan 28, 2026
58d7380
Merge branch 'master' into generate-withSchedule
AZabolotnikov Jan 30, 2026
50acbe6
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
f0d48df
Merge remote-tracking branch 'origin/generate-withSchedule' into gene…
AZabolotnikov Jan 30, 2026
769d14e
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
7a57ad2
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
13a8f09
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
5e41979
Update tests/AppBootstrapBuilderTest.php
AZabolotnikov Jan 30, 2026
dd730fd
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
fee25ee
Merge remote-tracking branch 'origin/generate-withSchedule' into gene…
AZabolotnikov Jan 30, 2026
3445a5f
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Jan 30, 2026
0f2c548
Update README.md
DenTray Feb 5, 2026
3092f2d
Update README.md
DenTray Feb 5, 2026
ecd449f
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Feb 10, 2026
f3a0605
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Feb 10, 2026
9bba11b
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Feb 10, 2026
7aaa525
feat: ability generate withSchedule for boostrap/app.php file
AZabolotnikov Mar 13, 2026
dd1402c
Merge branch 'master' into generate-withSchedule
DenTray Mar 23, 2026
a57198d
fix: remarks from reviewer
artengin Mar 23, 2026
94a51ea
fix: remarks from reviewer
artengin Mar 23, 2026
78d54df
fix: remarks
artengin Mar 23, 2026
751d0a8
Merge remote-tracking branch 'origin/master' into generate-withSchedule
artengin Mar 24, 2026
1427f86
fix: remarks from reviewer
artengin Mar 27, 2026
d550bee
Merge remote-tracking branch 'origin/master' into generate-withSchedule
artengin Mar 31, 2026
b7a9305
Merge branch 'master' into generate-withSchedule
artengin Apr 2, 2026
b9002a1
fix: update invalid_schedule_option fixture with daysOfMonth method
artengin Apr 2, 2026
235bed2
Merge remote-tracking branch 'origin/master' into generate-withSchedule
artengin Apr 10, 2026
7be3a6b
Merge remote-tracking branch 'origin/master' into generate-withSchedule
artengin Apr 10, 2026
0de859c
Merge branch 'master' into generate-withSchedule
artengin Apr 13, 2026
17ad6a7
Merge branch 'master' into generate-withSchedule
artengin Apr 14, 2026
48fbb27
Merge branch 'master' into generate-withSchedule
artengin Apr 14, 2026
4c3c2fe
fix: testScheduleOptionDTOInvalidMethod
artengin Apr 14, 2026
7647f88
fix: testScheduleOptionDTOInvalidMethod
artengin Apr 14, 2026
90dfe63
fix: remarks
artengin Apr 20, 2026
96d468a
feat: extend ScheduleOption validation to include Event scheduling me…
artengin Apr 20, 2026
821815b
docs: note automatic Schedule import in addScheduleCommand
artengin Apr 20, 2026
e5d10db
Merge remote-tracking branch 'origin/master' into generate-withSchedule
artengin May 7, 2026
9ada00c
fix: remarks
artengin May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ 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 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:
Comment thread
DenTray marked this conversation as resolved.

```php
new AppBootstrapBuilder()
->addScheduleCommand(
'command',
new ScheduleOptionDTO('environments', ['production']),
new ScheduleOptionDTO('daily'),
new ScheduleOptionDTO(
method: 'timezone',
attributes: ['America/New_York'],
),
)
->save();
```
## Contributing

Thank you for considering contributing to Laravel Builder package! The contribution guide
Expand Down
15 changes: 14 additions & 1 deletion src/Builders/AppBootstrapBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace RonasIT\Larabuilder\Builders;

use RonasIT\Larabuilder\ValueOptions\ScheduleOption;
use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddExceptionsRender;
use RonasIT\Larabuilder\Visitors\AppBootstrapVisitors\AddScheduleCommand;

class AppBootstrapBuilder extends PHPFileBuilder
{
Expand All @@ -15,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';
Expand All @@ -25,4 +27,15 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody,

return $this;
}

Comment thread
AZabolotnikov marked this conversation as resolved.
public function addScheduleCommand(string $command, ScheduleOption ...$options): self
{
$this->traverser->addVisitor(new AddScheduleCommand($command, ...$options));

$this->addImports([
'Illuminate\Support\Facades\Schedule',
]);
Comment on lines +35 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid importing facade Schedule unconditionally

addScheduleCommand() always adds Illuminate\Support\Facades\Schedule, but withSchedule() is commonly written with use Illuminate\Console\Scheduling\Schedule; for the callback parameter. Adding both imports creates a duplicate Schedule alias and the generated bootstrap/app.php will fail to parse (name is already in use) before the app can boot. This breaks valid existing bootstrap files that already follow Laravel's typed callback style.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The library provides removeImports() for exactly this case. If your bootstrap/app.php already has use Illuminate\Console\Scheduling\Schedule, chain removeImports() before addScheduleCommand() to drop the conflicting import first.

Comment on lines +35 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip facade import when Schedule alias is already used

Adding Illuminate\Support\Facades\Schedule unconditionally can make generated bootstrap/app.php fail to parse when the file already imports Illuminate\Console\Scheduling\Schedule for the withSchedule callback parameter, because both resolve to the Schedule alias. This happens in valid Laravel bootstrap files and stops app boot with a duplicate import name error. Fresh evidence: in this commit tree there is no removeImports() API (rg "removeImports" returns no matches), so the suggested thread workaround is not available here.

Useful? React with 👍 / 👎.


return $this;
}
}
16 changes: 16 additions & 0 deletions src/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\PropertyItem;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
Expand Down Expand Up @@ -122,4 +123,19 @@ protected function preparePreformattedCode(string $value): string

return rtrim($value);
}

protected function pExpr_MethodCall(MethodCall $node): string
{
if ($node->getAttribute('wasCreated')) {
$this->indent();

$newCall = $this->nl . '->' . $this->pObjectProperty($node->name) . '(' . $this->pMaybeMultiline($node->args) . ')';

$this->outdent();

return $this->pDereferenceLhs($node->var) . $newCall;
}

return parent::pExpr_MethodCall($node);
}
}
42 changes: 42 additions & 0 deletions src/ValueOptions/ScheduleOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace RonasIT\Larabuilder\ValueOptions;

use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\ManagesAttributes;
use Illuminate\Console\Scheduling\ManagesFrequencies;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;

readonly class ScheduleOption
{
public function __construct(
public string $method,
public array $arguments = [],
) {
$this->validateMethod($this->method);
}

private function validateMethod(string $method): void
{
$methods = array_merge(
$this->getMethods(ManagesAttributes::class),
$this->getMethods(ManagesFrequencies::class),
$this->getMethods(Event::class),
);
Comment on lines +23 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict schedule option methods to fluent mutators

ScheduleOption::validateMethod() currently whitelists every public method on Event, which includes non-fluent accessors like getSummaryForDisplay() / nextRunDate(). AddScheduleCommand then blindly chains all selected options, so a valid option sequence can generate calls like Schedule::command(...)->getSummaryForDisplay()->daily(), which fails at runtime because the accessor no longer returns an event object. Limiting allowed methods to fluent scheduling configurators avoids generating invalid chains that crash when the scheduler is loaded.

Useful? React with 👍 / 👎.


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);
}
}
80 changes: 76 additions & 4 deletions src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,71 @@
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 PhpParser\NodeVisitorAbstract;
use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException;

abstract class AbstractAppBootstrapVisitor extends NodeVisitorAbstract
{
protected const FORBIDDEN_NODES = [
protected const array FORBIDDEN_NODES = [
Class_::class,
Trait_::class,
Interface_::class,
Enum_::class,
];

abstract protected function insertNode(MethodCall $node): MethodCall;
protected static array $existingParentNodes = [];

abstract protected function getInsertableNode(): Expression;

public function __construct(
protected string $parentMethod,
protected string $targetMethod,
protected array $closureParams = [],
) {
}

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);
$isNotBootstrapAppFile = array_any(self::FORBIDDEN_NODES, fn ($type) => $node instanceof $type);

if ($isBootstrapAppFile) {
if ($isNotBootstrapAppFile) {
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 (!in_array($this->parentMethod, static::$existingParentNodes)) {
$node = $this->insertParentNode($node);
Comment thread
artengin marked this conversation as resolved.
}

if ($node->var->getAttribute('wasCreated')) {
$node->var = $this->handleParentNode($node->var);
}
}

if (!$node instanceof MethodCall) {
return $node;
}
Expand All @@ -51,6 +79,30 @@ public function leaveNode(Node $node): Node
return $node;
}

protected function insertParentNode(Node $node): Node
{
static::$existingParentNodes[] = $this->parentMethod;

$closure = new Closure([
'params' => $this->closureParams,
'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;
Expand All @@ -75,6 +127,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;
Expand Down
37 changes: 24 additions & 13 deletions src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,15 @@ public function __construct(
parent::__construct(
parentMethod: 'withExceptions',
targetMethod: 'render',
closureParams: [
new Param(
var: new Variable('exceptions'),
type: new Name('Exceptions'),
),
],
);
}

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;
Expand Down Expand Up @@ -97,4 +90,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;
}
}
60 changes: 60 additions & 0 deletions src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

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;

Comment thread
AZabolotnikov marked this conversation as resolved.
class AddScheduleCommand extends AbstractAppBootstrapVisitor
{
protected Expression $scheduleStatement;
protected array $options = [];

public function __construct(
protected string $command,
ScheduleOption ...$options,
) {
$this->options = $options;

$this->scheduleStatement = $this->buildScheduleCall();

parent::__construct(
parentMethod: 'withSchedule',
targetMethod: 'command',
);
}

protected function buildScheduleCall(): Expression
{
$call = new StaticCall(
class: new Name('Schedule'),
name: new Identifier('command'),
args: [
new Arg(NodeValueFactory::make($this->command)->node),
],
);

foreach ($this->options as $option) {
$arguments = array_map(fn ($argument) => new Arg(NodeValueFactory::make($argument)->node), $option->arguments);

$call = new MethodCall(
var: $call,
name: new Identifier($option->method),
args: $arguments,
);
}

return new Expression($call);
}

protected function getInsertableNode(): Expression
{
return $this->scheduleStatement;
}
}
Loading