Skip to content

feat: add Messenger class for multiple adapter failover support#128

Open
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:fix/115-messenger-failover
Open

feat: add Messenger class for multiple adapter failover support#128
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:fix/115-messenger-failover

Conversation

@deepshekhardas

Copy link
Copy Markdown

Port of PR #115 by TorstenDittmann.

Adds Messenger class that orchestrates multiple messaging adapters with automatic failover. If one adapter fails, it falls back to the next in the list.

Changes:

  • New Messenger class with failover orchestration
  • Adapter compatibility validation (same type/message type)
  • 14 comprehensive tests
  • Updated README with usage example

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a Messenger class that wraps one or more Adapter instances and provides automatic failover — trying each adapter in order until one succeeds, or throwing a combined error if all fail.

  • Messenger.php: new orchestrator class with constructor-time compatibility validation (getType, getMessageType), getMaxMessagesPerRequest returning the minimum across all adapters, and a send() loop that catches \Exception to trigger fallback.
  • MessengerTest.php: 14 unit tests covering successful delivery, multi-step fallback, all-fail aggregation, type validation, and explicit documentation that soft-failure payloads (deliveredTo: 0, no exception) do not trigger fallback.
  • README.md: adds a "Multiple Adapters (Failover)" section with a Twilio/Vonage usage example.

Confidence Score: 4/5

Safe to merge after fixing the array key normalisation issue in the constructor.

The constructor stores the raw input array and then accesses $adapters[0] directly in validateAdapters, send, getType, and getMessageType. An associative or non-zero-based array produces a TypeError from null->getType() instead of the clean InvalidArgumentException the class promises. Adding a single array_values() call resolves it.

src/Utopia/Messaging/Messenger.php — the array key normalisation fix is needed before the class handles all valid array inputs correctly.

Important Files Changed

Filename Overview
src/Utopia/Messaging/Messenger.php New Messenger failover class; catches \Exception only (not \Throwable) and breaks on non-0-indexed input arrays due to direct $adapters[0] access without array_values() normalization.
tests/Messaging/MessengerTest.php 14 tests covering construction validation, failover, single-adapter path, email adapters, invalid message types, and explicit test of soft-failure bypass behaviour; all test the 0-indexed array path only.
README.md Adds a "Multiple Adapters (Failover)" section with a correct Twilio/Vonage example and accurate description of the exception-based failover semantics.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/Utopia/Messaging/Messenger.php:52-66
**Non-zero-indexed arrays silently break construction**

The constructor stores the raw input array without normalising its keys, yet both `validateAdapters` and multiple public methods access `$adapters[0]` / `$this->adapters[0]` directly. If a caller passes an associative or non-zero-based array (e.g. `new Messenger(['primary' => $twilio, 'backup' => $vonage])`), `$adapters[0]` evaluates to `null`, and `null->getType()` inside `validateAdapters` throws an unhandled `\TypeError` instead of the clean `\InvalidArgumentException` the class documents. Adding `$adapters = array_values($adapters);` after the element-type loop fixes all affected sites (`validateAdapters`, `send`, `getType`, `getMessageType`) in one place.

Reviews (3): Last reviewed commit: "feat: add Messenger class for multiple a..." | Re-trigger Greptile

Comment on lines +90 to +95
if (! \is_a($message, $messageType)) {
throw new \Exception(
'Invalid message type. Expected "'
.$messageType
.'", got "'
.\get_class($message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Failover skipped on \Error subclasses

The catch block only catches \Exception, so any \TypeError or other \Error thrown by an adapter will propagate immediately without trying the next adapter, defeating the purpose of failover. Changing to \Throwable would cover both \Exception and \Error hierarchies.

Comment thread src/Utopia/Messaging/Messenger.php
@deepshekhardas deepshekhardas force-pushed the fix/115-messenger-failover branch from d4b60e4 to 834d575 Compare June 17, 2026 09:14
* successful adapter.
*
* @param Message $message The message to send.
* @return array{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Soft-failure responses bypass failover silently

Messenger::send() only falls back on thrown exceptions, so an adapter that returns a response with deliveredTo: 0 and all-failure results is treated as "success" — the second adapter is never tried. This is documented by the test, but the behaviour may surprise callers who expect the Messenger to be smarter about delivery failures. At minimum, the PHPDoc and README should warn that "failure" payloads without an exception are returned as-is without attempting the next adapter, so callers must inspect deliveredTo themselves.

Based on PR utopia-php#115 by TorstenDittmann. Adds Messenger class that orchestrates multiple adapters with automatic failover.
@deepshekhardas deepshekhardas force-pushed the fix/115-messenger-failover branch from 834d575 to dc20a1c Compare July 10, 2026 05:01
Comment on lines +52 to +66
foreach ($adapters as $index => $adapter) {
if (! $adapter instanceof Adapter) {
throw new \InvalidArgumentException(
'All elements must be instances of Adapter, but element '
.$index
.' is '
.\get_debug_type($adapter)
.'.'
);
}
}

$this->validateAdapters($adapters);

$this->adapters = $adapters;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Non-zero-indexed arrays silently break construction

The constructor stores the raw input array without normalising its keys, yet both validateAdapters and multiple public methods access $adapters[0] / $this->adapters[0] directly. If a caller passes an associative or non-zero-based array (e.g. new Messenger(['primary' => $twilio, 'backup' => $vonage])), $adapters[0] evaluates to null, and null->getType() inside validateAdapters throws an unhandled \TypeError instead of the clean \InvalidArgumentException the class documents. Adding $adapters = array_values($adapters); after the element-type loop fixes all affected sites (validateAdapters, send, getType, getMessageType) in one place.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Utopia/Messaging/Messenger.php
Line: 52-66

Comment:
**Non-zero-indexed arrays silently break construction**

The constructor stores the raw input array without normalising its keys, yet both `validateAdapters` and multiple public methods access `$adapters[0]` / `$this->adapters[0]` directly. If a caller passes an associative or non-zero-based array (e.g. `new Messenger(['primary' => $twilio, 'backup' => $vonage])`), `$adapters[0]` evaluates to `null`, and `null->getType()` inside `validateAdapters` throws an unhandled `\TypeError` instead of the clean `\InvalidArgumentException` the class documents. Adding `$adapters = array_values($adapters);` after the element-type loop fixes all affected sites (`validateAdapters`, `send`, `getType`, `getMessageType`) in one place.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

@deepshekhardas

Copy link
Copy Markdown
Author

Following up - this PR adds Messenger class for multiple adapter failover. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant