Laracon 2024: Taylor Otwell: Laravel Keynote

Taylor’s keynote is always the highlight of the conference, and this year was no exception.

Here’s the video; below are a few of the highlights and what I like about them.

First-Party VS Code Extension

Coming later this fall is a new VS Code Extension developed and maintained by Laravel.

It will offer autocomplete for a number of features (config and env values, app services, translations, views, Inertia props, and Eloquent), along with click-through links and preview-on-hover.

Probably my favorite feature is the Test Explorer integration: whether you’re using PHPUnit or Pest, your tests will show up in the sidebar, where you can run and see the status; you can also see test failure details in a “peek” UI widget.

This sounds like it will replace several of the extensions I currently use, and I’m excited to have first-party support for Laravel development in VS Code.

Container Attributes

A Container Attributes feature feels like dependency injection for config values or other attributes. This doesn’t seem like a huge feature, but will reduce some boilerplate code.

Chaperone Method

The just-released ->chaperone() method helps prevent n+1 queries when you need to do something like retrieve authors with their posts and then access something on the author model for each post.

I know I’ve run into this before, doing something like this to prevent the n+1 queries: $authors->with('posts.author');

defer() helper and Cache::flexible()

This looks very promising. It’s a simple way to push some work to the background, running it after the server has responded to the incoming request. I can immediately think of several apps where I have a super-simple job just to run something asynchronously after a request, and this can replace those.

Some potential use cases are for sending analytics, notifying third-party services, or any other “fire-and-forget” interaction.

This also enables a new Cache::flexible() method, providing a stale-while-revalidate mechanism to reduce the number of requests that hit a cold cache.

You provide two TTL numbers to this method:

  • The standard TTL indicating how long the cached value is valid
  • A second TTL indicating how long it’s acceptable to provide the stale value, while refreshing the value in the background so it’s ready for the next request

I’m very excited for this, as it will help improve several pain points in a couple of current applications.

Concurrency Facade

If some code needs to run multiple slow processes that don’t depend on each other, the new Concurrency facade will be helpful.

Concurrency::run([]) can be used to run multiple requests/jobs/etc. in parallel, returning the values for subsequent use; or Concurrency::defer([]) can run multiple processes in parallel after the current request, using the new defer() helper.

Inertia 2.0

Most of my apps are Livewire, but these features look amazing and I’ll definitely be using them:

  • Async requests: currently, Inertia runs only one request at a time; this allows multiple requests
  • Polling: I’ve manually written polling in several different components, and this will simplify and standardize those
  • WhenVisible: this will wait to load props until the user scrolls to a portion of the page that needs them, and then it loads each prop as it needs. I’ve almost written some code to do this, but it seemed too complex for the benefit, so found another solution instead. Having this available as a first-party solution will add definite performance improvements.
  • Infinite scrolling: not anything I’ve needed yet, but might be useful at some point
  • Prefetching: this stands to improve performance by optimistically loading data so it’s ready as soon as a user visits another page
  • Deferred props: another strategy for improving performance by waiting to load specific props until after the initial page has been rendered

Laravel Cloud

The big announcement: Laravel Cloud is a fully-managed infrastructure platform built specifically for hosting Laravel apps as simply as possible.

Taylor demonstrated the process of adding a new application, providing a repository, and deploying, all within 25 seconds!

It provides scalability both up and down; when not in use, it can be set to hibernate to save costs.

It allows you to create deployments from different branches, enabling an easy way to preview code in different branches before going live. (This is maybe my favorite feature.)

In the talk, he mentioned PostgreSQL as a serverless database option; later in a podcast interview with Matt Stauffer, he mentioned that MySQL is in the works for launch, but wasn’t quite complete in time for the talk.

Laravel Cloud will support creating multiple worker instances (separate from web instances) for handling queues.

It will provide SSL certificates and firewall using Cloudflare. He didn’t mention it in the talk, but in the podcast he did mention that Laravel Cloud runs on AWS, and another conference attendee said that Taylor told him it used Kubernetes.

Costs and Features

For sandbox:

  • No monthly fee
  • Compute: less than 1¢ per hour
  • Serverless Postgres: from 4¢ per hour plus 75¢ per GB
  • laravel.cloud domains included free

For production:

  • Costs not announced
  • Auto-scaling compute
  • Larger instance sizes
  • Custom domains included

Observability and debugging: wait until Laracon Australia!

Overall, Laravel Cloud seems like it dramatically lowers the barrier to entry for new developers, side projects, or anyone or any project that just wants to get something up and running with a low barrier to entry. I’m excited to see how this is going to change the ecosystem, as it makes it easier for people to focus on just building and shipping software.

Laracon 2024: Daniel Coulbourne: Verbs for Laravel

Daniel Coulbourne presented a very enlightening talk about Verbs, an event sourcing package that has been in the works for a while.

Earlier in the day, he and John Rudolph Drexler were walking around the conference with a ziploc bag containing $1,500 cash, signing up people to play their pyramid scheme game: the more people you recruited, the more votes you received.

During the talk, Daniel showed the leaderboard on screen and realized that at least two players had entered a single bonus code multiple times, gaining extra votes. He then merged a PR fixing the bug for future entries.

However, in a “normal” app storing the current state for each user, you would be hard-pressed to figure out how to remove the illegitimate points, while retaining legitimate ones.

That’s the beauty of event sourcing: since each vote and bonus code was stored an event, he was able to wipe out the database and re-run the events to determine the correct vote tallies.

(And of course, since he was doing this live, he accidentally wiped out the migrations table too and had to restore it…)

Once he re-processed the events, the leaderboard showed correct results, removing the extra votes from those users.

This was a powerful example of how event sourcing can be to prevent, catch, or clean up from certain types of bugs.

His agency, thunk.dev, also wrote up this blog post about the game overall.

Laracon 2024: Colin DeCarlo: Laravel and AI

Colin DeCarlo presented two AI-integrated applications that Vehikl recently built.

He went into some background about how LLMs work under the hood converting text to embeddings, and how to use embeddings in an Laravel app.

Documentation Helper

ai.vehikl.com is a documentation helper that Vehikl built for Laracon 2023, trained on Laravel, Vue, and Winter documentation.

They configured the LLM queries with a temperature of 0, instructing the model to not make up information when it doesn’t know the answer.

Chatbot

They also recently created a chat app providing a custom tools for the AI to use:

  • The app has an API integration with a weather API
  • When handling a chat request about the weather for an activity this evening, the app passes the user’s message to ChatGPT, along with a list of custom tools the app provides
  • ChatGPT interprets the request, realizes that it needs a custom tool to get the weather data, and then responds to the app with the name of the tool and the parameters to pass to it
  • The app calls the weather service, then returns that data back to ChatGPT along with a request or correlation ID (I didn’t take good notes on this part)
  • ChatGPT interprets the weather data and then writes a more natural-language chat message to send back to the user

I haven’t dived into LLMs much yet, but this approach of using custom tools seems like a pretty nice way of integrating other, more programmatic services along with AI tools.

Laracon 2024: Mateus Guimaraes: Behind Laravel Octane

Mateus Guimaraes brought a deep-dive through how Laravel Octane can massively improve performance of your apps.

Main benefits:

  • Reduced latency by eliminating the framework boot step on every request
  • Increased performance
  • Lower cost by reducing CPU usage

Aaron Francis asked a followup question about which driver is best:

None of the apps I’m currently working on need this level of performance (yet) but I’d be interested to try Octane to see how it could improve performance even now.

One more note: Octane can run multiple processes concurrently to save time during a request:

<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Laravel\Octane\Facades\Octane;

Route:: get('foo', function () {
    Octane::concurrently([
        fn () => DB::select('SELECT * WHERE SLEEP(1)'),
        fn () => DB::select('SELECT * WHERE SLEEP(1)'),
        fn () => DB::select('SELECT * WHERE SLEEP(1)'),
    ]);

    return ['foo' => 'bar'];
});

Laracon 2024: Philo Hermans: Livewire Beyond the Basics

Philo Hermans went deep into Livewire optimizations in his talk. A couple of key takeaways:

If your Livewire component calls an action that doesn’t need to re-render anything, you can skip re-rendering by using the [Renderless] attribute. I think I can find some immediate use cases for this.

For high-traffic apps with read replica databases, the sticky option can help guarantee consistency immediately after writes.

Optimistic UIs can improve perceived efficiency: use wire:loading.remove to immediately remove an element before the server round-trip has completed, so the app feels more snappy. I think I could also improve some UIs using this trick.

Laracon 2024: Luke Downing: Lessons From the Framework

Luke Downing gave a great talk about lessons you can learn from the framework.

He took us line-by-line through routes/web.php (from the app structure prior to Laravel 11), explaining all the things you can learn from that file:

  • Authorization: the app has users, allows registration and login, etc. and uses one of the Laravel authentication starter kits
  • Model relationships: nested resource routes indicate that other models belong to users, and while the public can view models, only users can edit them
  • and more

I’ve found the routes files to be extremely helpful in my day-to-day development. In fact, I usually add the routes file as a pinned tab in VS Code so it’s always open (depending on the app, sometimes web.php, sometimes api.php, and sometimes both).

Some quotes from the talk:

Ordinary and obvious, not clever and convoluted.

The more obvious and clear your code is, the easier it is to spot and prevent bugs, and the simpler it is to come back to it later.

Know the rules and when to break them.

e.g., using facades for simpler testing, instead of the “right way” of dependency injection

Laracon 2024: Nuno Maduro: Pest 3

In his talk, Nuno Maduro announced Pest v3, with several new features and no breaking changes.

I know this is pretty exciting to a lot of people, but personally I prefer PHPUnit for several reasons:

  • Better IDE support
    • e.g., in a Pest test, $this->assertDatabaseHas(…) runs without errors, but the IDE thinks it’s invalid because the test file doesn’t extend the base TestCase or inherit any of the parent methods
    • I personally like the discoverability of using a class, as my IDE will suggest available methods.
    • I do think there are some Pest helpers that I haven’t fully used, so those might help alleviate these gripes.
    • Yes, I know that Pest can be used as the test runner, and still use PHPUnit-style classes.
  • Improved VS Code test integration
    • This is the best VS Code extension I’ve found to work with tests in a PHP project. It shows all the tests along with pass/fail results in the sidebar, and I use the keyboard shortcut to run tests all the time.
    • The extension only works with PHPUnit tests, not Pest.
    • However, the upcoming first-party Laravel VS Code extension might change that…

These new features did catch my eye though:

  • Architecture presets: ensure your code follows best practices and conventions; read more here and see the plugin here
  • Mutation testing: let Pest modify parts of your code to ensure it causes failing tests, to ensure your tests are covering what you think they are; see the plugin here

I’ll be taking a look to see if I can use some of the underlying packages with PHPUnit.

All in all, seems like a solid upgrade, but it just doesn’t excite me all that much.

Easily Deduplicate Jobs Using Laravel Queues

TL:DR; use a delayed dispatch plus the ShouldBeUnique contract on your job, and let the framework deduplicate it for you!

Picture this scenario: you receive webhooks from a third-party service that you need to handle somehow in your application. Due to multiple changes at the third party, you may receive multiple webhooks about the same resource within a short timeframe (2 minutes, for example), but you only want to process it a single time.

You could add a processed_at timestamp and track when you last processed the resource, and bypass additional processing using that.

Or you could combine several tools the Laravel framework already provides:

  1. Determine the expected timeframe for multiple updates
    • e.g., if somebody is manually updating multiple fields and you get a webhook for each change, estimate how long a user might be working before you want to process the changes
  2. Add a delay when you dispatch the job to cover that expected timeframe, plus a little bit extra: SomeJob::dispatch($data)->delay(now()->addMinutes(3))
  3. Add the ShouldBeUnique interface to your job
  4. Add the uniqueId() method to your job with a unique ID or some other key that will be used to find a match
  5. Voila!

Now when you receive an incoming webhook, your app will delay the processing for 3 minutes, and if there is already a job on the queue for that unique ID, the framework will not dispatch a second job.

One additional thing to consider: within that job, you may wish to perform an API call to retrieve the current state of the resource, since the dispatched job will contain the state as of the first webhook, not the most recent.

<?php

// somewhere in your app

SomeJob::dispatch($data)->delay(
    now()->addMinutes(3)
);
<?php
 
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
 
class SomeJob implements ShouldQueue, ShouldBeUnique
{
    /**
     * Get the unique ID for the job.
     */
    public function uniqueId(): string
    {
        return $this->data->id;
    }

    ...
}

Fatal Errors with PHPUnit Test Suite: Laravel 10, PHPUnit 10, ParaTest 7

TL;DR: PHPUnit 10.5.32 is broken, 10.5.31 works.

After a composer update one one of my projects today, I started getting this error when running my test suite: Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable.

All the tests passed just fine…no errors or warnings. At the end of the test execution output, it displayed this stacktrace:

> php artisan test --parallel --stop-on-failure --stop-on-error
ParaTest v7.4.5 upon PHPUnit 10.5.32 by Sebastian Bergmann and contributors.

Processes:     10
Runtime:       PHP 8.3.10
Configuration: /Users/andrewminion/Sites/site-domain/phpunit.xml

.......................................................SS....   61 / 1014 (  6%)
.............................................................  122 / 1014 ( 12%)
.............................................................  183 / 1014 ( 18%)
.............................................................  244 / 1014 ( 24%)
.............................................................  305 / 1014 ( 30%)
.............................................................  366 / 1014 ( 36%)
..................................S...I......................  427 / 1014 ( 42%)
.............................................................  488 / 1014 ( 48%)
.............................................................  549 / 1014 ( 54%)
.............................................................  610 / 1014 ( 60%)
.............................................................  671 / 1014 ( 66%)
.............................................................  732 / 1014 ( 72%)
.............................................................  793 / 1014 ( 78%)
.............................................................  854 / 1014 ( 84%)
.............................................................  915 / 1014 ( 90%)
.........................I...................................  976 / 1014 ( 96%)
......................................                        1014 / 1014 (100%)

Time: 02:04.815, Memory: 70.50 MB

OK, but there were issues!
Tests: 1014, Assertions: 6462, Skipped: 3, Incomplete: 2.

Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Call Stack:
  125.2423   77506896   1. Illuminate\Foundation\Bootstrap\HandleExceptions->Illuminate\Foundation\Bootstrap\{closure:/Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:254-256}() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:0
  125.2423   77507336   2. Illuminate\Foundation\Bootstrap\HandleExceptions->handleException() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:255
  125.2423   77470472   3. Illuminate\Foundation\Bootstrap\HandleExceptions->getExceptionHandler() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:183
  125.2423   77470472   4. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:288
  125.2423   77470472   5. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:946
  125.2423   77470472   6. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:731
  125.2423   77470472   7. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:961
  125.2423   77470528   8. Illuminate\Foundation\Application->build() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
  125.2423   77470624   9. Illuminate\Foundation\Application->notInstantiable() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:921


Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Call Stack:
  125.2457   77507224   1. Illuminate\Foundation\Bootstrap\HandleExceptions->Illuminate\Foundation\Bootstrap\{closure:/Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:254-256}() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:0
  125.2457   77507448   2. Illuminate\Foundation\Bootstrap\HandleExceptions->handleShutdown() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:255
  125.2460   77519232   3. Illuminate\Foundation\Bootstrap\HandleExceptions->handleException() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:231
  125.2460   77519232   4. Illuminate\Foundation\Bootstrap\HandleExceptions->getExceptionHandler() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:183
  125.2460   77519232   5. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:288
  125.2460   77519232   6. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:946
  125.2460   77519232   7. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:731
  125.2460   77519232   8. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:961
  125.2460   77519232   9. Illuminate\Foundation\Application->build() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
  125.2460   77519328  10. Illuminate\Foundation\Application->notInstantiable() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:921

After some trial-and-error, I narrowed it down to PHPUnit 10.5.32. Something in the changeset from 10.5.31 to 10.5.32 caused this error.

This also happens only when running tests in parallel (php artisan test --parallel), suggesting to me that perhaps the PHPUnit output changed in some way that ParaTest was not expecting.

My fix, at least for now, is to stick with PHPUnit 10.5.31, and hopefully an imminent upgrade to Laravel 11 and PHPUnit 11 will resolve it.

Laravel Testing Tip: Reset UUID Creation

TL;DR: if you call Str::createUuidsUsing(…) in a test method, don’t forget to call Str::createUuidsNormally() later in that same method (or the tearDown method), or the rest of your test suite will continue to use that same UUID.

Laravel’s string helper provides a nice interface for generating uuids, as well as a nice way to fake the UUIDs during a test:

While this is very useful, I expected that it would reset between tests, similar to Queue::fake(), Http::fake(), etc.

However, because of how the Str helper generates UUIDs, whatever you provide will be used for the rest of the test run. If you have other tests or app code that expects a unique UUID each time Str::uuid() is used, you may get unexpected results.

There are a couple of options to work around this:

  1. After running the code that needs a UUID, call Str::createUuidsNormally() to reset the Str helper.
  2. If you don’t actually need the value of the UUID for testing, you can wrap your code in the freezeUuids() method instead. Once your code in the callback finishes running, the framework will call createUuidsNormally() to reset everything for you: