EmailTask fails with Unknown named parameter when address settings are associative arrays
Summary
Queue\Queue\Task\EmailTask can fail with Unknown named parameter when rebuilding an email from queued JSON data and the email address settings are stored as associative arrays, for example:
[
'to' => [
'recipient@example.com' => 'Recipient Name',
],
'from' => [
'sender@example.com' => 'Sender Name',
],
]
Those address maps are valid CakePHP mailer inputs, but EmailTask expands them through call_user_func_array(). On PHP 8+, string keys in the argument array are treated as named parameters, so the email address becomes a parameter name.
Current behavior
Given this queued payload:
{
"settings": {
"to": {
"recipient@example.com": "Recipient Name"
},
"from": {
"sender@example.com": "Sender Name"
}
},
"content": "Test message"
}
EmailTask eventually runs:
$setter = 'setTo';
$setting = [
'recipient@example.com' => 'Recipient Name',
];
call_user_func_array([$this->mailer, $setter], (array)$setting);
On PHP 8+, this is interpreted similarly to:
$this->mailer->setTo(recipient@example.com: 'Recipient Name');
That fails because setTo() does not have a parameter named recipient@example.com.
Error:
Unknown named parameter $recipient@example.com
The same issue can happen with from, and potentially with other address settings that accept an associative email => name map.
Expected behavior
EmailTask should accept associative address arrays as a single positional argument, matching CakePHP's mailer API:
$this->mailer->setTo([
'recipient@example.com' => 'Recipient Name',
]);
Why this became visible
Older queue payloads could preserve richer PHP values through PHP serialization. After moving queued_jobs.data to JSON, the worker receives plain arrays and needs to rebuild the email from settings.
When those settings contain associative address maps, the current call_user_func_array() usage expands the map incorrectly under PHP 8 named-argument semantics.
Reproduction
$data = [
'settings' => [
'to' => [
'recipient@example.com' => 'Recipient Name',
],
'from' => [
'sender@example.com' => 'Sender Name',
],
'subject' => 'Test',
],
'content' => 'Test message',
];
$task->run($data, 1);
Expected: the task sends or prepares the email.
Actual: the task throws Unknown named parameter.
Possible fix
Normalize the argument list passed to call_user_func_array().
For address setters such as to, from, cc, bcc, and replyTo, if the setting is an associative array, wrap it as a single positional argument instead of expanding the address map itself:
if (in_array($method, ['to', 'from', 'cc', 'bcc', 'replyTo'], true) && is_array($setting)) {
$isAssociative = array_keys($setting) !== range(0, count($setting) - 1);
if ($isAssociative) {
call_user_func_array([$this->mailer, $setter], [$setting]);
continue;
}
}
call_user_func_array([$this->mailer, $setter], (array)$setting);
This keeps the existing tuple-style payload working:
'to' => ['recipient@example.com', 'Recipient Name']
while also supporting CakePHP's associative address map format:
'to' => [
'recipient@example.com' => 'Recipient Name',
]
Alternative possible fix
Handle address settings explicitly before the generic settings loop.
Address setters such as setTo(), setFrom(), setCc(), setBcc(), and setReplyTo() already accept an associative email => name array as their first argument. These settings can be consumed before the generic call_user_func_array() path:
$addressArguments = static function (mixed $setting): array {
if (
is_array($setting)
&& array_is_list($setting)
&& count($setting) === 2
&& is_string($setting[0])
&& ($setting[1] === null || is_string($setting[1]))
) {
return $setting;
}
return [$setting];
};
if (array_key_exists('to', $settings)) {
$this->mailer->setTo(...$addressArguments($settings['to']));
unset($settings['to']);
}
if (array_key_exists('from', $settings)) {
$this->mailer->setFrom(...$addressArguments($settings['from']));
unset($settings['from']);
}
if (array_key_exists('cc', $settings)) {
$this->mailer->setCc(...$addressArguments($settings['cc']));
unset($settings['cc']);
}
if (array_key_exists('bcc', $settings)) {
$this->mailer->setBcc(...$addressArguments($settings['bcc']));
unset($settings['bcc']);
}
if (array_key_exists('replyTo', $settings)) {
$this->mailer->setReplyTo(...$addressArguments($settings['replyTo']));
unset($settings['replyTo']);
}
if (array_key_exists('theme', $settings)) {
$this->mailer->viewBuilder()->setTheme($settings['theme']);
unset($settings['theme']);
}
if (array_key_exists('template', $settings)) {
$this->mailer->viewBuilder()->setTemplate($settings['template']);
unset($settings['template']);
}
if (array_key_exists('layout', $settings)) {
$this->mailer->viewBuilder()->setLayout($settings['layout']);
unset($settings['layout']);
}
if (array_key_exists('helper', $settings)) {
$this->mailer->viewBuilder()->addHelper(...(array)$settings['helper']);
unset($settings['helper']);
}
if (array_key_exists('helpers', $settings)) {
$this->mailer->viewBuilder()->addHelpers($settings['helpers']);
unset($settings['helpers']);
}
if (array_key_exists('attachments', $settings)) {
$this->mailer->setAttachments($settings['attachments']);
unset($settings['attachments']);
}
foreach ($settings as $method => $setting) {
$setter = 'set' . ucfirst($method);
call_user_func_array([$this->mailer, $setter], (array)$setting);
}
This keeps the generic loop only for ordinary mailer settings and makes the special cases explicit before the fallback path.
A regression test should cover to and from using the associative email => name format.
EmailTask fails with
Unknown named parameterwhen address settings are associative arraysSummary
Queue\Queue\Task\EmailTaskcan fail withUnknown named parameterwhen rebuilding an email from queued JSON data and the email address settings are stored as associative arrays, for example:[ 'to' => [ 'recipient@example.com' => 'Recipient Name', ], 'from' => [ 'sender@example.com' => 'Sender Name', ], ]Those address maps are valid CakePHP mailer inputs, but
EmailTaskexpands them throughcall_user_func_array(). On PHP 8+, string keys in the argument array are treated as named parameters, so the email address becomes a parameter name.Current behavior
Given this queued payload:
{ "settings": { "to": { "recipient@example.com": "Recipient Name" }, "from": { "sender@example.com": "Sender Name" } }, "content": "Test message" }EmailTaskeventually runs:On PHP 8+, this is interpreted similarly to:
That fails because
setTo()does not have a parameter namedrecipient@example.com.Error:
Unknown named parameter $recipient@example.comThe same issue can happen with
from, and potentially with other address settings that accept an associativeemail => namemap.Expected behavior
EmailTaskshould accept associative address arrays as a single positional argument, matching CakePHP's mailer API:Why this became visible
Older queue payloads could preserve richer PHP values through PHP serialization. After moving
queued_jobs.datato JSON, the worker receives plain arrays and needs to rebuild the email fromsettings.When those settings contain associative address maps, the current
call_user_func_array()usage expands the map incorrectly under PHP 8 named-argument semantics.Reproduction
Expected: the task sends or prepares the email.
Actual: the task throws
Unknown named parameter.Possible fix
Normalize the argument list passed to
call_user_func_array().For address setters such as
to,from,cc,bcc, andreplyTo, if the setting is an associative array, wrap it as a single positional argument instead of expanding the address map itself:This keeps the existing tuple-style payload working:
while also supporting CakePHP's associative address map format:
Alternative possible fix
Handle address settings explicitly before the generic settings loop.
Address setters such as
setTo(),setFrom(),setCc(),setBcc(), andsetReplyTo()already accept an associativeemail => namearray as their first argument. These settings can be consumed before the genericcall_user_func_array()path:This keeps the generic loop only for ordinary mailer settings and makes the special cases explicit before the fallback path.
A regression test should cover
toandfromusing the associativeemail => nameformat.