feat: add email adapter DSN parsing#125
Conversation
This commit introduces the Messenger class, which enables automatic failover
across multiple messaging adapters. If one adapter throws an exception, the
next adapter in the sequence is tried until one succeeds or all fail.
Features:
- Accepts a single Adapter or an array of Adapters
- Tries adapters sequentially on exception
- Validates adapter compatibility (same type and message type)
- Returns the first successful response
- Throws aggregated exception with details if all adapters fail
- Supports SMS, Email, Push, and any other adapter types
Example usage:
$messenger = new Messenger([
new Twilio('sid', 'token'),
new Vonage('key', 'secret'),
]);
$result = $messenger->send($message);
Changes:
- Add src/Utopia/Messaging/Messenger.php
- Add tests/Messaging/MessengerTest.php with comprehensive test coverage
- Update README.md with usage example
Closes: feature request for multiple adapter support
Validate Messenger adapter arrays at runtime so invalid elements fail with a clear InvalidArgumentException instead of a PHP error. This keeps the new Adapter|Adapter[] constructor ergonomic without weakening input validation. Also replace Messenger sprintf-based error construction with direct string concatenation, pluralize the single-adapter failure message correctly, and add test coverage for single-adapter construction and invalid array elements.
Greptile SummaryThis PR adds DSN-string factory support (
Confidence Score: 3/5The new feature code is well-structured, but one test in DsnTest.php asserts the wrong exception message and will fail on the current implementation. The test_rejects_malformed_smtp_dsn test expects 'Invalid email DSN.' for smtp://, but the actual code path throws 'SMTP DSN must include a host.' - the test will fail as written. The Messenger catch-clause gap is a secondary concern with no current failing path. tests/Messaging/Adapter/Email/DsnTest.php - the malformed-SMTP test case expects the wrong exception message and will fail Important Files Changed
Reviews (1): Last reviewed commit: "feat: add email adapter DSN parsing" | Re-trigger Greptile |
| public function test_rejects_malformed_smtp_dsn(): void | ||
| { | ||
| $this->expectException(\InvalidArgumentException::class); | ||
| $this->expectExceptionMessage('Invalid email DSN.'); | ||
|
|
||
| EmailAdapter::fromDsn('smtp://'); | ||
| } |
There was a problem hiding this comment.
The expected exception message doesn't match what the code actually throws for
smtp://. parse_url('smtp://') returns ['scheme' => 'smtp'], so the scheme check passes and createSmtpAdapter is reached, which then throws 'SMTP DSN must include a host.' — not 'Invalid email DSN.'. This test will fail as-is.
| public function test_rejects_malformed_smtp_dsn(): void | |
| { | |
| $this->expectException(\InvalidArgumentException::class); | |
| $this->expectExceptionMessage('Invalid email DSN.'); | |
| EmailAdapter::fromDsn('smtp://'); | |
| } | |
| public function test_rejects_malformed_smtp_dsn(): void | |
| { | |
| $this->expectException(\InvalidArgumentException::class); | |
| $this->expectExceptionMessage('SMTP DSN must include a host.'); | |
| EmailAdapter::fromDsn('smtp://'); | |
| } |
| try { | ||
| return $adapter->send($message); | ||
| } catch (\Exception $e) { | ||
| $errors[] = $adapter->getName() | ||
| .' (adapter ' | ||
| .($index + 1) | ||
| .'): ' | ||
| .$e->getMessage(); | ||
| } |
There was a problem hiding this comment.
The catch block only catches
\Exception, so PHP \Error subclasses (\TypeError, \ValueError, \Error, etc.) thrown by an adapter will propagate uncaught and bypass the failover loop entirely. Catching \Throwable instead ensures all adapter-level failures are handled and the next adapter is tried.
| try { | |
| return $adapter->send($message); | |
| } catch (\Exception $e) { | |
| $errors[] = $adapter->getName() | |
| .' (adapter ' | |
| .($index + 1) | |
| .'): ' | |
| .$e->getMessage(); | |
| } | |
| try { | |
| return $adapter->send($message); | |
| } catch (\Throwable $e) { | |
| $errors[] = $adapter->getName() | |
| .' (adapter ' | |
| .($index + 1) | |
| .'): ' | |
| .$e->getMessage(); | |
| } |
Add DSN (Data Source Name) parsing support for email adapters, enabling connection configuration via DSN strings.