From 219a9bde44b67897d791bde6c910dc2e497d3d64 Mon Sep 17 00:00:00 2001 From: agis Date: Wed, 5 Aug 2026 16:41:42 +0700 Subject: [PATCH 1/3] fix(queue): make SQS connector work on AWS SDK v3 The built-in SqsConnector called the removed SDK v2 API `SqsClient::factory($config)`, which fatals under aws-sdk-php v3. Build the client the v3 way (explicit `version` + nested `credentials`), support a custom `endpoint` for SQS-compatible services (ElasticMQ/LocalStack) and an optional credentials fallback to the SDK default provider chain. SqsQueue::getQueue() now resolves a bare queue name into a full URL using a configured `prefix` (the account base URL), mirroring Laravel 5+ behaviour, so a worker's `--queue=default` is no longer sent as an invalid QueueUrl. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Queue/Connectors/SqsConnector.php | 31 +++++++++++++++++-- src/Illuminate/Queue/SqsQueue.php | 25 +++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 86aeeca56..c09999720 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -8,14 +8,41 @@ class SqsConnector implements ConnectorInterface { /** * Establish a queue connection. * + * The client is built the AWS SDK v3 way (explicit `version` + nested + * `credentials`); the removed v2 `SqsClient::factory()` is no longer used. + * * @param array $config * @return \Illuminate\Queue\QueueInterface */ public function connect(array $config) { - $sqs = SqsClient::factory($config); + $clientConfig = array( + 'region' => isset($config['region']) ? $config['region'] : 'us-east-1', + 'version' => isset($config['version']) ? $config['version'] : 'latest', + ); + + // Custom endpoint for SQS-compatible services (ElasticMQ/LocalStack) in + // local/dev; omitted in production so the SDK targets real AWS. + if ( ! empty($config['endpoint'])) + { + $clientConfig['endpoint'] = $config['endpoint']; + } + + // Credentials are optional: when absent the SDK falls back to its + // default provider chain (env vars, IAM instance/task role). + if ( ! empty($config['key']) && ! empty($config['secret'])) + { + $clientConfig['credentials'] = array( + 'key' => $config['key'], + 'secret' => $config['secret'], + ); + } - return new SqsQueue($sqs, $config['queue']); + return new SqsQueue( + new SqsClient($clientConfig), + $config['queue'], + isset($config['prefix']) ? $config['prefix'] : '' + ); } } diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index e78fb6090..a78c936dc 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -19,17 +19,26 @@ class SqsQueue extends Queue implements QueueInterface { */ protected $default; + /** + * The queue URL prefix (account base URL). + * + * @var string + */ + protected $prefix; + /** * Create a new Amazon SQS queue instance. * * @param \Aws\Sqs\SqsClient $sqs * @param string $default + * @param string $prefix * @return void */ - public function __construct(SqsClient $sqs, $default) + public function __construct(SqsClient $sqs, $default, $prefix = '') { $this->sqs = $sqs; $this->default = $default; + $this->prefix = $prefix; } /** @@ -105,12 +114,24 @@ public function pop($queue = null) /** * Get the queue or return the default. * + * A bare queue name is resolved into a full URL using the configured + * prefix (the account base URL); full URLs are returned untouched. + * * @param string|null $queue * @return string */ public function getQueue($queue) { - return $queue ?: $this->default; + $queue = $queue ?: $this->default; + + if (filter_var($queue, FILTER_VALIDATE_URL) !== false) + { + return $queue; + } + + return $this->prefix !== '' + ? rtrim($this->prefix, '/') . '/' . $queue + : $queue; } /** From 80cdeda4de0f80ca70a0f2b031e5294a40933cac Mon Sep 17 00:00:00 2001 From: agis Date: Wed, 5 Aug 2026 17:18:54 +0700 Subject: [PATCH 2/3] test(queue): cover SqsQueue::getQueue prefix resolution Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/Queue/QueueSqsQueueGetQueueTest.php | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Queue/QueueSqsQueueGetQueueTest.php diff --git a/tests/Queue/QueueSqsQueueGetQueueTest.php b/tests/Queue/QueueSqsQueueGetQueueTest.php new file mode 100644 index 000000000..80b8aaae2 --- /dev/null +++ b/tests/Queue/QueueSqsQueueGetQueueTest.php @@ -0,0 +1,38 @@ +assertSame($expected, $sqs->getQueue($queue)); + } + + public static function queueProvider(): array + { + return [ + 'bare name is prefixed' => [self::PREFIX, 'default', 'emails', self::PREFIX . '/emails'], + 'null falls back to default' => [self::PREFIX, 'default', null, self::PREFIX . '/default'], + 'full url passes through' => [self::PREFIX, 'default', self::PREFIX . '/emails', self::PREFIX . '/emails'], + 'trailing slash is trimmed' => [self::PREFIX . '/', 'default', 'emails', self::PREFIX . '/emails'], + 'empty prefix leaves name' => ['', 'default', 'emails', 'emails'], + ]; + } + +} From 9babdff436a8e83b4c2c78edaea236b8d447efe8 Mon Sep 17 00:00:00 2001 From: agis Date: Thu, 6 Aug 2026 10:17:09 +0700 Subject: [PATCH 3/3] test(queue): revive SqsQueue tests in the modern Laravel style The suite was skipped since 2017 and used the PHPUnit-removed getMock() API and a v2 Guzzle response Model. Port it to the current upstream style: mock the SqsClient with Mockery, use Aws\Result for responses, and drive partial mocks via getMockBuilder()->onlyMethods()->setConstructorArgs(). Adds direct coverage for SqsQueue::getQueue() prefix resolution (with and without a prefix), replacing the standalone getQueue test. Pulls aws/aws-sdk-php into require-dev (as upstream does) so the SQS driver tests can run; the FIFO/suffix cases are omitted as that feature isn't ported. Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.json | 1 + tests/Queue/QueueSqsQueueGetQueueTest.php | 38 ----- tests/Queue/QueueSqsQueueTest.php | 177 +++++++++++----------- 3 files changed, 86 insertions(+), 130 deletions(-) delete mode 100644 tests/Queue/QueueSqsQueueGetQueueTest.php diff --git a/composer.json b/composer.json index e5ef79e1f..a66324585 100755 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "illuminate/workbench": "self.version" }, "require-dev": { + "aws/aws-sdk-php": "^3.322.9", "mockery/mockery": "~1.3", "phpspec/prophecy-phpunit": "~2.0", "phpunit/phpunit": "~9.6", diff --git a/tests/Queue/QueueSqsQueueGetQueueTest.php b/tests/Queue/QueueSqsQueueGetQueueTest.php deleted file mode 100644 index 80b8aaae2..000000000 --- a/tests/Queue/QueueSqsQueueGetQueueTest.php +++ /dev/null @@ -1,38 +0,0 @@ -assertSame($expected, $sqs->getQueue($queue)); - } - - public static function queueProvider(): array - { - return [ - 'bare name is prefixed' => [self::PREFIX, 'default', 'emails', self::PREFIX . '/emails'], - 'null falls back to default' => [self::PREFIX, 'default', null, self::PREFIX . '/default'], - 'full url passes through' => [self::PREFIX, 'default', self::PREFIX . '/emails', self::PREFIX . '/emails'], - 'trailing slash is trimmed' => [self::PREFIX . '/', 'default', 'emails', self::PREFIX . '/emails'], - 'empty prefix leaves name' => ['', 'default', 'emails', 'emails'], - ]; - } - -} diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index a6e477d83..f37035cc3 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -1,7 +1,8 @@ markTestSkipped(); - - // Use Mockery to mock the SqsClient - $this->sqs = m::mock('Aws\Sqs\SqsClient'); + $this->sqs = m::mock(SqsClient::class); $this->account = '1234567891011'; $this->queueName = 'emails'; $this->baseUrl = 'https://sqs.someregion.amazonaws.com'; - // This is how the modified getQueue builds the queueUrl - $this->queueUrl = $this->baseUrl . '/' . $this->account . '/' . $this->queueName; + // This is how the modified getQueue builds the queueUrl. + $this->prefix = $this->baseUrl . '/' . $this->account . '/'; + $this->queueUrl = $this->prefix . $this->queueName; - $this->mockedJob = 'foo'; - $this->mockedData = ['data']; - $this->mockedPayload = json_encode(['job' => $this->mockedJob, 'data' => $this->mockedData]); - $this->mockedDelay = 10; - $this->mockedMessageId = 'e3cd03ee-59a3-4ad8-b0aa-ee2e3808ac81'; - $this->mockedReceiptHandle = '0NNAq8PwvXuWv5gMtS9DJ8qEdyiUwbAjpp45w2m6M4SJ1Y+PxCh7R930NRB8ylSacEmoSnW18bgd4nK\/O6ctE+VFVul4eD23mA07vVoSnPI4F\/voI1eNCp6Iax0ktGmhlNVzBwaZHEr91BRtqTRM3QKd2ASF8u+IQaSwyl\/DGK+P1+dqUOodvOVtExJwdyDLy1glZVgm85Yw9Jf5yZEEErqRwzYz\/qSigdvW4sm2l7e4phRol\/+IjMtovOyH\/ukueYdlVbQ4OshQLENhUKe7RNN5i6bE\/e5x9bnPhfj2gbM'; + $this->mockedJob = 'foo'; + $this->mockedData = ['data']; + $this->mockedPayload = json_encode(['job' => $this->mockedJob, 'data' => $this->mockedData]); + $this->mockedDelay = 10; + $this->mockedMessageId = 'e3cd03ee-59a3-4ad8-b0aa-ee2e3808ac81'; + $this->mockedReceiptHandle = '0NNAq8PwvXuWv5gMtS9DJ8qEdyiUwbAjpp45w2m6M4SJ1Y+PxCh7R930NRB8ylSacEmoSnW18bgd4nK/O6ctE'; - $this->mockedSendMessageResponseModel = new Model([ + $this->mockedSendMessageResponseModel = new Result([ 'Body' => $this->mockedPayload, - 'MD5OfBody' => md5((string) $this->mockedPayload), - 'ReceiptHandle' => $this->mockedReceiptHandle, - 'MessageId' => $this->mockedMessageId, - 'Attributes' => ['ApproximateReceiveCount' => 1] + 'MD5OfBody' => md5((string) $this->mockedPayload), + 'ReceiptHandle' => $this->mockedReceiptHandle, + 'MessageId' => $this->mockedMessageId, + 'Attributes' => ['ApproximateReceiveCount' => 1], ]); - $this->mockedReceiveMessageResponseModel = new Model([ + $this->mockedReceiveMessageResponseModel = new Result([ 'Messages' => [ 0 => [ - 'Body' => $this->mockedPayload, - 'MD5OfBody' => md5((string) $this->mockedPayload), - 'ReceiptHandle' => $this->mockedReceiptHandle, - 'MessageId' => $this->mockedMessageId - ] - ] + 'Body' => $this->mockedPayload, + 'MD5OfBody' => md5((string) $this->mockedPayload), + 'ReceiptHandle' => $this->mockedReceiptHandle, + 'MessageId' => $this->mockedMessageId, + ], + ], ]); - } - - - public function testPopProperlyPopsJobOffOfSqs() - { - $queue = $this->getMock(SqsQueue::class, ['getQueue'], [$this->sqs, $this->queueName, $this->account]); - $queue->setContainer(m::mock(Container::class)); - $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); - $this->sqs->shouldReceive('receiveMessage')->once()->with( - ['QueueUrl' => $this->queueUrl, 'AttributeNames' => ['ApproximateReceiveCount']] - )->andReturn($this->mockedReceiveMessageResponseModel); - $result = $queue->pop($this->queueName); - $this->assertInstanceOf(SqsJob::class, $result); - } - - - public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs() - { - $now = Carbon::now(); - $queue = $this->getMock(SqsQueue::class, ['createPayload', 'getSeconds', 'getQueue'], [$this->sqs, $this->queueName, $this->account] - ); - $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn( - $this->mockedPayload - ); - $queue->expects($this->once())->method('getSeconds')->with($now)->willReturn(5); - $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); - $this->sqs->shouldReceive('sendMessage')->once()->with( - ['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => 5] - )->andReturn($this->mockedSendMessageResponseModel); - $id = $queue->later($now->addSeconds(5), $this->mockedJob, $this->mockedData, $this->queueName); - $this->assertEquals($this->mockedMessageId, $id); - } - - - public function testDelayedPushProperlyPushesJobOntoSqs() - { - $queue = $this->getMock(SqsQueue::class, ['createPayload', 'getSeconds', 'getQueue'], [$this->sqs, $this->queueName, $this->account] - ); - $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn( - $this->mockedPayload - ); - $queue->expects($this->once())->method('getSeconds')->with($this->mockedDelay)->willReturn($this->mockedDelay); - $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); - $this->sqs->shouldReceive('sendMessage')->once()->with( - ['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => $this->mockedDelay] - )->andReturn($this->mockedSendMessageResponseModel); - $id = $queue->later($this->mockedDelay, $this->mockedJob, $this->mockedData, $this->queueName); - $this->assertEquals($this->mockedMessageId, $id); - } - - - public function testPushProperlyPushesJobOntoSqs() - { - $queue = $this->getMock(SqsQueue::class, ['createPayload', 'getQueue'], [$this->sqs, $this->queueName, $this->account] - ); - $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn( - $this->mockedPayload - ); - $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); - $this->sqs->shouldReceive('sendMessage')->once()->with( - ['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload] - )->andReturn($this->mockedSendMessageResponseModel); - $id = $queue->push($this->mockedJob, $this->mockedData, $this->queueName); - $this->assertEquals($this->mockedMessageId, $id); - } + } + + public function testPopProperlyPopsJobOffOfSqs() + { + $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->setContainer(m::mock(Container::class)); + $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); + $this->sqs->shouldReceive('receiveMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'AttributeNames' => ['ApproximateReceiveCount']])->andReturn($this->mockedReceiveMessageResponseModel); + $result = $queue->pop($this->queueName); + $this->assertInstanceOf(SqsJob::class, $result); + } + + public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs() + { + $now = Carbon::now(); + $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'getSeconds', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn($this->mockedPayload); + $queue->expects($this->once())->method('getSeconds')->with($now)->willReturn(5); + $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); + $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => 5])->andReturn($this->mockedSendMessageResponseModel); + $id = $queue->later($now, $this->mockedJob, $this->mockedData, $this->queueName); + $this->assertEquals($this->mockedMessageId, $id); + } + + public function testDelayedPushProperlyPushesJobOntoSqs() + { + $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'getSeconds', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn($this->mockedPayload); + $queue->expects($this->once())->method('getSeconds')->with($this->mockedDelay)->willReturn($this->mockedDelay); + $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); + $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => $this->mockedDelay])->andReturn($this->mockedSendMessageResponseModel); + $id = $queue->later($this->mockedDelay, $this->mockedJob, $this->mockedData, $this->queueName); + $this->assertEquals($this->mockedMessageId, $id); + } + + public function testPushProperlyPushesJobOntoSqs() + { + $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->mockedData)->willReturn($this->mockedPayload); + $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); + $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload])->andReturn($this->mockedSendMessageResponseModel); + $id = $queue->push($this->mockedJob, $this->mockedData, $this->queueName); + $this->assertEquals($this->mockedMessageId, $id); + } + + public function testGetQueueProperlyResolvesUrlWithPrefix() + { + $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix); + $this->assertEquals($this->queueUrl, $queue->getQueue(null)); + $this->assertEquals($this->baseUrl . '/' . $this->account . '/test', $queue->getQueue('test')); + } + + public function testGetQueueProperlyResolvesUrlWithoutPrefix() + { + $queue = new SqsQueue($this->sqs, $this->queueUrl); + $this->assertEquals($this->queueUrl, $queue->getQueue(null)); + $this->assertEquals($this->queueUrl, $queue->getQueue($this->queueUrl)); + } }