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-appsBot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a Messenger class that wraps multiple messaging adapters and provides automatic exception-based failover, along with 14 unit tests and a README example.

  • Messenger.php: Orchestrates adapters in sequence, validating type compatibility at construction and delegating send() to each adapter until one succeeds; catches \Exception for fallback but does not cover \Error/\TypeError subclasses.
  • MessengerTest.php: test_throws_when_all_adapters_fail and test_throws_when_single_adapter_fails each call expectExceptionMessage() three times — PHPUnit silently keeps only the last call, so the error header and first-adapter error detail are never actually verified.
  • README.md: Adds a failover usage example without documenting that non-exception failure responses bypass failover.

Confidence Score: 4/5

  • The Messenger class itself is safe to merge; the test suite has a structural defect that leaves parts of the error message format unverified.
  • Two tests call expectExceptionMessage() multiple times, but PHPUnit only honours the last call — the earlier assertions checking the "All N adapters failed" header and individual adapter error lines are silently dropped. This means the exception message format is only partially tested, and a regression in error reporting would not be caught by these tests.
  • tests/Messaging/MessengerTest.php — specifically test_throws_when_all_adapters_fail and test_throws_when_single_adapter_fails.

Important Files Changed

FilenameOverview
src/Utopia/Messaging/Messenger.phpNew Messenger class with failover orchestration; the send() catch block only covers \Exception, not \Error subclasses, and direct $adapters[0] access in validateAdapters, send, getType, and getMessageType will produce a null-dereference \TypeError when a non-zero-indexed array is passed.
tests/Messaging/MessengerTest.php14 tests covering the Messenger class; test_throws_when_all_adapters_fail and test_throws_when_single_adapter_fails use multiple expectExceptionMessage() calls that silently overwrite each other, leaving only the last assertion active and giving false confidence over the full exception message structure.
README.mdAdds a "Multiple Adapters (Failover)" section with a working usage example; accurate but omits the caveat that soft-failure responses (no exception thrown, deliveredTo: 0) are returned as-is without attempting the next adapter.

Fix all with GreploopFix All in Claude CodeFix All in Codex

Prompt To Fix All With AI
### Issue 1
tests/Messaging/MessengerTest.php:195-205
**Multiple `expectExceptionMessage` calls silently overwrite each other**
PHPUnit stores a single expected message string; each call to `expectExceptionMessage()` replaces the previous one. In `test_throws_when_all_adapters_fail` (and similarly in `test_throws_when_single_adapter_fails`), only the last call (`'SecondAdapter (adapter 2): API error'`) is actually asserted. The earlier checks for `'All 2 adapters failed'` and `'FirstAdapter (adapter 1): Connection timeout'` are silently discarded, leaving the error header and first-adapter detail completely untested. Use a manual `try/catch` with `assertStringContainsString()` calls to assert all three sub-strings.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (7): 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.

P2Failover 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 threadsrc/Utopia/Messaging/Messenger.php
* 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.

P1Soft-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.

Comment threadsrc/Utopia/Messaging/Messenger.php
@deepshekhardas

Copy link
Copy Markdown
Author

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

Based on PR utopia-php#115 by TorstenDittmann. Adds Messenger class that orchestrates multiple adapters with automatic failover.
Comment on lines +195 to +205
}

public function test_rejects_empty_adapter_list(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('At least one adapter must be provided');

new Messenger([]);
}

public function test_rejects_non_adapter_array_element(): void

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1Multiple expectExceptionMessage calls silently overwrite each other

PHPUnit stores a single expected message string; each call to expectExceptionMessage() replaces the previous one. In test_throws_when_all_adapters_fail (and similarly in test_throws_when_single_adapter_fails), only the last call ('SecondAdapter (adapter 2): API error') is actually asserted. The earlier checks for 'All 2 adapters failed' and 'FirstAdapter (adapter 1): Connection timeout' are silently discarded, leaving the error header and first-adapter detail completely untested. Use a manual try/catch with assertStringContainsString() calls to assert all three sub-strings.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/Messaging/MessengerTest.php
Line: 195-205
Comment:
**Multiple `expectExceptionMessage` calls silently overwrite each other**
PHPUnit stores a single expected message string; each call to `expectExceptionMessage()` replaces the previous one. In `test_throws_when_all_adapters_fail` (and similarly in `test_throws_when_single_adapter_fails`), only the last call (`'SecondAdapter (adapter 2): API error'`) is actually asserted. The earlier checks for `'All 2 adapters failed'` and `'FirstAdapter (adapter 1): Connection timeout'` are silently discarded, leaving the error header and first-adapter detail completely untested. Use a manual `try/catch` with `assertStringContainsString()` calls to assert all three sub-strings.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude CodeFix in Codex

Sign up for freeto 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

@deepshekhardas