Laravel 13.35 was tagged on October 6, 2026, and it is mostly a bug-fix release with a handful of small additions that you can use immediately: a Route::query() method, a one-line way to run every scheduled task on a single server, default values for Eloquent models, an opt-in setting for lazily created local disk roots, and a fix that makes strict form requests work with Blade forms. This post covers those, with short examples, and then lists the fixes most likely to affect a real application. To get the release, run composer update laravel/framework.
1. Route::query() for the HTTP QUERY method
The router now has a query() method next to get() and post(). QUERY is an HTTP method for read-only requests that carry a request body, which is handy when search filters are too large or too structured to squeeze into a query string.
// routes/api.php
use App\Http\Controllers\ProductSearchController;
use Illuminate\Support\Facades\Route;
Route::query('/products/search', [ProductSearchController::class, 'index']);
// app/Http/Controllers/ProductSearchController.php
public function index(Request $request)
{
$filters = $request->input('filters', []);
return Product::query()
->when($filters['category'] ?? null, fn ($q, $v) => $q->where('category', $v))
->paginate();
}
curl -X QUERY https://app.test/api/products/search \
-H 'Content-Type: application/json' \
-d '{"filters":{"category":"books"}}'
Send the body as JSON. Laravel reads the input of a JSON request from the body regardless of the method, whereas a non-JSON request is read from the query string only for GET and HEAD, so a form-encoded QUERY body relies on your server stack parsing it. Two other details from the change: QUERY was added to the router's list of verbs, so Route::any() now matches it too, and QUERY requests are still subject to CSRF verification, so on web routes they need the token like a POST. Client, proxy and CDN support for the method varies, so test the full path before you depend on it.
2. Schedule::alwaysOnOneServer()
When your scheduler runs on several servers, each task needs ->onOneServer() or it runs once per server. Forgetting it on one task is an easy mistake. A single static call now applies it to every scheduled event:
// app/Providers/AppServiceProvider.php
use Illuminate\Console\Scheduling\Schedule;
public function boot(): void
{
Schedule::alwaysOnOneServer();
}
The same requirements as onOneServer() apply. The Laravel documentation says your default cache driver must be database, memcached, dynamodb or redis, and all servers must talk to the same central cache. One more detail from the implementation: closures scheduled with Schedule::call() are skipped unless they have a name, which matches the documented rule that single-server closures must be named:
Schedule::call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily(); // runs on one server because of alwaysOnOneServer()
Related: php artisan schedule:list --json now includes an on_one_server field for each event, which makes it easy to audit from a script.
3. Model defaults() via a trait
Eloquent has long supported a $attributes property for default values. 13.35 adds an opt-in defaults() method, which can compute values at runtime. Add the HasDefaultAttributes trait to use it:
use Illuminate\Database\Eloquent\Concerns\HasDefaultAttributes;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasDefaultAttributes;
protected $guarded = [];
protected function defaults(): array
{
return [
'status' => 'draft',
'views' => 0,
];
}
}
(new Post)->getAttributes(); // ['status' => 'draft', 'views' => 0]
(new Post(['status' => 'published']))->status; // 'published'
The framework's tests pin down the behavior: defaults apply when you create a new model, values passed to the constructor override them, they are not applied to models loaded from the database, and the model is not marked dirty by them. The method is evaluated for every new instance, so a default such as a trial length can come from configuration at the time the model is created. If a model also has an $attributes property, defaults() wins on conflicting keys. Without the trait, the method is never called.
4. FormRequest strict mode now ignores _token and _method
Form requests can reject any input that your rules do not mention, either per class with the #[FailOnUnknownFields] attribute or globally with FormRequest::failOnUnknownFields(). The problem was that a normal Blade form sends _token (from @csrf) and _method (from @method('PUT')), which would count as unknown fields. 13.35 excludes those two keys from the check:
use Illuminate\Foundation\Http\Attributes\FailOnUnknownFields;
use Illuminate\Foundation\Http\FormRequest;
#[FailOnUnknownFields]
class UpdateProfileRequest extends FormRequest
{
public function rules(): array
{
return ['name' => ['required', 'string', 'max:255']];
}
}
A submission with _token, _method and name now validates. A submission that adds a field such as unexpected still fails, and only that field is reported. This is a useful guard against mass-assignment surprises, because a client cannot slip in extra fields that your rules never considered.
5. lazy_root_creation for local disks
By default, Flysystem's local adapter creates the disk's root directory as soon as the adapter is constructed, which in Laravel happens when the disk is first resolved. A new lazy_root_creation option on a local disk defers that until the root is actually needed:
// config/filesystems.php
'disks' => [
'uploads' => [
'driver' => 'local',
'root' => env('UPLOADS_PATH', '/mnt/uploads'),
'lazy_root_creation' => true,
],
],
It defaults to false, so nothing changes unless you set it. A typical reason to turn it on is a root that sits on a mount which may not be ready when the disk is first resolved. I could not find this option in the Laravel documentation yet; the behavior above comes from the framework change and from the Flysystem local adapter source, which ensures the root exists before writes and directory creation when lazy mode is on.
Other fixes worth knowing about
-
incrementOrCreate()now uses the extra attributes when it creates the row. Previously they were only applied when an existing row was incremented. -
withCasts()on a cloned query no longer changes the original query. -
chunkById()andlazyById()no longer loop forever on models whose primary key has a cast. -
cursorPaginate()keeps the bindings of union selects and joins. - Route caching: a root route now matches correctly with a cached route file when the app is served from a subdirectory.
-
Storage:
FilesystemManager::set(),forgetDisk()andpurge()accept enums as disk names. -
HTTP client and mail: reference cycles from
PendingRequest::throw()'s default callback and from Mailableheaders()andpriority()callbacks were fixed, which can matter for long-running workers.
Because most of this release is fixes to Eloquent and the query builder, run your test suite after updating, particularly if you use morphTo, HasOneThrough or JSON casts.
References: the v13.35.0 release on GitHub, Laravel scheduling: running tasks on one server, and Laravel filesystem.
Originally published on DEV Talk.
Top comments (0)