Skip to content

beforeCommit method does nothing when job has ShouldQueueAfterCommit contract #56397

@m-develops

Description

@m-develops

Laravel Version

11.45.1

PHP Version

8.3.8

Database Driver & Version

No response

Description

When a job is using the ShouldQueueAfterCommit contract it will always be dispatched after the commit even when the beforeCommit method is used.

DB::transaction(function(){
    $user = User::create(...);

    SendWelcomeEmail::dispatch($user)->beforeCommit();
});

The PR #35422 introduced a beforeCommit method to override the afterCommit behaviour.

The PR #48705 added a ShouldQueueAfterCommit contract but it ignores if the $job->afterCommit is already set to false.

    protected function shouldDispatchAfterCommit($job)
    {
        if ($job instanceof ShouldQueueAfterCommit) {
            return true;
        }

        if (! $job instanceof Closure && is_object($job) && isset($job->afterCommit)) {
            return $job->afterCommit;
        }

        return $this->dispatchAfterCommit ?? false;
    }

I think it should still respect the beforeCommit behaviour even when the ShouldQueueAfterCommit contract is used.

Could be solved by changing the shouldDispatchAfterCommit method to:

    protected function shouldDispatchAfterCommit($job)
    {
        if ($job instanceof ShouldQueueAfterCommit) {
            return isset($job->afterCommit) && $job->afterCommit === false ? false : true;
        }

        if (! $job instanceof Closure && is_object($job) && isset($job->afterCommit)) {
            return $job->afterCommit;
        }

        return $this->dispatchAfterCommit ?? false;
    }

Steps To Reproduce

This test that I added now fails:

<?php

namespace Illuminate\Tests\Integration\Queue;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Database\DatabaseTransactionsManager;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Bus;
use Mockery as m;
use Orchestra\Testbench\Attributes\WithConfig;
use Orchestra\Testbench\TestCase;
use Throwable;

#[WithConfig('queue.default', 'sqs')]
#[WithConfig('queue.connections.sqs.after_commit', true)]
class QueueConnectionTest extends TestCase
{
    protected function tearDown(): void
    {
        QueueConnectionTestShouldQueueAfterCommitJob::$ran = false;

        parent::tearDown();
    }

    public function testJobWithShouldQueueAfterCommitContractWillGetDispatchedInsideATransactionWhenExplicitlyIndicated()
    {
        $this->app->singleton('db.transactions', function () {
            $transactionManager = m::mock(DatabaseTransactionsManager::class);
            $transactionManager->shouldNotReceive('addCallback')->andReturn(null);
            $transactionManager->shouldNotReceive('addCallbackForRollback');

            return $transactionManager;
        });

        try {
            Bus::dispatch((new QueueConnectionTestShouldQueueAfterCommitJob)->beforeCommit());
        } catch (Throwable $exception) {
            // This job was dispatched
        }
    }
}

class QueueConnectionTestShouldQueueAfterCommitJob implements ShouldQueueAfterCommit
{
    use Dispatchable, Queueable;

    public static $ran = false;

    public function handle()
    {
        static::$ran = true;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions