Upgrading Medley to Laravel 11

March 14, 2024

On the day, I upgraded Medley to Laravel 11 using Laravel Shift. almost perfectly smooth, it missed custom routing and my events, but nothing that I couldn't fix myself.

By the way, you can check if you can upgrade your Laravel application here! Can I Upgrade Laravel. Useful before you check out a Laravel Shift!

Alright, let's talk about some of the changes that caught my eye in Laravel 11.

Slimmed down configuration

L11 now ships with smaller config files. Basically, you can remove configs you don't need from your config and Laravel will do a recursive merge to provide those defaults.

Here's what my config/filesystems.php look like now.

return [
'disks' => [
'r2' => [
'driver' => 's3',
'key' => env('CLOUDFLARE_R2_ACCESS_KEY_ID'),
'region' => env('CLOUDFLARE_R2_DEFAULT_REGION'),
'bucket' => env('CLOUDFLARE_R2_BUCKET'),
'url' => env('CLOUDFLARE_R2_URL'),
'cdn_url' => env('CLOUDFLARE_R2_CDN_URL'),
'endpoint' => env('CLOUDFLARE_R2_ENDPOINT'),
'use_path_style_endpoint' => env('CLOUDFLARE_R2_USE_PATH_STYLE_ENDPOINT', false),
'visibility' => 'public-read',

I was able to remove anything Laravel provided by default and only add the custom ones I need.

I like this change a lot

I think one of the most tedious things when manually upgrading Laravel projects prior to 11 was keeping config files up to date. Especially the ones I don't end up using.

I jump between different projects a lot for my work. Configs for L11+ projects will now be "first stops" for myself to get a temperature check of the project.

I will miss just going around config files seeing what the other offerings are within the core though. I know I can always publish the config files via php artisan config:publish but when I'm working on my own projects, it's sometimes nice to just read around and discover something new.

Over all, I love this change! Makes maintenance and upgrades less of a slog.

Things I haven't tested or I'm assuming

  • I hope the package config files function the same. I would love to just only have package configs that I want to customize rather than having a lot of things I'll never touch. I'll test this out.


In L11, instead of having fragmented configuration between config/app.php and different Service Providers, a lot the configuration layer is moved to the bootstrap/app.php.

The bootstrap file is now where you configure:

  • Custom exception handling
  • Routing
  • Middleware
  • Events

I think this is over all a great change. It makes it another one of the first stops to get a feel of the application.

Because of this change, new L11 applications come with only a single AppServiceProvider.

My knee-jerk reaction to this was a bit negative since potentially bootstrap/app.php and AppServiceProvider could become huge files if I'm not too careful.

How it looks right now in Medley

For context, Medley is a medium sized application. It has about 246 endpoints and counting.

For this upgrade, I decided to lean in for now on L11 changes and let everything happen in AppServiceProvider and bootstrap/app.php.

Even with all that, AppServiceProvider is only 187 lines long and bootstrap/app.php is only 239 lines long. It's not a lot for a medium sized application!

I'm personally gonna move middleware registration and route registration to their own service provider though since that's the majority of the bloat of my app.php.

That's what I like with these updates in Laravel. I've been using Laravel since 3, professionally since 4.2 and a lot of the practices since 5.0, I can still do myself. The changes are just syntactic sugar.

Improved encryption key rotation!

This was the one I'm most excited about! I've personally been digging into Rails' DX on how they're handling key rotation to build a solution for Medley.

Then Laravel 11 ships with it!

For context, all of Medley's data is encrypted, so I've been struggling to figure out how to perform regular key rotations without having to do it when customers are asleep or taking the site down.

With the latest update, Laravel allows fallback app keys to attempt to use previous keys and re-encrypt data with the new cipher text.

It's an amazing improvement!

A couple of things I'm a bit unfamiliar with

To be clear, these things can either be unfamiliarity, not reading the docs, misunderstanding things, or lack of documentation. Nothing that can't be improved by learning a bit more, or opening a PR to the docs.

Exception Handling

After the upgrade, this was the change I had the most friction with. It took around 15 - 20 minutes so it wasn't too much of a head-scratcher, but I did have to source dive to figure stuff out.

Previously, projects came with a Exceptions/Handler.php file out of the box where you can customize how your project reacts based on specific exceptions.

Here are the custom exceptions I handle in Medley:

  • 419 errors should just redirect back and flash an error message for a toast message.
  • 403 and 503 errors are custom. I want them to use Inertia views so I can have better looking error pages.

With the new API, I couldn't find in the docs how the Request object was provided. It took a few minutes of source digging to see it's a third parameter on the $exceptions->respond() function.

Here's how my exception handling looks in the new L11 API.

->withExceptions(function (Exceptions $exceptions) {
$exceptions->respond(function (Response $response, Throwable $e, Request $request) {
if ($response->getStatusCode() === 419) {
'title' => __('errors.session-expired.title'),
'caption' => __('errors.session-expired.caption'),
return redirect()->back();
if ($request->isMethod('GET') && $response->getStatusCode() === 403) {
return inertia('errors/403')->toResponse($request)->setStatusCode(403);
if ($request->isMethod('GET') && $response->getStatusCode() === 503) {
return inertia('errors/503')->toResponse($request)->setStatusCode(503);
return $response;

It's nothing too different from the previous API aside from the source diving.

Event Handling

Now this one I think I just need to play around with the "new" events API. I've never liked event discovery so I've kept that off ever since been introduced.

So with the new API, I had to manually register all my event bindings via Event::listen() in my AppServiceProvider.

private function registerEventListeners(): void
Event::listen(HeartbeatAnswered::class, NotifyRecordSubscribers::class);
Event::listen(PostPublished::class, NotifyRecordSubscribers::class);
Event::listen(NodeCreated::class, NotifyRecordSubscribers::class);
Event::listen(StatusUpdateCreated::class, NotifyRecordSubscribers::class);
Event::listen(CommentCreated::class, NotifyRecordSubscribers::class);
Event::listen(TaskStageChanged::class, NotifyRecordSubscribers::class);
Event::listen(UserCreated::class, DownloadUserAvatar::class);
// ...

I don't quite like this, but I think I'm just doing something wrong here. 🤔

bootstrap/providers.php vs ->withProviders()

The L11 docs say any user-defined providers should be registered in bootstrap/providers.php. Although, Larvel Shift did add my custom providers to bootstrap/app.php via ->withProviders().

My assumption here is I'll just use bootstrap/providers.php but they're functionally the same.

I don't have anything too custom to have any motivation to dig further though!


It's overall an easy update! Aside from the exception handling and a named parameter change in Carbon 3, there wasn't any extreme changes that was bothersome.

I love the improvement in encryption. Making upgrades easier for future versions.

Hope you all have a smooth upgrade process too!