The Laravel 13 Features That Matter in Real Projects
Laravel 13 shipped on March 17, 2026, and the upgrade story is unusually simple: zero application-level breaking changes from Laravel 12, one hard requirement (PHP 8.3), and several features that are genuinely useful in production rather than just impressive in release notes.
This post focuses on the features you will actually reach for on real client projects — not an exhaustive tour. For the full release overview, upgrade checklist, and breaking changes reference, see Laravel 13: Features, Upgrade Guide, and Breaking Changes.
Prerequisites: PHP 8.3+, Laravel 13.x (latest stable: 13.14.0 as of June 2026), Composer 2.x.
1. PHP Attributes on Models and Controllers
Laravel 13 adds PHP 8-style #[Attribute] support across 15+ framework locations. The old property-based syntax still works — this is purely additive.
On Eloquent Models:
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
#[Table('posts', primaryKey: 'id', incrementing: true, timestamps: true)]
#[Fillable('title', 'body', 'user_id')]
#[Hidden('deleted_at')]
class Post extends Model {}
On Controllers:
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class CommentController extends Controller
{
#[Middleware('subscribed')]
#[Authorize('create', [Comment::class, 'post'])]
public function store(Post $post) { }
}
When to actually use this: Attributes shine on large domain models where $fillable, $hidden, $casts, and relationship declarations are scattered across the class. Collocating table definition and mass assignment rules at the top of the file improves readability at a glance. On small CRUD models, the tradeoff is extra import lines for minimal gain.
Common mistake: Mass-converting every existing model to attribute syntax in a single PR. It creates a large, noisy diff with zero runtime benefit and can introduce subtle errors if you miss a property. Migrate incrementally, only when touching the file for another reason.
2. Cache::touch() — Extend TTL Without a Round-Trip
// Extends the expiry of 'session-token' by 2 hours without fetching or rewriting the value
Cache::touch('session-token', now()->addHours(2));
Before this, extending a cache item's TTL required a get → put cycle, which wasted a round-trip and reset the value unnecessarily. Cache::touch() maps directly to:
-
Redis: a single
EXPIREcommand -
Memcached: the native
TOUCHoperation
Practical use: Session-adjacent caches, expensive computed aggregates you want to keep alive while the page is being viewed, and rate-limit counters where you want sliding window behaviour without rewriting the counter.
Limitation you need to know: If you maintain a custom cache store driver, it must implement a touch(string $key, $seconds) method or you will get a fatal runtime error as soon as any code path calls Cache::touch(). Audit your custom drivers before upgrading.
3. Typed Configuration Retrieval
Laravel 12 had typed config helpers, but mismatches were caught at runtime — often in a code path that only ran under specific conditions. Laravel 13 throws ConfigTypeMismatchException at boot.
// Throws ConfigTypeMismatchException at boot if APP_DEBUG is not a boolean
$debug = config()->boolean('app.debug');
// Throws if CACHE_TTL is not an integer
$ttl = config()->integer('cache.ttl');
Why this matters in real projects: Misconfigured environment variables are one of the most common causes of silent production bugs. A PHP string "true" passing as a boolean is a classic failure mode. With Laravel 13, your application will refuse to boot with an incorrect config type rather than misbehaving hours later in a specific feature.
Tradeoff: This is a fail-fast design. If your team is not used to boot-time exceptions, the first deployment after introducing typed config calls can be alarming. Add these calls deliberately, and test them in staging with the actual production .env shape before deploying.
4. JSON:API Resources Out of the Box
JSON:API compliance previously required a third-party package. Laravel 13 ships JsonApiResource natively.
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
class PostResource extends JsonApiResource
{
public function toAttributes($request): array
{
return [
'title' => $this->title,
'body' => $this->body,
];
}
}
The response automatically uses application/vnd.api+json as the Content-Type and wraps the payload in the spec-compliant data.attributes structure.
Generate one with Artisan:
php artisan make:resource PostResource --json-api
When to prefer this over existing packages: For teams building new APIs that need basic JSON:API compliance — resource objects, relationships, and sparse fieldsets — the built-in class handles the common cases with less setup. Teams with complex compound document requirements, or existing APIs built on cloudcreativity/laravel-json-api, are better served staying on their current package.
5. Queue::route() — Centralized Job Routing
Previously, assigning a job to a specific queue and connection required per-class configuration inside each job's $queue and $connection properties. Laravel 13 introduces centralized routing:
use Illuminate\Support\Facades\Queue;
// In a service provider or bootstrap/app.php
Queue::route(ProcessPodcast::class, queue: 'podcasts', connection: 'redis');
Queue::route(SendEmailJob::class, queue: 'emails', connection: 'sqs');
Queue::route(GenerateReport::class, queue: 'reports', connection: 'redis');
This is particularly useful when job classes live in packages or vendor code and you cannot modify them directly. It also keeps infrastructure decisions — which connection, which queue — in a single place rather than embedded in business logic classes.
Discoverability tradeoff: Routing rules defined in a service provider are less immediately visible to developers reading a job class. Document the routing location in your project's README or leave a comment in the job class pointing to it.
6. Queue Inspection Methods (Added in 13.8.0)
Building admin panels or monitoring dashboards for queues previously required raw Redis or SQS calls. Laravel 13.8.0 adds first-party inspection:
use Illuminate\Support\Facades\Queue;
$pending = Queue::allPendingJobs(); // all pending jobs across every queue
$reserved = Queue::allReservedJobs(); // jobs currently being processed
$delayed = Queue::allDelayedJobs(); // jobs waiting for their delay to expire
These return collections you can filter, count, and display. Worker pause and resume events are also added in the same release, enabling graceful drain-and-restart patterns during deployment.
7. The Laravel AI SDK Goes Stable
The AI SDK was in beta during the Laravel 12 era. It is now a fully stable, first-party API in Laravel 13. This is the one to re-evaluate if you dismissed it during the beta.
Basic text generation:
use Laravel\AI\Facades\AI;
$response = AI::text()
->using('openai', 'gpt-4o')
->prompt('Summarize this article: ' . $article)
->generate();
echo $response->text();
Embeddings and vector search (with PostgreSQL + pgvector):
$embedding = AI::embeddings()
->using('openai', 'text-embedding-3-small')
->input('What is the refund policy?')
->generate();
$results = Post::query()
->nearestTo('embedding_column', $embedding->vector)
->limit(5)
->get();
Supported providers: OpenAI, Anthropic (Claude), Google Gemini. The SDK also covers tool-calling agents, image generation, and audio synthesis and transcription through the same unified API.
Security note: AI provider credentials must live in .env only. The SDK reads from config/ai.php, which references environment variables. Never hard-code or commit API keys.
Tradeoff vs. community packages: Prism and the OpenAI PHP package may support newer model features sooner, since the first-party SDK moves at a measured pace. If you need access to cutting-edge model capabilities the day they release, evaluate community packages. For stable, Laravel-native usage, the first-party SDK is now the correct default.
The Only Real Blocker: PHP 8.3
With zero application-level breaking changes, the upgrade from Laravel 12 to 13 is straightforward — unless your hosting environment is still on PHP 8.2. PHP 8.2 reaches end of life in December 2026, so the Laravel 13 requirement is also a nudge to move off a runtime that will stop receiving security patches.
Before running composer update, confirm your PHP version:
php -v
# Should show PHP 8.3.x or higher
If you are using Laravel Boost (laravel/boost ^2.0), the /upgrade-laravel-v13 command inside Claude Code, Cursor, or VS Code automates most of the mechanical upgrade steps. You still need to manually audit config/cache.php (the serializable_classes default changed to false) and your CSRF configuration.
Testing the Features You Adopt
-
PHP Attributes: Run
php artisan model:show Postto confirm the model resolves its table and fillable rules correctly after converting to attribute syntax. -
Cache::touch(): Write a unit test that asserts the TTL changes without the value changing. Use
Cache::fake()and inspect the underlying store. -
Typed config: Force a type mismatch in your local
.envand confirm theConfigTypeMismatchExceptionis thrown at boot, not silently ignored. -
Queue inspection: In a local Horizon or queue worker setup, dispatch a delayed job and call
Queue::allDelayedJobs()from Tinker to verify it appears. - AI SDK: Test against the real provider API in a feature test with a real but low-cost call (e.g., a short embedding), and mock the facade in unit tests.
If you need Laravel development in Mumbai, Mumbai Web Designer builds production-grade Laravel applications.
Top comments (0)