Laravel 13 was released March 17, 2026, announced live by Taylor Otwell at Laracon EU 2026. The headline promise was zero breaking changes — and it delivered. This is one of the most developer-friendly major releases in Laravel’s history.
This guide covers every confirmed new feature with before/after code examples, the requirements, the support timeline, and a step-by-step upgrade guide from Laravel 12.
Requirements
| Requirement | Laravel 12 | Laravel 13 |
|---|---|---|
| PHP Minimum | 8.2 | 8.3 |
| PHP Maximum | 8.5 | 8.5 |
| MySQL | 8.0+ | 8.0+ |
| MariaDB | 10.6+ | 10.6+ |
| SQLite | 3.35+ | 3.35+ |
The only infrastructure change: PHP 8.3 is now the minimum. If your server runs PHP 8.2, upgrade PHP before upgrading Laravel. Everything else is application-level and non-breaking.
Support Timeline
| Version | Release | Bug Fixes Until | Security Fixes Until |
|---|---|---|---|
| Laravel 11 | Mar 12, 2024 | Sep 3, 2025 | Mar 12, 2026 |
| Laravel 12 | Feb 24, 2025 | Aug 13, 2026 | Feb 24, 2027 |
| Laravel 13 | Mar 17, 2026 | Q3 2027 | Q1 2028 |
Laravel 12 is fully supported until February 2027 — no emergency to upgrade. For new projects, start on Laravel 13 today.
Feature 1 — PHP Attributes for Everything
The biggest quality-of-life improvement in Laravel 13. PHP 8 Attributes are now an alternative to class properties for configuring Laravel components. This is a non-breaking change — existing property-based configuration continues to work.
Eloquent Models
Before (Laravel 12):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $primaryKey = 'user_id';
protected $keyType = 'string';
public $incrementing = false;
protected $hidden = ['password', 'remember_token'];
protected $fillable = [
'name',
'email',
'phone',
'company_id',
];
}
After (Laravel 13):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Table('users', key: 'user_id', keyType: 'string', incrementing: false)]
#[Hidden(['password', 'remember_token'])]
#[Fillable(['name', 'email', 'phone', 'company_id'])]
class User extends Model
{
// Clean — no property clutter
}
All available model attributes:
#[Appends(['full_name', 'avatar_url'])]
#[Connection('mysql_readonly')]
#[Fillable(['name', 'email'])]
#[Guarded(['id', 'created_at'])]
#[Hidden(['password'])]
#[Table('users', key: 'user_id')]
#[Touches(['profile', 'posts'])]
#[Unguarded]
#[Visible(['name', 'email'])]
Queue Jobs
Before (Laravel 12):
class ProcessPodcast implements ShouldQueue
{
public $connection = 'redis';
public $queue = 'podcasts';
public $tries = 3;
public $timeout = 120;
public $backoff = [1, 5, 10];
}
After (Laravel 13):
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Backoff;
#[Connection('redis')]
#[Queue('podcasts')]
#[Tries(3)]
#[Timeout(120)]
#[Backoff([1, 5, 10])]
class ProcessPodcast implements ShouldQueue
{
// No property clutter
}
All available queue attributes:
#[Backoff([1, 5, 10])]
#[Connection('redis')]
#[FailOnTimeout]
#[MaxExceptions(3)]
#[Queue('podcasts')]
#[Timeout(120)]
#[Tries(3)]
#[UniqueFor(3600)]
Console Commands
Before (Laravel 12):
class SendMailCommand extends Command
{
protected $signature = 'mail:send {user} {--queue}';
protected $description = 'Send a marketing email to a user';
}
After (Laravel 13):
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Attributes\Description;
#[Signature('mail:send {user} {--queue}')]
#[Description('Send a marketing email to a user')]
class SendMailCommand extends Command
{
// Signature and description live at the top, not buried in properties
}
Form Requests
#[RedirectTo('/dashboard')]
#[StopOnFirstFailure]
class CreateOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
];
}
}
API Resources
#[Collects(OrderResource::class)]
#[PreserveKeys]
class OrderCollection extends ResourceCollection {}
Attributes also work on Listeners, Notifications, Mailables, Broadcast Events, Factories, and Test Seeders — the full framework is covered.
Feature 2 — Cache::touch()
A small but frequently needed feature. The new Cache::touch() method extends a cached item’s TTL without fetching or re-storing the value.
Before (Laravel 12 — required a get + put):
// Old way — fetches the value unnecessarily
$value = Cache::get('user_session:123');
if ($value !== null) {
Cache::put('user_session:123', $value, 3600); // Re-stores unnecessarily
}
After (Laravel 13):
// Just extend the TTL — no get, no re-store
Cache::touch('user_session:123', 3600);
// With a DateTime
Cache::touch('analytics_data', now()->addHours(6));
// Extend indefinitely (remove expiry)
Cache::touch('permanent_cache', null);
// Returns true if key exists, false if not
$extended = Cache::touch('user_session:123', 3600);
if (!$extended) {
// Key expired before we could touch it — re-create it
}
This is implemented across all cache drivers: Array, APC, Database, DynamoDB, File, Memcached, Memoized, Null, and Redis. Redis uses a single EXPIRE command, Memcached uses TOUCH, and the database driver issues a single UPDATE.
Perfect for session extension, sliding cache windows, and keeping computed results alive while they’re being used.
Feature 3 — Laravel AI SDK (Stable)
The headline feature in the release notes is the new Laravel AI SDK. Previously in beta, it went production-stable with Laravel 13.
The AI SDK gives Laravel a first-party, provider-agnostic way to integrate AI — supporting Claude (Anthropic), OpenAI, and Gemini from a single unified API:
composer require laravel/ai
// config/ai.php
return [
'default' => env('AI_PROVIDER', 'anthropic'),
'providers' => [
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL', 'claude-sonnet-4-5'),
],
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'model' => env('OPENAI_MODEL', 'gpt-4o'),
],
],
];
use Laravel\AI\Facades\AI;
// Simple completion
$response = AI::complete('Summarize this text: ' . $text);
// Chat with history
$response = AI::chat([
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'What is Laravel?'],
]);
// Stream response
AI::stream('Write a product description for: ' . $product->name,
function (string $chunk) {
echo $chunk;
}
);
// Switch provider on the fly
AI::provider('openai')->complete('Hello');
AI::provider('anthropic')->complete('Hello');
The SDK handles token counting, retry logic, streaming, and provider switching — without locking you to a specific AI company’s SDK.
Feature 4 — Passkey Authentication
Laravel 13 ships with first-party Passkey support — the WebAuthn-based authentication standard that replaces passwords with device biometrics (Face ID, fingerprint, Windows Hello).
composer require laravel/passkeys
php artisan passkeys:install
// Register a passkey
use Laravel\Passkeys\Facades\Passkeys;
// In your registration controller
public function startRegistration(Request $request)
{
$options = Passkeys::generateRegistrationOptions($request->user());
return response()->json($options);
}
public function finishRegistration(Request $request)
{
$passkey = Passkeys::verifyRegistration(
$request->user(),
$request->all()
);
return response()->json(['passkey_id' => $passkey->id]);
}
// Authenticate with passkey
public function startAuthentication(Request $request)
{
$options = Passkeys::generateAuthenticationOptions();
return response()->json($options);
}
public function finishAuthentication(Request $request)
{
$user = Passkeys::verifyAuthentication($request->all());
Auth::login($user);
return redirect('/dashboard');
}
Frontend integration uses the WebAuthn browser API — compatible with all modern browsers and devices with biometric sensors. No passwords stored, no password reset flows, no phishing risk.
Feature 5 — Queue::route()
The new Queue::route() method lets you define which queue and connection each job class uses from a single location in a service provider. Previously, teams either set queue properties on each job class or repeated the configuration at every dispatch site.
// Before — repeated at every dispatch
ProcessPodcast::dispatch($podcast)->onQueue('podcasts')->onConnection('redis');
SendWelcomeEmail::dispatch($user)->onQueue('emails')->onConnection('sqs');
GenerateReport::dispatch($report)->onQueue('reports')->onConnection('redis');
// After — define once in AppServiceProvider
use Illuminate\Support\Facades\Queue;
Queue::route([
ProcessPodcast::class => 'redis:podcasts',
SendWelcomeEmail::class => 'sqs:emails',
GenerateReport::class => 'redis:reports',
]);
// Now just dispatch — routing is automatic
ProcessPodcast::dispatch($podcast);
SendWelcomeEmail::dispatch($user);
GenerateReport::dispatch($report);
Combined with the new PHP Attributes for queue configuration, this is the cleanest queue setup Laravel has ever had.
Feature 6 — JSON:API Support
Laravel 13 includes first-party support for the JSON:API specification. The new resource classes handle response object serialization, relationship inclusion, sparse fieldsets, links and compliant response headers automatically.
php artisan make:json-api-resource PostResource
use Illuminate\Http\Resources\Json\JsonApiResource;
class PostResource extends JsonApiResource
{
public function toAttributes(Request $request): array
{
return [
'title' => $this->title,
'content' => $this->content,
'published' => $this->published_at?->toISOString(),
];
}
public function toRelationships(Request $request): array
{
return [
'author' => AuthorResource::make($this->author),
'comments' => CommentResource::collection($this->comments),
];
}
public function toLinks(Request $request): array
{
return [
'self' => route('posts.show', $this->id),
];
}
}
Response automatically follows JSON:API spec:
{
"data": {
"type": "posts",
"id": "1",
"attributes": {
"title": "Laravel 13 Released",
"content": "...",
"published": "2026-03-17T09:00:00Z"
},
"relationships": {
"author": {
"data": { "type": "users", "id": "42" }
}
},
"links": {
"self": "https://api.example.com/posts/1"
}
}
}
Feature 7 — Typed Configuration
No more guessing whether a config value is a string, boolean, or integer:
// Before — could return string "true" or boolean true depending on env
$debug = config('app.debug'); // What type is this?
// Laravel 13 — typed retrieval
$debug = config()->boolean('app.debug'); // Always bool
$port = config()->integer('database.port'); // Always int
$name = config()->string('app.name'); // Always string
$hosts = config()->array('cache.stores'); // Always array
// Throws ConfigTypeMismatchException if type doesn't match
// Catches a whole class of bugs at boot time
Feature 8 — Middleware Simplification
Laravel 13 simplifies the middleware stack. The app/Http/Kernel.php file is gone (deprecated since Laravel 11, now removed). All middleware configuration lives in bootstrap/app.php.
// bootstrap/app.php — everything in one place
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
)
->withMiddleware(function (Middleware $middleware) {
// Global middleware
$middleware->append(ForceHttps::class);
// Route group middleware
$middleware->api(append: [
EnsureApiToken::class,
]);
// Middleware aliases
$middleware->alias([
'verified' => EnsureEmailIsVerified::class,
]);
})
->create();
Feature 9 — Reverb Database Driver
Laravel Reverb (the WebSocket server) now includes a database driver — meaning you can run real-time WebSockets without Redis for smaller applications:
# .env — use database instead of Redis for WebSocket state
REVERB_DRIVER=database
# No Redis required for:
# - Channel presence tracking
# - Connection state
# - Message queuing (small scale)
For production at scale, Redis remains the recommended driver. The database driver is ideal for development environments and small-scale deployments where Redis is overkill.
Feature 10 — route:conflicts Command
The route:conflicts command detects overlapping routes — a common source of hard-to-debug bugs:
php artisan route:conflicts
+-------------------+--------+---------------------------+
| URI | Method | Conflict |
+-------------------+--------+---------------------------+
| /users/{id} | GET | Overlaps with /users/new |
| /products/{slug} | GET | Overlaps with /products/1 |
+-------------------+--------+---------------------------+
PHP 8.3 — What You Actually Gain
Upgrading to PHP 8.3 for Laravel 13 isn’t just a requirement — it brings real improvements:
// Typed class constants (PHP 8.3)
class Status
{
const string ACTIVE = 'active';
const string INACTIVE = 'inactive';
const int MAX_RETRY = 3;
}
// json_validate() — no more json_decode() just to check validity
$valid = json_validate($jsonString); // true or false, no decode needed
// Readonly property improvements
class Order
{
public function __construct(
public readonly string $id,
public readonly float $total,
) {}
}
// Dynamic class constant fetch
$constant = Status::{$name}; // Valid in PHP 8.3
Laravel 12 vs Laravel 13 — Side by Side
| Feature | Laravel 12 | Laravel 13 |
|---|---|---|
| PHP minimum | 8.2 | 8.3 |
| Model configuration | Class properties | PHP Attributes (optional) |
| Job configuration | Class properties | PHP Attributes (optional) |
| Command signature | Class property | PHP Attribute (optional) |
| Cache TTL extension | get + put | Cache::touch() |
| AI integration | Third-party packages | First-party Laravel AI SDK |
| Passkeys | Third-party only | First-party support |
| Queue routing | Per-dispatch | Queue::route() central config |
| JSON:API | Third-party | First-party resources |
| Config types | Untyped | Typed retrieval |
| Middleware config | Kernel.php (legacy) | bootstrap/app.php only |
| WebSockets (Reverb) | Redis only | Redis + Database driver |
| Route conflicts | Manual detection | route:conflicts command |
How to Upgrade from Laravel 12 to Laravel 13
Most applications upgrade in under 10 minutes. Here’s the process:
Step 1 — Check PHP Version
php -v
# Must show PHP 8.3.x or higher
# If not, upgrade PHP in your hosting panel (Hostinger/cPanel: PHP Configuration)
Step 2 — Check Package Compatibility
# Check which packages need updates
composer outdated
# Key packages to verify:
# - livewire/livewire
# - inertiajs/inertia-laravel
# - filament/filament
# - spatie/* packages
# - laravel/sanctum
# - laravel/horizon
Most major packages — Livewire, Inertia, Filament, Spatie — already have Laravel 13 support. Check each package’s GitHub releases page.
Step 3 — Update composer.json
{
"require": {
"php": "^8.3",
"laravel/framework": "^13.0"
}
}
Step 4 — Run Composer Update
composer update laravel/framework --with-dependencies
Step 5 — Run the Upgrade Command
php artisan upgrade
Laravel 13 includes an upgrade command that checks for common issues and guides you through any manual changes needed.
Step 6 — Clear and Test
php artisan optimize:clear
php artisan test
php artisan route:conflicts
Step 7 — Use Laravel Shift (Alternative)
For complex applications, Laravel Shift automates the upgrade — it opens a PR with atomic commits covering every change. Worth the small fee for large codebases.
Should You Upgrade Now?
New projects: Yes — always start new projects on the latest version. Laravel 13 is the current stable release.
Existing projects on Laravel 12: No emergency — Laravel 12 gets bug fixes until August 2026 and security fixes until February 2027. Plan the upgrade for your next maintenance window.
Existing projects on Laravel 11 or older: Upgrade now. Laravel 11’s security support ended March 2026.
The real reason to upgrade: PHP Attributes alone reduce boilerplate significantly. The Laravel AI SDK stable means AI integration is now a first-party, supported part of the framework. Queue::route() cleans up dispatch sites across large codebases. These are genuine improvements worth having.
If you need help upgrading a Laravel application to version 13, or want a new project built on the latest stack, our team at Softcrony is happy to help.
Leave a comment