Scheduler Attributes and Listener Discovery Control in Laravel 13.12.0
Last updated on by Paul Redmond
Laravel 13.12.0 adds the ability to attach metadata to scheduled events via a new withAttributes() method, a ShouldBeDiscovered interface that gives auto-discovered listeners control over their own registration, opt-out behavior for worker restarts on lost connections, SQLite file: URI support, and a handful of testing and string improvements.
- Custom attributes on scheduled events via
withAttributes() ShouldBeDiscoveredinterface for conditional listener auto-discovery- Opt out of queue worker restart on lost connection
- SQLite URI-based connections using the
file:prefix normalizeparameter forStr::studly()andStr::pascal()Client\Request::uri()for HTTP client middlewareassertJsonPathsCanonicalizing()onTestResponse- Prohibitable
cache:clearandkey:generatecommands - Fix for scheduler lifecycle callback parameter resolution by type
What's New
Custom Attributes on Scheduled Events
Scheduled events now support a withAttributes() method that lets you attach arbitrary key-value data directly to an event. These attributes are accessible inside lifecycle callbacks — useful for tagging events with metadata for monitoring, metrics, or logging without resorting to parsing the command string or description.
Schedule::after(function (Event $event) { $tag = $event->attributes['tag'] ?? null; if ($tag) { Metrics::increment( $event->exitCode === 0 ? 'scheduled.succeeded' : 'scheduled.failed', tags: ['command' => $tag] ); }})->group(function (Schedule $schedule) { $schedule->command('audio:import-podcasts --only-premium') ->withoutOverlapping() ->withAttributes(['tag' => 'import-premium-podcasts']); $schedule->command('audio:import-free') ->withoutOverlapping() ->withAttributes(['tag' => 'import-free-podcasts']);});
PR: #60255 by @cosmastech
Listener Discovery Opt-Out via ShouldBeDiscovered
A new ShouldBeDiscovered interface lets auto-discovered listeners decide whether to register themselves. Implementing the interface and returning false from its static shouldBeDiscovered() method prevents the listener from being included during discovery, without needing to remove it from the listeners directory or move logic into the handle method.
This is particularly useful for listeners that implement ShouldQueue — skipping discovery at the class level means the job is never dispatched at all, rather than being queued and then deciding to do nothing in handle.
use Illuminate\Contracts\Events\ShouldBeDiscovered;use Illuminate\Contracts\Queue\ShouldQueue; class NotifyExternalCrm implements ShouldBeDiscovered, ShouldQueue{ public static function shouldBeDiscovered(): bool { return app()->environment('production'); } public function handle(CustomerRegistered $event): void { Http::post(config('services.crm.webhook'), [ 'email' => $event->customer->email, ]); }}
PR: #60209 by @jackbayliss
Opt Out of Worker Restart on Lost Connection
Queue workers currently restart themselves whenever a database connection is lost. This can cause boot-looping when using multiple database connections (such as replicas) and one becomes temporarily unavailable. A new static property lets you disable this behavior:
use Illuminate\Queue\Worker; Worker::$stopOnLostConnection = false;
When set to false, the worker logs the lost connection and continues polling rather than exiting. This is opt-in and does not change the default behavior.
PR: #60201 by @jackbayliss
SQLite URI-Based Connections
SQLite connections now accept the file: URI prefix format introduced in PHP 8.1 and PDO. This allows specifying SQLite options such as cache=shared and mode=ro directly in the connection string:
'default' => [ 'driver' => 'sqlite', 'database' => 'file:/absolute/path/to/database.sqlite?cache=shared',],
PR: #60261 by @crynobone
Str::studly() and Str::pascal() Normalization
Both Str::studly() and Str::pascal() now accept a normalize parameter. When true, any all-uppercase word segment is lowercased before conversion, so strings like ALL_CAPS or acronyms produce the expected StudlyCase output:
Str::studly('ALL_CAPS'); // → 'ALLCAPS'Str::studly('ALL_CAPS', normalize: true); // → 'AllCaps' Str::studly('CBOR', normalize: true); // → 'Cbor'Str::studly('AllJersey', normalize: true); // → 'AllJersey' (mixed-case unchanged)
The default is false, so existing behavior is preserved.
PR: #60229 by @hotmeteor
Client\Request::uri() for HTTP Client Middleware
HTTP client request objects now have a uri() method that returns the full UriInterface for the request. This is useful in middleware when you need the complete URI — including scheme, host, path, and query string — without manually reconstructing it:
Http::withRequestMiddleware(function (RequestInterface $request) { $uri = $request->uri(); // returns a UriInterface Log::info('Outgoing request', ['url' => (string) $uri]); return $request;})->get('https://example.com/api/resource');
PR: #60282 by @stevebauman
assertJsonPathsCanonicalizing() on TestResponse
A new assertJsonPathsCanonicalizing() method on TestResponse asserts that a set of JSON paths contain the expected values, comparing them in a canonicalized (order-independent) way. This complements the existing assertJsonPath() and assertJsonPathCanonicalizing() for single paths.
PR: #60225 by @Tresor-Kasenda
Prohibitable cache:clear and key:generate Commands
Both cache:clear and key:generate Artisan commands can now be prohibited using Artisan::prohibit(). This is consistent with other prohibitable commands and useful in production environments where you want to prevent accidental key rotation or cache flushes:
// In AppServiceProvider::boot()if ($this->app->isProduction()) { \Illuminate\Console\Commands\KeyGenerateCommand::prohibit();}
PRs: #60215, #60224 by @jackbayliss
Scheduler Callback Parameter Resolution Fixed
A bug introduced in v13.10.0 caused scheduled event lifecycle callbacks to only inject the Event instance when the parameter was literally named $event. Parameters with any other name — such as $scheduledEvent or $task — would cause a BindingResolutionException. The fix now resolves the parameter by type rather than by name, matching how the rest of the Laravel container works:
// Previously failed with BindingResolutionExceptionSchedule::command('inspire') ->before(function (Event $scheduledEvent) { Log::info("Starting {$scheduledEvent->command}"); });
PR: #60197 by @kayw-geek
Miscellaneous Fixes and Improvements
- Fix path separator encoding in
LocalFilesystemAdapterandtemporaryUrl— path separators are no longer double-encoded for local disk operations (#60194, #60230 by @jackbayliss, @kayw-geek) - Fix async HTTP retries with array backoff values — passing an array of backoff intervals to async HTTP retry now works correctly (#60214 by @LucasCavalheri)
- JsonSchema fluent boolean flags can now be unset — calling a fluent boolean setter with
falsenow removes the flag rather than setting it tofalsein the schema output (#60239 by @LucasCavalheri) - Factory pivot stub — the generated stub for pivot models now includes a factory reference (#60204 by @ludo237)
- Up/Down commands report exceptions —
db:wipe/inspireand similar commands now surface exceptions through the registered exception handler (#60232 by @jackbayliss) ManagedQueueNotFoundExceptionfor missing managed queues — a dedicated exception is thrown when a managed queue cannot be found, improving error messages for Laravel Cloud queue setups (#60275 by @kieranbrown)
References