Laravel 11, Pennant, and Conditional Scheduled Jobs

TL;DR: pass a function instead of Feature::active(…) to your console jobs’ ->when(…) method.

I recently upgraded an app from Laravel 10 to Laravel 11 (I know, I know…I’m a few months behind).

This app was using Laravel Pennant to conditionally register some jobs, based on feature flags:

Schedule::job(GetNewOrders::class)
    ->when(Feature::active(GetOrders::class))
    ->everyFiveMinutes();

Laravel Shift moved these jobs from app/Console/Kernel.php to routes/console.php as expected for the new Laravel 11 structure.

However, for every test that I ran, I was getting these errors:

Base table or view not found: 1146 Table 'test_database.features' doesn't exist

I spent a bit of time troubleshooting and verifying that the migration existed, the database schema was squashed, etc. I was expecting it to fail during the setUpTraits() step of booting the framework, but it actually failed while booting the application. I stepped through more of the setup steps (thanks, xdebug!) and realized that it failed while discovering commands.

That prompted me to comment out the ->when(Feature::active(…)) lines, and voila! my tests suddenly worked!

The when() method accepts either a boolean or a Closure, so I tried wrapping the feature flag in a closure, and my tests still worked:

Schedule::job(GetNewOrders::class)
    ->when(fn () => Feature::active(GetOrders::class))
    ->everyFiveMinutes();

It appears that if your scheduled job ->when(…) conditions depend on the database, you’ll want to wrap them in a function so they aren’t evaluated until they’re actually needed, after the database has already been set up.

Using Heredoc Syntax for Improved Syntax Highlighting

If you find yourself writing PHP code containing other languages (JS snippets, SQL queries, etc.), you can use PHP’s Heredoc syntax for improved syntax highlighting, at least in VS Code.

Basically, Heredoc syntax uses <<< to note the beginning of a string, followed by an identifier. Everything until the next instance of that identifier is treated as a string.

Here are examples:

You may not get autocompletion and other IDE features, but at least it’s easier to read.

What you choose for the identifier does impact the syntax highlighting…for example, if I had used SQL instead of JS in the example above, the code would have been highlighted differently. I suspect this is a feature of ProseMirror, though I haven’t dug into it to find out for sure.

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.

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;
    }

    ...
}

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:

Laravel Tip: Generating Signed URLs with Ignored Parameters

TL;DR: don’t use ignored URL parameters when building signed URLs or the resulting signed URL will be invalid. Instead, manually append them to the resulting URL.

Laravel includes some really nice helpers for building signed URLs: https://laravel.com/docs/master/urls#signed-urls

They allow you to generate a URL containing a signature that prevents anybody from modifying the URL to access something you didn’t intend (e.g., you could provide a signed URL for a specific post with ID 123; if somebody changed that ID to 124, then Laravel will display a 403 Signature Invalid error rather than happily displaying post 124).

Occasionally you may wish to ignore certain URL parameters when validating the signature (e.g., a pagination or print parameter).

In this case, you cannot include the ignored parameter when generating the signed URL, or the URL will be invalid.

Here’s an example. This route ignores the print parameter when verifying the signature:

If you generate a signed URL without the print parameter, it will be valid. But if you include print in the URL parameters for the helper method, the resulting signature will be invalid, because Laravel uses all of those parameters to generate the signature. Instead, just add the new parameter to the end of the resulting URL:

Note how examples 1 and 3 have the same signature; that is the signature that Laravel calculates when determining what the correct signature should be to verify that the URL has not been modified. The example 2 use print=true when generating the signature, but will remove that parameter when verifying the signature, so they don’t match.

Update: I submitted a PR to the framework to pass ignored parameters to the signed route methods to make this easier.