What Actually Changed in Laravel 12
Laravel 12 is a focused release rather than a sweeping rewrite. The headline additions are typed configuration objects, a significantly slimmer default application skeleton, and a handful of helper and collection improvements. If you are running Laravel 11 on PHP 8.2+, the upgrade path is straightforward — but a few structural decisions deserve deliberate attention.
Typed Configuration Objects
The most architecturally interesting addition is first-class support for binding typed config objects into the service container. Instead of scattering config('services.stripe.key') calls throughout your codebase, you can now define a plain PHP object and register it once.
// app/Config/StripeConfig.php
readonly class StripeConfig
{
public function __construct(
public string $key,
public string $webhookSecret,
public bool $testMode,
) {}
}
// AppServiceProvider::register()
$this->app->singleton(StripeConfig::class, fn () => new StripeConfig(
key: config('services.stripe.key'),
webhookSecret: config('services.stripe.webhook_secret'),
testMode: (bool) config('services.stripe.test_mode', false),
));
Now any class that type-hints StripeConfig receives a fully resolved, IDE-friendly object — no more string-keyed lookups, no more silent null surprises when a key is missing.
Testing Becomes Trivial
it('charges the card in test mode', function () {
$this->app->instance(StripeConfig::class, new StripeConfig(
key: 'sk_test_fake',
webhookSecret: 'whsec_fake',
testMode: true,
));
// act and assert...
});
No more Config::set() scattered across test files.
The Slimmer Skeleton
Laravel 12 ships with far fewer stub files in a fresh laravel new project. Several files that previously lived in app/Http/Middleware/ are now consolidated or removed entirely, with their logic folded into the framework itself.
What is gone from the default skeleton:
TrimStrings,ConvertEmptyStringsToNull, andPreventRequestsDuringMaintenancemiddleware stubs — they still run, but you no longer own copies unless you publish them.- The
app/Http/Kernel.phpfile is absent; middleware registration lives inbootstrap/app.php(introduced in Laravel 11, now the only way).
If your upgrade path involves a legacy Kernel.php, the migration is mechanical:
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\MyCustomMiddleware::class);
$middleware->remove(\Illuminate\Http\Middleware\TrimStrings::class);
})
->create();
Collection and Helper Additions
collect()->intersectAssoc()
A long-requested method that compares both keys and values:
$a = collect(['color' => 'red', 'size' => 'M']);
$b = collect(['color' => 'red', 'size' => 'L']);
$a->intersectAssoc($b); // ['color' => 'red']
Str::chopStart() / Str::chopEnd()
Clean alternatives to ltrim/rtrim for specific substrings:
Str::chopStart('/api/v1/users', '/api'); // '/v1/users'
Str::chopEnd('report.csv.tmp', '.tmp'); // 'report.csv'
Upgrade Notes Worth Acting On
- PHP minimum is 8.2. Drop any
7.x/8.0/8.1compatibility shims. Model::preventLazyLoading()is on by default inlocal. Audit your resources and Nova/Filament panels for N+1s before upgrading.assertJsonPathnow validates strict types. Tests that previously passed with loose comparisons may fail — fix the assertions, not the strictness.- Vite 6 is the default. If you pinned
viteinpackage.json, bump it and review yourvite.config.jsfor deprecated plugin options.
Key Takeaways
- Typed config objects eliminate string-keyed
config()calls and make test overrides explicit. - The slimmer skeleton reduces noise; understand which middleware moved into the framework before upgrading.
Str::chopStart/chopEndandintersectAssocare small but immediately useful.preventLazyLoadingbeing on by default in local is a gift — let it surface your N+1s now.- The upgrade from Laravel 11 is low-risk if you already adopted
bootstrap/app.phpmiddleware registration.