Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Illuminate/Notifications/SendQueuedNotifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class SendQueuedNotifications implements ShouldQueue
*/
public $shouldBeEncrypted = false;

/**
* Indicates if the job should be deleted when models are missing.
*
* @var bool
*/
public $deleteWhenMissingModels = false;

/**
* Create a new job instance.
*
Expand All @@ -81,6 +88,10 @@ public function __construct($notifiables, $notification, ?array $channels = null
$this->timeout = property_exists($notification, 'timeout') ? $notification->timeout : null;
$this->maxExceptions = property_exists($notification, 'maxExceptions') ? $notification->maxExceptions : null;

$this->deleteWhenMissingModels = property_exists($notification, 'deleteWhenMissingModels')
? $notification->deleteWhenMissingModels
: false;

if ($notification instanceof ShouldQueueAfterCommit) {
$this->afterCommit = true;
} else {
Expand Down
27 changes: 24 additions & 3 deletions src/Illuminate/Queue/CallQueuedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Log\Context\Repository as ContextRepository;
use Illuminate\Notifications\SendQueuedNotifications;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use ReflectionClass;
Expand Down Expand Up @@ -229,10 +230,16 @@ protected function handleModelNotFound(Job $job, $e)
$class = $job->resolveQueuedJobClass();

try {
$reflectionClass = new ReflectionClass($class);
$shouldDelete = $this->shouldDeleteWhenMissingModels($class);

$shouldDelete = $reflectionClass->getDefaultProperties()['deleteWhenMissingModels']
?? count($reflectionClass->getAttributes(DeleteWhenMissingModels::class)) !== 0;
if (! $shouldDelete && $class === SendQueuedNotifications::class) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is all this code needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taylorotwell

I think when we're in handleModelNotFound, the model couldn't be restored from the queue, so we only have the class name, not the actual job instance.

Reflection on SendQueuedNotifications just gives us the default value (false), not what was actually set from the notification. The payload's displayName is the only way to get back to the original notification class at that point.

Copy link
Author

@GalahadXVI GalahadXVI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the smallest change I could make to get it working. Happy on any suggestions though!

(though, I'm still confused as to why it worked pre-laravel 12 without this change. I couldn't figure it out)

Edit: I just tried out of curiosity to see if Claude could figure out why it worked previously and here is what it came back with:

commit dc63a5d changed resolveName() to resolveQueuedJobClass() in Laravel 12. For notifications, resolveName() returned the notification class via displayName, which accidentally made $deleteWhenMissingModels work. The new method returns the actual job class (SendQueuedNotifications), which broke it.

Copy link
Contributor

@cosmastech cosmastech Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆 I feel called out.

$payload = $job->payload();
$notificationClass = $payload['displayName'] ?? null;

if ($notificationClass && class_exists($notificationClass)) {
$shouldDelete = $this->shouldDeleteWhenMissingModels($notificationClass);
}
}
} catch (Exception) {
$shouldDelete = false;
}
Expand All @@ -246,6 +253,20 @@ protected function handleModelNotFound(Job $job, $e)
return $job->fail($e);
}

/**
* Determine if the job should be deleted when models are missing.
*
* @param string $class
* @return bool
*/
protected function shouldDeleteWhenMissingModels(string $class)
{
$reflectionClass = new ReflectionClass($class);

return $reflectionClass->getDefaultProperties()['deleteWhenMissingModels']
?? count($reflectionClass->getAttributes(DeleteWhenMissingModels::class)) !== 0;
}

/**
* Ensure the lock for a unique job is released via context.
*
Expand Down
137 changes: 137 additions & 0 deletions tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace Illuminate\Tests\Integration\Queue;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\Attributes\WithMigration;

#[WithMigration]
#[WithMigration('queue')]
class DeleteNotificationWhenMissingModelTest extends QueueTestCase
{
protected function defineEnvironment($app)
{
parent::defineEnvironment($app);
$app['config']->set('queue.default', 'database');
$this->driver = 'database';
}

protected function setUp(): void
{
parent::setUp();

Schema::create('notification_test_models', function (Blueprint $table) {
$table->id();
$table->string('name');
});
}

protected function tearDown(): void
{
DeleteMissingModelNotification::$handled = false;
FailingMissingModelNotification::$handled = false;
Schema::dropIfExists('notification_test_models');

parent::tearDown();
}

public function test_deleteWhenMissingModels_deletes_job_when_true(): void
{
$model = NotificationTestModel::query()->create(['name' => 'test']);

$notifiable = new TestNotifiableUser;
$notifiable->notify(new DeleteMissingModelNotification($model));

NotificationTestModel::query()->where('name', 'test')->delete();

$this->runQueueWorkerCommand(['--once' => '1']);

$this->assertFalse(DeleteMissingModelNotification::$handled);
$this->assertNull(\DB::table('failed_jobs')->first());
}

public function test_deleteWhenMissingModels_fails_job_when_false(): void
{
$model = NotificationTestModel::query()->create(['name' => 'test']);

$notifiable = new TestNotifiableUser;
$notifiable->notify(new FailingMissingModelNotification($model));

NotificationTestModel::query()->where('name', 'test')->delete();

$this->runQueueWorkerCommand(['--once' => '1']);

$this->assertFalse(FailingMissingModelNotification::$handled);
$this->assertNotNull(\DB::table('failed_jobs')->first());
}
}

class NotificationTestModel extends Model
{
protected $table = 'notification_test_models';

public $timestamps = false;

protected $guarded = [];
}

class TestNotifiableUser
{
use Notifiable;
}

class DeleteMissingModelNotification extends Notification implements ShouldQueue
{
use Queueable;

public static bool $handled = false;

public $deleteWhenMissingModels = true;

public function __construct(public NotificationTestModel $model)
{
}

public function via($notifiable): array
{
return ['mail'];
}

public function toArray($notifiable): array
{
self::$handled = true;

return ['model_id' => $this->model->id];
}
}

class FailingMissingModelNotification extends Notification implements ShouldQueue
{
use Queueable;

public static bool $handled = false;

public $deleteWhenMissingModels = false;

public function __construct(public NotificationTestModel $model)
{
}

public function via($notifiable): array
{
return ['mail'];
}

public function toArray($notifiable): array
{
self::$handled = true;

return ['model_id' => $this->model->id];
}
}
13 changes: 13 additions & 0 deletions tests/Notifications/NotificationSendQueuedNotificationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ public function testNotificationCanSetMaxExceptions()

$this->assertEquals(23, $job->maxExceptions);
}

public function testNotificationCanSetDeleteWhenMissingModels()
{
$notifiable = new NotifiableUser;
$notification = new class
{
public $deleteWhenMissingModels = true;
};

$job = new SendQueuedNotifications($notifiable, $notification);

$this->assertTrue($job->deleteWhenMissingModels);
}
}

class NotifiableUser extends Model
Expand Down
Loading