app->runningInConsole()) { return; } // Guard against table not existing (pre-migration) try { if (! Schema::hasTable('scheduled_actions')) { return; } } catch (\Throwable) { // DB unreachable — skip gracefully so scheduler doesn't crash return; } $this->app->booted(function () { $this->registerScheduledActions(); }); } private function registerScheduledActions(): void { $schedule = $this->app->make(Schedule::class); $actions = ScheduledAction::enabled()->get(); foreach ($actions as $action) { try { $class = $action->action_class; // Validate namespace prefix against allowlist $hasAllowedNamespace = false; foreach (self::ALLOWED_NAMESPACES as $prefix) { if (str_starts_with($class, $prefix)) { $hasAllowedNamespace = true; break; } } if (! $hasAllowedNamespace) { logger()->warning("Scheduled action {$class} has disallowed namespace — skipping"); continue; } if (! class_exists($class)) { continue; } // Verify the class uses the Action trait if (! in_array(Action::class, class_uses_recursive($class), true)) { logger()->warning("Scheduled action {$class} does not use the Action trait — skipping"); continue; } // Validate frequency method against allowlist $method = $action->frequencyMethod(); if (! in_array($method, self::ALLOWED_FREQUENCIES, true)) { logger()->warning("Scheduled action {$class} has invalid frequency method: {$method}"); continue; } $event = $schedule->call(function () use ($class, $action) { $class::run(); $action->markRun(); })->name($class); // Apply frequency $args = $action->frequencyArgs(); $event->{$method}(...$args); // Apply options if ($action->without_overlapping) { $event->withoutOverlapping(); } if ($action->run_in_background) { $event->runInBackground(); } if ($action->timezone) { $event->timezone($action->timezone); } } catch (\Throwable $e) { logger()->warning("Failed to register scheduled action: {$action->action_class} — {$e->getMessage()}"); } } } }