diff --git a/README.md b/README.md index 6709896..e612957 100644 --- a/README.md +++ b/README.md @@ -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: + +```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 diff --git a/src/Builders/AppBootstrapBuilder.php b/src/Builders/AppBootstrapBuilder.php index f3a0316..abf02a6 100644 --- a/src/Builders/AppBootstrapBuilder.php +++ b/src/Builders/AppBootstrapBuilder.php @@ -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 { @@ -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'; @@ -25,4 +27,15 @@ public function addExceptionsRender(string $exceptionClass, string $renderBody, return $this; } + + public function addScheduleCommand(string $command, ScheduleOption ...$options): self + { + $this->traverser->addVisitor(new AddScheduleCommand($command, ...$options)); + + $this->addImports([ + 'Illuminate\Support\Facades\Schedule', + ]); + + return $this; + } } diff --git a/src/Printer.php b/src/Printer.php index f487f3e..0110bc2 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -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; @@ -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); + } } diff --git a/src/ValueOptions/ScheduleOption.php b/src/ValueOptions/ScheduleOption.php new file mode 100644 index 0000000..0b00e60 --- /dev/null +++ b/src/ValueOptions/ScheduleOption.php @@ -0,0 +1,42 @@ +validateMethod($this->method); + } + + 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)) { + $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/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php index 3c92660..265de8d 100644 --- a/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php +++ b/src/Visitors/AppBootstrapVisitors/AbstractAppBootstrapVisitor.php @@ -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); + } + + if ($node->var->getAttribute('wasCreated')) { + $node->var = $this->handleParentNode($node->var); + } + } + if (!$node instanceof MethodCall) { return $node; } @@ -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; @@ -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; diff --git a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php index 1a18e20..5022dc8 100644 --- a/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php +++ b/src/Visitors/AppBootstrapVisitors/AddExceptionsRender.php @@ -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; @@ -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; + } } diff --git a/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php new file mode 100644 index 0000000..6e2842a --- /dev/null +++ b/src/Visitors/AppBootstrapVisitors/AddScheduleCommand.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/tests/AppBootstrapBuilderTest.php b/tests/AppBootstrapBuilderTest.php index 6945e47..bb37395 100644 --- a/tests/AppBootstrapBuilderTest.php +++ b/tests/AppBootstrapBuilderTest.php @@ -2,25 +2,82 @@ namespace RonasIT\Larabuilder\Tests; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use RonasIT\Larabuilder\Builders\AppBootstrapBuilder; use RonasIT\Larabuilder\Exceptions\InvalidBootstrapAppFileException; use RonasIT\Larabuilder\Exceptions\InvalidPHPCodeException; use RonasIT\Larabuilder\Tests\Support\Traits\PHPFileBuilderTestMockTrait; +use RonasIT\Larabuilder\ValueOptions\ScheduleOption; use Symfony\Component\HttpKernel\Exception\HttpException; class AppBootstrapBuilderTest extends TestCase { use PHPFileBuilderTestMockTrait; - public function testAddExceptionsRenderEmpty(): void + public static function provideForbiddenFiles(): array + { + return [ + [ + 'fixture' => 'class.php', + 'type' => 'class', + ], + [ + 'fixture' => 'trait.php', + 'type' => 'trait', + ], + [ + 'fixture' => 'interface.php', + 'type' => 'interface', + ], + [ + 'fixture' => 'enum.php', + 'type' => 'enum', + ], + ]; + } + + #[DataProvider('provideForbiddenFiles')] + public function testInvalidBootstrapAppFileException(string $fixture, string $type): void { - $file = $this->generateOriginalStructurePath('bootstrap_empty.php'); + $file = $this->generateOriginalStructurePath($fixture); + + $this->assertExceptionThrew(InvalidBootstrapAppFileException::class, "Bootstrap app file must not contain {$type} declarations"); + + new AppBootstrapBuilder($file) + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: 'return;', + ) + ->save(); + } + + public function testAddExceptionsRenderBootstrapEmpty(): void + { + $file = $this->generateOriginalStructurePath('bootstrap_without_with_calls.php'); $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', - $this->callFilePutContent($file, 'bootstrap_empty.php'), + $this->callFilePutContent($file, 'bootstrap_exceptions.php'), + ); + + new AppBootstrapBuilder($file) + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: $this->getJsonFixture('render_body'), + includeRequestArg: true, + ) + ->save(); + } + + public function testAddExceptionsRenderWithExceptionsEmpty(): void + { + $file = $this->generateOriginalStructurePath('bootstrap_with_calls_empty.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'bootstrap_empty_exceptions.php'), ); new AppBootstrapBuilder($file) @@ -40,11 +97,11 @@ public function testAddExceptionsRenderEmpty(): void public function testAddExceptionsRenderCustom(): void { - $file = $this->generateOriginalStructurePath('bootstrap_configured.php'); + $file = $this->generateOriginalStructurePath('bootstrap.php'); $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', - $this->callFilePutContent($file, 'bootstrap_configured.php'), + $this->callFilePutContent($file, 'bootstrap_configured_exceptions.php'), ); new AppBootstrapBuilder($file) @@ -68,25 +125,26 @@ public function testAddExceptionsRenderCustom(): void public function testAddExceptionsRenderExist(): void { - $file = $this->generateOriginalStructurePath('bootstrap_with_render.php'); + $file = $this->generateOriginalStructurePath('bootstrap.php'); $this->mockNativeFunction( 'RonasIT\Larabuilder\Builders', - $this->callFilePutContent($file, 'bootstrap_with_render.php'), + $this->callFilePutContent($file, 'bootstrap_unchanged.php'), ); new AppBootstrapBuilder($file) ->addExceptionsRender( - exceptionClass: HttpException::class, - renderBody: $this->getJsonFixture('render_body'), - includeRequestArg: true, + exceptionClass: ExpectationFailedException::class, + renderBody: ' + throw $exception; + ', ) ->save(); } public function testAddExceptionsRenderInvalidBody() { - $file = $this->generateOriginalStructurePath('bootstrap_configured.php'); + $file = $this->generateOriginalStructurePath('bootstrap.php'); $this->assertExceptionThrew( expectedClassName: InvalidPHPCodeException::class, @@ -101,40 +159,91 @@ public function testAddExceptionsRenderInvalidBody() ->save(); } - public static function provideForbiddenFiles(): array + public function testAddScheduleCommandBootstrapEmpty(): void { - return [ - [ - 'fixture' => 'class.php', - 'type' => 'class', - ], - [ - 'fixture' => 'trait.php', - 'type' => 'trait', - ], - [ - 'fixture' => 'interface.php', - 'type' => 'interface', - ], - [ - 'fixture' => 'enum.php', - 'type' => 'enum', - ], - ]; + $file = $this->generateOriginalStructurePath('bootstrap_without_with_calls.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'bootstrap_schedule.php'), + ); + + new AppBootstrapBuilder($file) + ->addScheduleCommand( + 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + new ScheduleOption('environments', ['production']), + new ScheduleOption('evenInMaintenanceMode'), + new ScheduleOption('daily'), + 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']), + ) + ->addScheduleCommand( + 'reports:generate', + new ScheduleOption('daily'), + new ScheduleOption('sendOutputTo', ['/var/log/reports.log']), + new ScheduleOption('pingOnFailure', ['https://example.com/ping']), + ) + ->save(); } - #[DataProvider('provideForbiddenFiles')] - public function testInvalidBootstrapAppFileException(string $fixture, string $type): void + public function testAddScheduleCommandWithScheduleEmpty(): void { - $file = $this->generateOriginalStructurePath($fixture); + $file = $this->generateOriginalStructurePath('bootstrap_with_calls_empty.php'); - $this->assertExceptionThrew(InvalidBootstrapAppFileException::class, "Bootstrap app file must not contain {$type} declarations"); + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'bootstrap_empty_schedule.php'), + ); new AppBootstrapBuilder($file) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + options: new ScheduleOption('environments', ['production']), + ) + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception:12222') + ->save(); + } + + public function testCombineScheduleAndExceptionRenders(): void + { + $file = $this->generateOriginalStructurePath('bootstrap_with_calls_empty.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'bootstrap_combine.php'), + ); + + new AppBootstrapBuilder($file) + ->addScheduleCommand( + command: 'telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336', + options: new ScheduleOption('environments', ['production']), + ) + ->addExceptionsRender( + exceptionClass: HttpException::class, + renderBody: 'return;', + ) + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception_2') ->addExceptionsRender( exceptionClass: HttpException::class, renderBody: 'return;', ) + ->addScheduleCommand('telescope:prune --set-hours=resolved_exception_3') ->save(); } + + public function testScheduleOptionDTOInvalidMethod(): void + { + $this->assertExceptionThrew( + expectedClassName: InvalidArgumentException::class, + expectedMessage: $this->getExceptionFixture('invalid_schedule_option'), + isStrict: false, + ); + + new ScheduleOption('invalid_frequency'); + } } diff --git a/tests/Support/OriginStructures/bootstrap_configured.php b/tests/Support/OriginStructures/bootstrap.php similarity index 82% rename from tests/Support/OriginStructures/bootstrap_configured.php rename to tests/Support/OriginStructures/bootstrap.php index d6b223d..f506f2c 100644 --- a/tests/Support/OriginStructures/bootstrap_configured.php +++ b/tests/Support/OriginStructures/bootstrap.php @@ -11,6 +11,7 @@ use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Http\Middleware\HandleCors; use Illuminate\Session\TokenMismatchException; +use Illuminate\Support\Facades\Schedule; use Illuminate\Validation\ValidationException; use PHPUnit\Framework\ExpectationFailedException; @@ -23,7 +24,7 @@ health: '/status', apiPrefix: '', ) - ->withMiddleware(function (Middleware $middleware) { + ->withMiddleware(function (Middleware $middleware): void { $middleware->use([ HandleCors::class, CheckForMaintenanceMode::class, @@ -31,7 +32,7 @@ ConvertEmptyStringsToNull::class, ]); }) - ->withExceptions(function (Exceptions $exceptions) { + ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontReport([ AuthenticationException::class, AuthorizationException::class, @@ -49,4 +50,6 @@ 'password_confirmation', ]); }) - ->create(); + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + })->create(); diff --git a/tests/Support/OriginStructures/bootstrap_empty.php b/tests/Support/OriginStructures/bootstrap_with_calls_empty.php similarity index 77% rename from tests/Support/OriginStructures/bootstrap_empty.php rename to tests/Support/OriginStructures/bootstrap_with_calls_empty.php index c183276..415a0e1 100644 --- a/tests/Support/OriginStructures/bootstrap_empty.php +++ b/tests/Support/OriginStructures/bootstrap_with_calls_empty.php @@ -6,13 +6,14 @@ return Application::configure(basePath: dirname(__DIR__)) ->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 { + // })->create(); diff --git a/tests/Support/OriginStructures/bootstrap_without_with_calls.php b/tests/Support/OriginStructures/bootstrap_without_with_calls.php new file mode 100644 index 0000000..ca0f8d6 --- /dev/null +++ b/tests/Support/OriginStructures/bootstrap_without_with_calls.php @@ -0,0 +1,5 @@ +create(); diff --git a/tests/TestCase.php b/tests/TestCase.php index c67000d..3a185d2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,4 +23,11 @@ protected function generateOriginalStructurePath(string $name): string { return __DIR__ . "/Support/OriginStructures/{$name}"; } + + public function getExceptionFixture(string $fixtureName): string + { + $fixtureName = (str_contains($fixtureName, '.')) ? $fixtureName : "{$fixtureName}.txt"; + + return $this->getFixture("exceptions/{$fixtureName}"); + } } diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_combine.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_combine.php new file mode 100644 index 0000000..b6b7654 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_combine.php @@ -0,0 +1,27 @@ +withRouting( + // + ) + ->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/bootstrap_configured.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_configured_exceptions.php similarity index 85% rename from tests/fixtures/AppBootstrapBuilderTest/bootstrap_configured.php rename to tests/fixtures/AppBootstrapBuilderTest/bootstrap_configured_exceptions.php index 92a6dc6..40a0abb 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_configured.php +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_configured_exceptions.php @@ -11,6 +11,7 @@ use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Http\Middleware\HandleCors; use Illuminate\Session\TokenMismatchException; +use Illuminate\Support\Facades\Schedule; use Illuminate\Validation\ValidationException; use PHPUnit\Framework\ExpectationFailedException; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -25,7 +26,7 @@ health: '/status', apiPrefix: '', ) - ->withMiddleware(function (Middleware $middleware) { + ->withMiddleware(function (Middleware $middleware): void { $middleware->use([ HandleCors::class, CheckForMaintenanceMode::class, @@ -33,7 +34,7 @@ ConvertEmptyStringsToNull::class, ]); }) - ->withExceptions(function (Exceptions $exceptions) { + ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontReport([ AuthenticationException::class, AuthorizationException::class, @@ -57,4 +58,6 @@ : null; }); }) - ->create(); + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_exceptions.php similarity index 88% rename from tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty.php rename to tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_exceptions.php index 4f18b17..a35ddf2 100644 --- a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty.php +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_exceptions.php @@ -9,9 +9,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', - commands: __DIR__.'/../routes/console.php', - health: '/up', + // ) ->withMiddleware(function (Middleware $middleware): void { // @@ -24,4 +22,7 @@ $exceptions->render(function (ExpectationFailedException $exception) { throw $exception; }); + }) + ->withSchedule(function (): void { + // })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_schedule.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_schedule.php new file mode 100644 index 0000000..2f546c2 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_empty_schedule.php @@ -0,0 +1,22 @@ +withRouting( + // + ) + ->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/Support/OriginStructures/bootstrap_with_render.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_exceptions.php similarity index 50% rename from tests/Support/OriginStructures/bootstrap_with_render.php rename to tests/fixtures/AppBootstrapBuilderTest/bootstrap_exceptions.php index f482eb7..4ef368d 100644 --- a/tests/Support/OriginStructures/bootstrap_with_render.php +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_exceptions.php @@ -2,21 +2,12 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; -use Illuminate\Foundation\Configuration\Middleware; use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) - ->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; + return ($request->expectsJson()) ? response()->json(['error' => $exception->getMessage()], $exception->getStatusCode()) : null; }); })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php new file mode 100644 index 0000000..7495cdd --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_schedule.php @@ -0,0 +1,15 @@ +withSchedule(function (): void { + 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'); + + Schedule::command('reports:generate')->daily()->sendOutputTo('/var/log/reports.log')->pingOnFailure('https://example.com/ping'); + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_unchanged.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_unchanged.php new file mode 100644 index 0000000..f506f2c --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_unchanged.php @@ -0,0 +1,55 @@ +withRouting( + web: __DIR__ . '/../routes/web.php', + api: __DIR__ . '/../routes/api.php', + commands: __DIR__ . '/../routes/console.php', + channels: __DIR__ . '/../routes/channels.php', + health: '/status', + apiPrefix: '', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->use([ + HandleCors::class, + CheckForMaintenanceMode::class, + ValidatePostSize::class, + ConvertEmptyStringsToNull::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->dontReport([ + AuthenticationException::class, + AuthorizationException::class, + ModelNotFoundException::class, + TokenMismatchException::class, + ValidationException::class, + ]); + + $exceptions->render(function (ExpectationFailedException $exception) { + throw $exception; + }); + + $exceptions->dontFlash([ + 'password', + 'password_confirmation', + ]); + }) + ->withSchedule(function (): void { + Schedule::command('telescope:prune --set-hours=resolved_exception:1,completed_job:0.1 --hours=336')->environments('production'); + })->create(); diff --git a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_with_render.php b/tests/fixtures/AppBootstrapBuilderTest/bootstrap_with_render.php deleted file mode 100644 index f482eb7..0000000 --- a/tests/fixtures/AppBootstrapBuilderTest/bootstrap_with_render.php +++ /dev/null @@ -1,22 +0,0 @@ -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(); 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..85e8ef3 --- /dev/null +++ b/tests/fixtures/AppBootstrapBuilderTest/exceptions/invalid_schedule_option.txt @@ -0,0 +1,2 @@ +Unknown schedule method `invalid_frequency`. +Allowed methods: \ No newline at end of file