├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── phpstan.yml ├── .phpcs.xml.dist ├── .prettyci.composer.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE-0.4.md ├── composer.json ├── phpstan.neon.dist └── src ├── BrefMessengerBundle.php ├── DependencyInjection └── BrefMessengerExtension.php ├── Resources └── config │ └── services.yaml └── Service ├── BusDriver.php ├── EventBridge ├── EventBridgeConsumer.php ├── EventBridgeTransport.php └── EventBridgeTransportFactory.php ├── MessengerTransportConfiguration.php ├── SimpleBusDriver.php ├── Sns ├── SnsConsumer.php ├── SnsFifoStamp.php ├── SnsTransport.php ├── SnsTransportFactory.php └── SnsTransportNameResolver.php └── Sqs ├── SqsConsumer.php └── SqsTransportNameResolver.php /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, **thank you** for contributing! 4 | 5 | Here are a few rules to follow in order to ease code reviews and merging: 6 | 7 | - follow [PSR-1](http://www.php-fig.org/psr/1/) and [PSR-2](http://www.php-fig.org/psr/2/) 8 | - run the test suite 9 | - write (or update) tests when applicable 10 | - write documentation for new features 11 | - use [commit messages that make sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 12 | 13 | One may ask you to [squash your commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) too. This is used to "clean" your pull request before merging it (we don't want commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 14 | 15 | When creating your pull request on GitHub, please write a description which gives the context and/or explains why you are creating it. 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mnapoli # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | jobs: 10 | php-tests: 11 | name: Tests - PHP ${{ matrix.php }} with Symfony ${{ matrix.sf_version }} 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 15 14 | strategy: 15 | max-parallel: 10 16 | fail-fast: false 17 | matrix: 18 | php: ['8.2', '8.3'] 19 | sf_version: ['7.0.*'] 20 | include: 21 | - php: '8.0' 22 | sf_version: '5.4.*' 23 | - php: '8.1' 24 | sf_version: '6.4.*' 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: ${{ matrix.php }} 33 | tools: composer:v2, flex 34 | coverage: none 35 | ini-values: expose_php=1 36 | - name: Install dependencies 37 | run: 'composer update --no-interaction --prefer-dist --no-progress' 38 | env: 39 | SYMFONY_REQUIRE: ${{ matrix.sf_version }} 40 | - name: Execute Unit Tests 41 | run: 'vendor/bin/phpunit' 42 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Composer 14 | run: composer install --prefer-dist --no-progress --no-suggest 15 | 16 | - name: PHPStan 17 | run: vendor/bin/phpstan analyse 18 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | tests 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.prettyci.composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "mnapoli/hard-mode": "^0.2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 0.4.2 6 | 7 | Added support for [EventBusName](https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html#eventbridge-Type-PutEventsRequestEntry-EventBusName) with EventBridgeTransport 8 | ## 0.4.0 9 | 10 | Use the SQS transport provided by [Symfony Amazon SQS Messenger](https://symfony.com/doc/current/messenger.html#amazon-sqs). 11 | See [UPGRADE-0.4.md](UPGRADE-0.4.md) 12 | ## 0.1.0 13 | 14 | First release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Matthieu Napoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bridge to use Symfony Messenger on AWS Lambda with [Bref](https://bref.sh). 2 | 3 | This bridge allows messages to be dispatched to SQS, SNS or EventBridge, while workers handle those messages on AWS Lambda. 4 | 5 | ## Documentation 6 | 7 | You can find the documentation [on the Bref website here](https://bref.sh/docs/symfony/messenger). 8 | -------------------------------------------------------------------------------- /UPGRADE-0.4.md: -------------------------------------------------------------------------------- 1 | UPGRADE to 0.4.0 2 | ================ 3 | 4 | Since version 0.4.0 this package uses the transport provided by [Symfony Amazon SQS Messenger](https://symfony.com/doc/current/messenger.html#amazon-sqs) 5 | and comes with these BC breaks: 6 | 7 | Message Headers 8 | --------------- 9 | > This does not affect transports that have the default `PhpSerializer` 10 | 11 | Previously the message headers were combined in a [single MessageAttribute](https://github.com/brefphp/symfony-messenger/blob/0.3.4/src/Service/Sqs/SqsTransport.php#L46), 12 | now each header is a [separate MessageAttribute](https://github.com/symfony/amazon-sqs-messenger/blob/v5.2.0/Transport/Connection.php#L310). 13 | This means that the SQS records are incompatible between bref/symfony-messenger 0.3.x and 0.4.0. 14 | 15 | 16 | Fifo Queues 17 | ----------- 18 | The setup for Fifo queues has changed; from transport configuration to a `Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsFifoStamp` 19 | 20 | Before: 21 | ```yaml 22 | # config/packages/messenger.yaml 23 | 24 | framework: 25 | messenger: 26 | transports: 27 | async: 28 | dsn: 'https://sqs.us-east-1.amazonaws.com/123456789/my-queue.fifo' 29 | options: 30 | message_group_id: com_example # This option is now invalid 31 | ``` 32 | After: 33 | ```php 34 | use Symfony\Component\Messenger\MessageBus; 35 | use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsFifoStamp; 36 | 37 | /* @var MessageBus $messageBus */ 38 | $messageBus->dispatch(new MyAsyncMessage(), [new AmazonSqsFifoStamp('com_example')]); 39 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bref/symfony-messenger", 3 | "description": "Symfony Messenger bridge to run with SQS and SNS on AWS Lambda with Bref", 4 | "keywords": ["bref", "symfony", "messenger", "sqs", "sns", "aws", "aws-lambda"], 5 | "license": "MIT", 6 | "type": "library", 7 | "autoload": { 8 | "psr-4": { 9 | "Bref\\Symfony\\Messenger\\": "src/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "Bref\\Symfony\\Messenger\\Test\\": "tests/" 15 | } 16 | }, 17 | "require": { 18 | "php": ">=8.0", 19 | "ext-json": "*", 20 | "async-aws/sns": "^1.0", 21 | "async-aws/sqs": "^1.2|^2.0", 22 | "async-aws/event-bridge": "^1.0", 23 | "bref/bref": "^1.5 || ^2.0", 24 | "symfony/amazon-sqs-messenger": "^5.4 || ^6.0 || ^7.0", 25 | "symfony/config": "^5.4 || ^6.0 || ^7.0", 26 | "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", 27 | "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", 28 | "symfony/messenger": "^5.4 || ^6.0 || ^7.0", 29 | "symfony/yaml": "^5.4 || ^6.0 || ^7.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^9.4", 33 | "mnapoli/hard-mode": "^0.3.0", 34 | "phpstan/phpstan": "^1.7.10", 35 | "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", 36 | "nyholm/symfony-bundle-test": "^3.0", 37 | "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0", 38 | "phpspec/prophecy": "^1.15", 39 | "phpspec/prophecy-phpunit": "^2.0" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests 6 | -------------------------------------------------------------------------------- /src/BrefMessengerBundle.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 17 | } 18 | 19 | public function prepend(ContainerBuilder $container): void 20 | { 21 | $frameworkConfig = $container->getExtensionConfig('framework'); 22 | $messengerTransports = $this->getMessengerTransports($frameworkConfig); 23 | 24 | $container->setParameter('messenger.transports', $messengerTransports); 25 | } 26 | 27 | private function getMessengerTransports(array $frameworkConfig): array 28 | { 29 | $transportConfigs = array_column( 30 | array_column($frameworkConfig, 'messenger'), 31 | 'transports', 32 | ); 33 | $transportConfigs = array_filter($transportConfigs); 34 | 35 | if (empty($transportConfigs)) { 36 | return []; 37 | } 38 | 39 | return array_merge(...$transportConfigs); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Resources/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | Bref\Symfony\Messenger\Service\BusDriver: '@Bref\Symfony\Messenger\Service\SimpleBusDriver' 3 | Bref\Symfony\Messenger\Service\SimpleBusDriver: 4 | arguments: 5 | - '@logger' 6 | 7 | Bref\Symfony\Messenger\Service\MessengerTransportConfiguration: 8 | arguments: 9 | $messengerTransportsConfiguration: '%messenger.transports%' 10 | 11 | # SNS 12 | Bref\Symfony\Messenger\Service\Sns\SnsTransportNameResolver: 13 | arguments: 14 | - '@Bref\Symfony\Messenger\Service\MessengerTransportConfiguration' 15 | Bref\Symfony\Messenger\Service\Sns\SnsTransportFactory: 16 | tags: ['messenger.transport_factory'] 17 | arguments: 18 | - '@bref.messenger.sns_client' 19 | bref.messenger.sns_client: 20 | class: AsyncAws\Sns\SnsClient 21 | 22 | # SQS 23 | Bref\Symfony\Messenger\Service\Sqs\SqsTransportNameResolver: 24 | arguments: 25 | - '@Bref\Symfony\Messenger\Service\MessengerTransportConfiguration' 26 | 27 | # EventBridge 28 | Bref\Symfony\Messenger\Service\EventBridge\EventBridgeTransportFactory: 29 | tags: ['messenger.transport_factory'] 30 | arguments: 31 | - '@bref.messenger.eventbridge_client' 32 | bref.messenger.eventbridge_client: 33 | class: AsyncAws\EventBridge\EventBridgeClient 34 | -------------------------------------------------------------------------------- /src/Service/BusDriver.php: -------------------------------------------------------------------------------- 1 | busDriver = $busDriver; 30 | $this->bus = $bus; 31 | $this->serializer = $serializer; 32 | $this->transportName = $transportName; 33 | } 34 | 35 | public function handleEventBridge(EventBridgeEvent $event, Context $context): void 36 | { 37 | $envelope = $this->serializer->decode($event->getDetail()); 38 | $this->busDriver->putEnvelopeOnBus($this->bus, $envelope, $this->transportName); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Service/EventBridge/EventBridgeTransport.php: -------------------------------------------------------------------------------- 1 | eventBridge = $eventBridge; 27 | $this->serializer = $serializer; 28 | $this->source = $source; 29 | $this->eventBusName = $eventBusName; 30 | } 31 | 32 | public function send(Envelope $envelope): Envelope 33 | { 34 | $encodedMessage = $this->serializer->encode($envelope); 35 | $arguments = [ 36 | 'Entries' => [ 37 | [ 38 | 'Detail' => json_encode($encodedMessage, JSON_THROW_ON_ERROR), 39 | // Ideally here we could put the class name of the message, but how to retrieve it? 40 | 'DetailType' => 'Symfony Messenger message', 41 | 'Source' => $this->source, 42 | ], 43 | ], 44 | ]; 45 | 46 | if ($this->eventBusName) { 47 | $arguments['Entries'][0]['EventBusName'] = $this->eventBusName; 48 | } 49 | 50 | try { 51 | $result = $this->eventBridge->putEvents($arguments); 52 | $failedCount = $result->getFailedEntryCount(); 53 | } catch (Throwable $e) { 54 | throw new TransportException($e->getMessage(), 0, $e); 55 | } 56 | 57 | if ($failedCount > 0) { 58 | foreach ($result->getEntries() as $entry) { 59 | $reason = $entry->getErrorMessage() ?? 'no reason provided'; 60 | throw new TransportException("$failedCount message(s) could not be published to EventBridge: $reason."); 61 | } 62 | } 63 | 64 | return $envelope; 65 | } 66 | 67 | public function get(): iterable 68 | { 69 | throw new Exception('Not implemented'); 70 | } 71 | 72 | public function ack(Envelope $envelope): void 73 | { 74 | throw new Exception('Not implemented'); 75 | } 76 | 77 | public function reject(Envelope $envelope): void 78 | { 79 | throw new Exception('Not implemented'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Service/EventBridge/EventBridgeTransportFactory.php: -------------------------------------------------------------------------------- 1 | eventBridge = $eventBridge; 19 | } 20 | 21 | public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface 22 | { 23 | if (false === $parsedUrl = parse_url($dsn)) { 24 | throw new InvalidArgumentException(sprintf('The given EventBridge DSN "%s" is invalid.', $dsn)); 25 | } 26 | 27 | $query = []; 28 | if (isset($parsedUrl['query'])) { 29 | parse_str($parsedUrl['query'], $query); 30 | } 31 | 32 | return new EventBridgeTransport($this->eventBridge, $serializer, $parsedUrl['host'], $query['event_bus_name'] ?? null); 33 | } 34 | 35 | public function supports(string $dsn, array $options): bool 36 | { 37 | return strpos($dsn, 'eventbridge://') === 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Service/MessengerTransportConfiguration.php: -------------------------------------------------------------------------------- 1 | messengerTransportsConfiguration as $messengerTransport => $messengerOptions) { 19 | $dsn = $this->extractDsnFromTransport($messengerOptions); 20 | 21 | if ($dsn === $eventSourceWithProtocol) { 22 | return $messengerTransport; 23 | } 24 | } 25 | 26 | throw new InvalidArgumentException(sprintf('No transport found for eventSource "%s".', $eventSourceWithProtocol)); 27 | } 28 | 29 | private function extractDsnFromTransport(string|array $messengerTransport): string 30 | { 31 | if (is_array($messengerTransport) && array_key_exists('dsn', $messengerTransport)) { 32 | return $messengerTransport['dsn']; 33 | } 34 | 35 | return $messengerTransport; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/SimpleBusDriver.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 20 | } 21 | 22 | public function putEnvelopeOnBus(MessageBusInterface $bus, Envelope $envelope, string $transportName): void 23 | { 24 | try { 25 | $envelope = $envelope->with(new ReceivedStamp($transportName), new ConsumedByWorkerStamp); 26 | $bus->dispatch($envelope); 27 | 28 | $message = $envelope->getMessage(); 29 | $this->logger->info('{class} was handled successfully.', [ 30 | 'class' => get_class($message), 31 | 'message' => $message, 32 | 'transport' => $transportName, 33 | ]); 34 | } catch (HandlerFailedException $e) { 35 | while ($e instanceof HandlerFailedException) { 36 | $e = $e->getPrevious() ?? $e; 37 | } 38 | 39 | throw $e; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Service/Sns/SnsConsumer.php: -------------------------------------------------------------------------------- 1 | busDriver = $busDriver; 35 | $this->bus = $bus; 36 | $this->serializer = $serializer; 37 | $this->transportName = $transportName; 38 | $this->transportNameResolver = $transportNameResolver; 39 | } 40 | 41 | public function handleSns(SnsEvent $event, Context $context): void 42 | { 43 | foreach ($event->getRecords() as $record) { 44 | $attributes = $record->getMessageAttributes(); 45 | $headers = isset($attributes['Headers']) ? $attributes['Headers']->getValue() : '[]'; 46 | $envelope = $this->serializer->decode(['body' => $record->getMessage(), 'headers' => json_decode($headers, true)]); 47 | 48 | $this->busDriver->putEnvelopeOnBus($this->bus, $envelope, $this->resolveTransportName($record)); 49 | } 50 | } 51 | 52 | private function resolveTransportName(SnsRecord $record): string 53 | { 54 | if (null === $this->transportName && null === $this->transportNameResolver) { 55 | throw new LogicException('You need to set $transportNameResolver or $transportName.'); 56 | } 57 | 58 | return $this->transportName ?? ($this->transportNameResolver)($record); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Service/Sns/SnsFifoStamp.php: -------------------------------------------------------------------------------- 1 | messageGroupId = $messageGroupId; 14 | $this->messageDeduplicationId = $messageDeduplicationId; 15 | } 16 | 17 | public function getMessageGroupId(): ?string 18 | { 19 | return $this->messageGroupId; 20 | } 21 | 22 | public function getMessageDeduplicationId(): ?string 23 | { 24 | return $this->messageDeduplicationId; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Service/Sns/SnsTransport.php: -------------------------------------------------------------------------------- 1 | sns = $sns; 26 | $this->serializer = $serializer; 27 | $this->topic = $topic; 28 | } 29 | 30 | public function send(Envelope $envelope): Envelope 31 | { 32 | $encodedMessage = $this->serializer->encode($envelope); 33 | $headers = $encodedMessage['headers'] ?? []; 34 | $arguments = [ 35 | 'MessageAttributes' => [ 36 | 'Headers' => new MessageAttributeValue(['DataType' => 'String', 'StringValue' => json_encode($headers, JSON_THROW_ON_ERROR)]), 37 | ], 38 | 'Message' => $encodedMessage['body'], 39 | 'TopicArn' => $this->topic, 40 | ]; 41 | if (str_contains($this->topic, ".fifo")) { 42 | $stamps = $envelope->all(); 43 | $dedupeStamp = $stamps[SnsFifoStamp::class][0] ?? false; 44 | if (!$dedupeStamp || $dedupeStamp instanceof SnsFifoStamp == false) { 45 | throw new Exception("SnsFifoStamp required for fifo topic"); 46 | } 47 | $messageGroupId = $dedupeStamp->getMessageGroupId() ?? false; 48 | $messageDeDuplicationId = $dedupeStamp->getMessageDeduplicationId() ?? false; 49 | if ($messageDeDuplicationId) { 50 | $arguments['MessageDeduplicationId'] = $messageDeDuplicationId; 51 | } 52 | if ($messageGroupId) { 53 | $arguments['MessageGroupId'] = $messageGroupId; 54 | } 55 | } 56 | try { 57 | $result = $this->sns->publish($arguments); 58 | $messageId = $result->getMessageId(); 59 | } catch (Throwable $e) { 60 | throw new TransportException($e->getMessage(), 0, $e); 61 | } 62 | 63 | if ($messageId === null) { 64 | throw new TransportException('Could not add a message to the SNS topic'); 65 | } 66 | 67 | return $envelope; 68 | } 69 | 70 | public function get(): iterable 71 | { 72 | throw new Exception('Not implemented'); 73 | } 74 | 75 | public function ack(Envelope $envelope): void 76 | { 77 | throw new Exception('Not implemented'); 78 | } 79 | 80 | public function reject(Envelope $envelope): void 81 | { 82 | throw new Exception('Not implemented'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Service/Sns/SnsTransportFactory.php: -------------------------------------------------------------------------------- 1 | sns = $sns; 18 | } 19 | 20 | public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface 21 | { 22 | return new SnsTransport($this->sns, $serializer, substr($dsn, 6)); 23 | } 24 | 25 | public function supports(string $dsn, array $options): bool 26 | { 27 | return strpos($dsn, 'sns://arn:aws:sns:') === 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Service/Sns/SnsTransportNameResolver.php: -------------------------------------------------------------------------------- 1 | toArray()['Sns'])) { 23 | throw new InvalidArgumentException('TopicArn is missing in sns record.'); 24 | } 25 | 26 | $eventSourceArn = $snsRecord->getTopicArn(); 27 | 28 | return $this->configurationProvider->provideTransportFromEventSource(self::TRANSPORT_PROTOCOL . $eventSourceArn); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Service/Sqs/SqsConsumer.php: -------------------------------------------------------------------------------- 1 | busDriver = $busDriver; 49 | $this->bus = $bus; 50 | $this->serializer = $serializer; 51 | $this->transportName = $transportName; 52 | $this->logger = $logger ?? new NullLogger(); 53 | $this->partialBatchFailure = $partialBatchFailure; 54 | $this->transportNameResolver = $transportNameResolver; 55 | } 56 | 57 | public function handleSqs(SqsEvent $event, Context $context): void 58 | { 59 | $isFifoQueue = null; 60 | $hasPreviousMessageFailed = false; 61 | $failingMessageGroupIds = []; 62 | 63 | foreach ($event->getRecords() as $record) { 64 | if ($isFifoQueue === null) { 65 | $isFifoQueue = \str_ends_with($record->toArray()['eventSourceARN'], '.fifo'); 66 | } 67 | 68 | $messageGroupId = $this->readMessageGroupIdOfRecord($record); 69 | 70 | /* 71 | * When using FIFO queues, preserving order is important. 72 | * If a previous message has failed in the batch, we need to skip the next ones and requeue them. 73 | */ 74 | if ($isFifoQueue && $hasPreviousMessageFailed && in_array($messageGroupId, $failingMessageGroupIds, true)) { 75 | $this->markAsFailed($record); 76 | continue; 77 | } 78 | 79 | $headers = []; 80 | $attributes = $record->getMessageAttributes(); 81 | 82 | if (isset($attributes[self::MESSAGE_ATTRIBUTE_NAME]) && $attributes[self::MESSAGE_ATTRIBUTE_NAME]['dataType'] === 'String') { 83 | $headers = json_decode($attributes[self::MESSAGE_ATTRIBUTE_NAME]['stringValue'], true); 84 | unset($attributes[self::MESSAGE_ATTRIBUTE_NAME]); 85 | } 86 | 87 | foreach ($attributes as $name => $attribute) { 88 | if ($attribute['dataType'] !== 'String') { 89 | continue; 90 | } 91 | $headers[$name] = $attribute['stringValue']; 92 | } 93 | 94 | try { 95 | $envelope = $this->serializer->decode(['body' => $record->getBody(), 'headers' => $headers]); 96 | 97 | $stamps = [new AmazonSqsReceivedStamp($record->getMessageId())]; 98 | 99 | if ('' !== $context->getTraceId()) { 100 | $stamps[] = new AmazonSqsXrayTraceHeaderStamp($context->getTraceId()); 101 | } 102 | $this->busDriver->putEnvelopeOnBus($this->bus, $envelope->with(...$stamps), $this->resolveTransportName($record)); 103 | } catch (UnrecoverableExceptionInterface $exception) { 104 | $this->logger->error(sprintf('SQS record with id "%s" failed to be processed. But failure was marked as unrecoverable. Message will be acknowledged.', $record->getMessageId())); 105 | $this->logger->error($exception); 106 | } catch (Throwable $exception) { 107 | if ($this->partialBatchFailure === false) { 108 | throw $exception; 109 | } 110 | 111 | $this->logger->error(sprintf('SQS record with id "%s" failed to be processed.', $record->getMessageId())); 112 | $this->logger->error($exception->getMessage()); 113 | $this->markAsFailed($record); 114 | $hasPreviousMessageFailed = true; 115 | $failingMessageGroupIds[] = $this->readMessageGroupIdOfRecord($record); 116 | } 117 | } 118 | } 119 | 120 | private function readMessageGroupIdOfRecord(SqsRecord $record): ?string 121 | { 122 | $recordAsArray = $record->toArray(); 123 | return $recordAsArray['attributes']['MessageGroupId'] ?? null; 124 | } 125 | 126 | private function resolveTransportName(SqsRecord $record): string 127 | { 128 | if (null === $this->transportName && null === $this->transportNameResolver) { 129 | throw new LogicException('You need to set $transportNameResolver or $transportName.'); 130 | } 131 | 132 | return $this->transportName ?? ($this->transportNameResolver)($record); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Service/Sqs/SqsTransportNameResolver.php: -------------------------------------------------------------------------------- 1 | toArray())) { 22 | throw new InvalidArgumentException('EventSourceArn is missing in sqs record.'); 23 | } 24 | 25 | $eventSourceArn = $sqsRecord->toArray()['eventSourceARN']; 26 | 27 | return $this->configurationProvider->provideTransportFromEventSource(self::TRANSPORT_PROTOCOL . $eventSourceArn); 28 | } 29 | } 30 | --------------------------------------------------------------------------------