Skip to content

Commit 2e823a4

Browse files
fix: harden Messenger validation and review feedback
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.
1 parent c1c2409 commit 2e823a4

2 files changed

Lines changed: 95 additions & 33 deletions

File tree

src/Utopia/Messaging/Messenger.php

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class Messenger
3737
* At least one adapter must be provided.
3838
* All adapters must support the same message type.
3939
*
40-
* @throws \InvalidArgumentException If no adapters are provided or adapters have mixed types.
40+
* @throws \InvalidArgumentException If no adapters are provided, an array element is not an adapter, or adapters have mixed types.
4141
*/
4242
public function __construct(Adapter|array $adapters)
4343
{
@@ -49,6 +49,18 @@ public function __construct(Adapter|array $adapters)
4949
throw new \InvalidArgumentException('At least one adapter must be provided.');
5050
}
5151

52+
foreach ($adapters as $index => $adapter) {
53+
if (! $adapter instanceof Adapter) {
54+
throw new \InvalidArgumentException(
55+
'All elements must be instances of Adapter, but element '
56+
.$index
57+
.' is '
58+
.\get_debug_type($adapter)
59+
.'.'
60+
);
61+
}
62+
}
63+
5264
$this->validateAdapters($adapters);
5365

5466
$this->adapters = $adapters;
@@ -76,31 +88,38 @@ public function send(Message $message): array
7688
$messageType = $this->adapters[0]->getMessageType();
7789

7890
if (! \is_a($message, $messageType)) {
79-
throw new \Exception(sprintf(
80-
'Invalid message type. Expected "%s", got "%s".',
81-
$messageType,
82-
get_class($message)
83-
));
91+
throw new \Exception(
92+
'Invalid message type. Expected "'
93+
.$messageType
94+
.'", got "'
95+
.\get_class($message)
96+
.'".'
97+
);
8498
}
8599

86100
foreach ($this->adapters as $index => $adapter) {
87101
try {
88102
return $adapter->send($message);
89103
} catch (\Exception $e) {
90-
$errors[] = sprintf(
91-
'%s (adapter %d): %s',
92-
$adapter->getName(),
93-
$index + 1,
94-
$e->getMessage()
95-
);
104+
$errors[] = $adapter->getName()
105+
.' (adapter '
106+
.($index + 1)
107+
.'): '
108+
.$e->getMessage();
96109
}
97110
}
98111

99-
throw new \Exception(sprintf(
100-
"All %d adapters failed:\n%s",
101-
count($this->adapters),
102-
implode("\n", $errors)
103-
));
112+
$adapterCount = \count($this->adapters);
113+
$adapterLabel = $adapterCount === 1 ? 'adapter' : 'adapters';
114+
115+
throw new \Exception(
116+
'All '
117+
.$adapterCount
118+
.' '
119+
.$adapterLabel
120+
." failed:\n"
121+
.\implode("\n", $errors)
122+
);
104123
}
105124

106125
/**
@@ -151,25 +170,33 @@ private function validateAdapters(array $adapters): void
151170
$expectedType = $firstAdapter->getType();
152171
$expectedMessageType = $firstAdapter->getMessageType();
153172

154-
foreach ($adapters as $index => $adapter) {
173+
foreach (\array_slice($adapters, 1, preserve_keys: true) as $index => $adapter) {
155174
if ($adapter->getType() !== $expectedType) {
156-
throw new \InvalidArgumentException(sprintf(
157-
'All adapters must be of the same type. Expected "%s", but adapter %d (%s) has type "%s".',
158-
$expectedType,
159-
$index + 1,
160-
$adapter->getName(),
161-
$adapter->getType()
162-
));
175+
throw new \InvalidArgumentException(
176+
'All adapters must be of the same type. Expected "'
177+
.$expectedType
178+
.'", but adapter '
179+
.($index + 1)
180+
.' ('
181+
.$adapter->getName()
182+
.') has type "'
183+
.$adapter->getType()
184+
.'".'
185+
);
163186
}
164187

165188
if ($adapter->getMessageType() !== $expectedMessageType) {
166-
throw new \InvalidArgumentException(sprintf(
167-
'All adapters must support the same message type. Expected "%s", but adapter %d (%s) supports "%s".',
168-
$expectedMessageType,
169-
$index + 1,
170-
$adapter->getName(),
171-
$adapter->getMessageType()
172-
));
189+
throw new \InvalidArgumentException(
190+
'All adapters must support the same message type. Expected "'
191+
.$expectedMessageType
192+
.'", but adapter '
193+
.($index + 1)
194+
.' ('
195+
.$adapter->getName()
196+
.') supports "'
197+
.$adapter->getMessageType()
198+
.'".'
199+
);
173200
}
174201
}
175202
}

tests/Messaging/MessengerTest.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,36 @@ public function test_throws_when_single_adapter_fails(): void
164164
);
165165

166166
$this->expectException(\Exception::class);
167-
$this->expectExceptionMessage('All 1 adapters failed');
167+
$this->expectExceptionMessage('All 1 adapter failed');
168168
$this->expectExceptionMessage('OnlyAdapter (adapter 1): Network error');
169169

170170
$messenger->send($message);
171171
}
172172

173+
public function test_accepts_single_adapter_instance(): void
174+
{
175+
$adapter = $this->createMock(Adapter::class);
176+
$adapter->method('getName')->willReturn('OnlyAdapter');
177+
$adapter->method('getType')->willReturn('sms');
178+
$adapter->method('getMessageType')->willReturn(SMS::class);
179+
$adapter->method('getMaxMessagesPerRequest')->willReturn(100);
180+
$adapter->method('send')->willReturn([
181+
'deliveredTo' => 1,
182+
'type' => 'sms',
183+
'results' => [['recipient' => '+1234567890', 'status' => 'success', 'error' => '']],
184+
]);
185+
186+
$messenger = new Messenger($adapter);
187+
188+
$result = $messenger->send(new SMS(
189+
to: ['+1234567890'],
190+
content: 'Test message'
191+
));
192+
193+
$this->assertEquals(1, $result['deliveredTo']);
194+
$this->assertEquals('success', $result['results'][0]['status']);
195+
}
196+
173197
public function test_rejects_empty_adapter_list(): void
174198
{
175199
$this->expectException(\InvalidArgumentException::class);
@@ -178,6 +202,17 @@ public function test_rejects_empty_adapter_list(): void
178202
new Messenger([]);
179203
}
180204

205+
public function test_rejects_non_adapter_array_element(): void
206+
{
207+
$this->expectException(\InvalidArgumentException::class);
208+
$this->expectExceptionMessage('All elements must be instances of Adapter, but element 1 is string.');
209+
210+
new Messenger([
211+
$this->createMock(Adapter::class),
212+
'not-an-adapter',
213+
]);
214+
}
215+
181216
public function test_rejects_mixed_adapter_types(): void
182217
{
183218
$smsAdapter = $this->createMock(Adapter::class);

0 commit comments

Comments
 (0)