TL;DR
- Shipped an email broadcast module: audience filtering, suppression, queued per-recipient sends, delivery tracking.
- Fixed a silent implicit-route-binding bug — the route param name didn't match the method parameter, so Laravel injected an empty model.
- Lesson of the day: Laravel's implicit binding fails quietly, not loudly.
Email broadcasts
The bulk of today. A campaign resolves an audience filter into a contact query, passes it through a suppression gate (unsubscribed, do-not-contact, no email), materialises one recipient row per contact, then dispatches one queued job per row.
The design principle: the recipient row is the source of truth. Every job re-checks the row's status before sending, so a retry or a double dispatch is a no-op. No locks, no dedupe table.
Delivery webhooks sync back onto the same rows with a rank guard (sent < delivered < opened < clicked) so an out-of-order event never walks a status backwards. Terminal states — bounced, unsubscribed, failed — are handled separately.
Full write-up in the focused post.
The route binding bug that says nothing
A show page blew up with Call to a member function label() on null. The cause was two names that had drifted apart:
| Before | After | |
|---|---|---|
| Route param | {infraProvider} |
{provider} |
| Mount signature | mount(InfraProvider $provider) |
mount(InfraProvider $provider) |
| Result | empty model injected | correct model |
// route
Route::get('/{provider}', ProviderShow::class)->name('show');
// component
public function mount(InfraProvider $provider): void { /* ... */ }
Here's the part worth remembering: Laravel matches implicit route-model bindings by name. The URI segment {infraProvider} has to match the variable $provider. When it doesn't, you don't get an exception — you get a freshly instantiated, empty model. id is 0, every attribute is null, and the failure surfaces three layers later when something calls a method on a null enum.
It's a latent bug. The page had never been opened with real data before, so nothing had noticed. Seeding the first record surfaced it immediately.
The fix was renaming the param to match the sibling resource routes ({project}, {team}). Consistency across a route file is the cheap defence here — if every route uses the singular model name, the mismatch never happens.
it('resolves the provider on the show route', function () {
$provider = InfraProvider::factory()->create();
get(route('providers.show', $provider))
->assertOk()
->assertSee($provider->name);
});
One test hitting the real route would have caught it. Testing the component in isolation would not — the binding only happens when the router does the work.
Seeding as a smoke test
The other commit was an idempotent, config-driven seeder that provisions a realistic baseline environment rather than dummy rows. It earned its keep the same afternoon — it produced the first real record on a page nobody had exercised, which is how the binding bug surfaced. A good seeder is a smoke test that leaves useful data behind.
Takeaway
Failures that return a valid-looking empty object are worse than exceptions. Convention — matching names, consistent route params — is what keeps them from happening in the first place.
Top comments (0)