Laravel 13: New Features, Helpers, and Practical Upgrade Notes
#laravel #laravel-13 #php #upgrade

Laravel 13: New Features, Helpers, and Practical Upgrade Notes

4 min read Mohamed Said Mohamed Said

What Laravel 13 Actually Changes

Laravel 13 is a focused release. Rather than a sweeping API redesign, it tightens the framework's internals, promotes PHP 8.3 features to first-class status, and ships a handful of helpers that remove common boilerplate. The upgrade path from Laravel 12 is intentionally narrow — most apps need only a few targeted changes.


Bootstrapping and Application Lifecycle

The Application bootstrap sequence now exposes a dedicated booting / booted hook pair directly on the application instance, giving you a cleaner place to register deferred work without abusing service providers.

// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php')
    ->booting(function (Application $app) {
        // Runs before any provider boot() is called.
        $app->bind(ReportFormatter::class, JsonReportFormatter::class);
    })
    ->booted(function (Application $app) {
        // All providers are booted; safe to resolve.
        $app->make(MetricsCollector::class)->register();
    })
    ->create();

This replaces the pattern of creating a throwaway service provider just to hook into the lifecycle.


New Helpers Worth Knowing

rescue_unless

The inverse of rescue(). It runs the callback only when the condition is falsy, swallowing exceptions and returning a default — useful for optional third-party calls:

$price = rescue_unless(
    condition: $this->cache->has('price:'.$sku),
    callback: fn () => $this->pricingApi->fetch($sku),
    rescue: null
);

str()->toBase64url() and fromBase64url()

URL-safe Base64 encoding lands on the Stringable fluent API, removing the need for manual strtr gymnastics:

$token = str($payload)->toBase64url();
$decoded = str($token)->fromBase64url();

Collection::groupByMany()

Groups a collection by multiple keys in a single pass, returning a nested structure:

$grouped = $orders->groupByMany(['region', 'status']);
// $grouped['EU']['pending'] => Collection of orders

PHP 8.3 Integrations

Laravel 13 requires PHP 8.2 minimum and is optimised for 8.3. Typed class constants are now used throughout the framework's own codebase, and the json_validate() built-in replaces several internal json_decode + error-check patterns. Your own code can follow the same idiom:

// Before
$valid = json_decode($raw, true) !== null && json_last_error() === JSON_ERROR_NONE;

// After (PHP 8.3)
$valid = json_validate($raw);

Readonly class promotion is also used in new stubs, so generated DTOs and form requests lean on readonly properties by default.


Breaking Changes to Watch

Illuminate\Http\Client\Response::json() strict mode

Passing an invalid JSON body no longer silently returns null; it throws JsonException. Wrap legacy HTTP client calls:

try {
    $data = Http::get($url)->json();
} catch (\JsonException $e) {
    Log::warning('Malformed API response', ['url' => $url]);
    $data = [];
}

Removed: $loop variable in Blade @each

The $loop magic variable inside @each directives was deprecated in Laravel 12 and is removed in 13. Replace @each with @foreach to retain loop metadata.

Queue after_commit default

The after_commit flag on dispatched jobs now defaults to true when a database transaction is active. If you rely on jobs firing mid-transaction for testing or side-effects, set public bool $afterCommit = false explicitly on the job class.


Upgrade Checklist

  • Run composer require laravel/framework:^13.0 and fix any immediate constraint failures.
  • Search for @each in Blade templates and convert to @foreach.
  • Audit HTTP client calls that parse JSON — add JsonException handling.
  • Review jobs that dispatch inside transactions; verify $afterCommit intent.
  • Update phpunit.xml to the Laravel 13 stub if you are on PHPUnit 11.
  • Run php artisan config:clear && php artisan cache:clear after deployment.

Key Takeaways

  • The new booting / booted hooks on Application reduce throwaway service providers.
  • rescue_unless, toBase64url, and groupByMany cover real day-to-day gaps.
  • Http::json() now throws on bad JSON — handle it explicitly.
  • @each is gone; after_commit defaults changed for queued jobs.
  • PHP 8.3 is the sweet spot; json_validate() and readonly properties are first-class.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Is the upgrade from Laravel 12 to 13 disruptive?
For most apps, no. The main breaking changes are the Http client throwing JsonException on bad JSON, the removal of $loop inside @each, and the changed after_commit default for jobs. A focused audit of those three areas covers the majority of upgrade work.
Q02 Does Laravel 13 require PHP 8.3?
No — the minimum is PHP 8.2. However, the framework is optimised for 8.3 and uses features like json_validate() and typed class constants internally. Running 8.3 gives you the best performance and the cleanest code alignment with the new stubs.
Q03 What replaces the @each directive now that it is removed?
Use a standard @foreach loop. The @each directive was a convenience wrapper that never supported the full $loop variable reliably. Switching to @foreach gives you $loop, better IDE support, and clearer template logic.

Continue reading

More Articles

View all