feca-mailshots-plugin/feca_mailshots_plugin/src/Admin/MailshotTestAdminPage.php

561 lines
27 KiB
PHP

<?php
declare(strict_types=1);
namespace FecaMailshots\Admin;
use FecaMailshots\Application\MailshotRunService;
use FecaMailshots\Application\MailshotService;
use FecaMailshots\WordPress\WordPressFacade;
final class MailshotTestAdminPage
{
use AdminRequestHelpers;
private const RESULT_OPTION_KEY = 'feca_mailshots_test_ui_result';
private const CAPABILITY = 'edit_pages';
private const NONCE_ACTION = 'feca_mailshots_test';
private const DOWNLOAD_MEMORY_LIMIT_ENV = 'FECA_MAILSHOTS_DOWNLOAD_MEMORY_LIMIT';
private const DOWNLOAD_MEMORY_LIMIT_OPTION = 'feca_mailshots_download_memory_limit';
/** @var callable(): MailshotRunService */
private $runServiceFactory;
/** @var callable(): MailshotService */
private $mailshotServiceFactory;
private WordPressFacade $wp;
/** @param callable(): MailshotRunService $runServiceFactory @param callable(): MailshotService $mailshotServiceFactory */
public function __construct(callable $runServiceFactory, callable $mailshotServiceFactory, WordPressFacade $wp)
{
$this->runServiceFactory = $runServiceFactory;
$this->mailshotServiceFactory = $mailshotServiceFactory;
$this->wp = $wp;
}
public function register(): void
{
$this->wp->addAction('admin_menu', [$this, 'registerMenu']);
$this->wp->addAction('admin_post_feca_mailshots_test_api', [$this, 'handleApi']);
$this->wp->addAction('admin_post_feca_mailshots_test_render_ui', [$this, 'handleRenderUi']);
$this->wp->addAction('admin_post_feca_mailshots_test_send_ui', [$this, 'handleSendUi']);
}
public function registerMenu(): void
{
$this->wp->addSubmenuPage('feca-mailshot', 'Mailshot Test', 'Mailshot Test', self::CAPABILITY, 'feca-mailshots-test', [$this, 'render']);
}
public function render(): void
{
if (!$this->wp->currentUserCan(self::CAPABILITY)) {
echo 'Permission denied';
return;
}
$mailshots = $this->mailshotService()->list();
$selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
if ($selectedMailshotId <= 0 && $mailshots !== []) {
$selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0);
}
$preview = ['ok' => false, 'rows' => [], 'errors' => ['Create a mailshot before loading recipients.']];
if ($selectedMailshotId > 0) {
$preview = $this->runService()->previewRecipients($selectedMailshotId, 100);
}
$rows = is_array($preview['rows'] ?? null) ? $preview['rows'] : [];
$selectedRecipientIndex = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
if ($selectedRecipientIndex < -1) {
$selectedRecipientIndex = 0;
}
$defaultEmail = $this->runService()->defaultTestEmail()['default_test_email'] ?? '';
$testEmail = (string) ($this->wp->requestParam('test_email', (string) $defaultEmail) ?? $defaultEmail);
$result = $this->result();
if ((string) ($this->wp->requestParam('render_test', '0') ?? '0') === '1') {
if ($selectedRecipientIndex < 0) {
$result = ['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']];
} else {
try {
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
$result = $this->runService()->renderTest($selectedMailshotId, $selectedRecipientIndex);
} catch (\Throwable $e) {
$result = ['ok' => false, 'errors' => ['Render test failed: ' . $e->getMessage()]];
}
}
$result['ui_action'] = 'render';
}
$action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
echo '<div class="wrap feca-mailshots-admin"><h1>Mailshot Test</h1>';
echo $this->renderAdminUiStyles();
echo '<p>Render with a selected recipient context, then optionally send one test email.</p>';
if ($result !== null) {
$ok = !empty($result['ok']);
$bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
echo '<div class="feca-banner ' . $bannerClass . '">';
echo '<strong>' . ($ok ? 'Test action succeeded.' : 'Test action failed.') . '</strong>';
if (!empty($result['errors']) && is_array($result['errors'])) {
echo '<p class="feca-banner-note">' . htmlspecialchars(implode('; ', $result['errors'])) . '</p>';
}
if (!empty($result['warnings']) && is_array($result['warnings'])) {
echo '<p class="feca-banner-note">Warnings: ' . htmlspecialchars(implode('; ', $result['warnings'])) . '</p>';
}
if (!empty($result['sent_to'])) {
echo '<p class="feca-banner-note">Sent to: <code>' . htmlspecialchars((string) $result['sent_to']) . '</code></p>';
}
if (!empty($result['sent_at'])) {
echo '<p class="feca-banner-note">Sent at: <code>' . htmlspecialchars((string) $result['sent_at']) . '</code></p>';
}
if (($result['ui_action'] ?? '') === 'render' && !empty($result['ok']) && is_array($result['rendered'] ?? null)) {
echo '<p class="feca-button-row"><button type="button" class="button button-primary" id="ms-open-render-preview">Open Render Preview</button></p>';
}
echo '</div>';
}
echo '<div class="feca-panel">';
echo '<h2 class="feca-section-title">1. Select Mailshot</h2>';
echo '<form method="get" action="' . htmlspecialchars($this->wp->adminUrl('admin.php')) . '" id="mst-mailshot-select-form">';
echo '<input type="hidden" name="page" value="feca-mailshots-test">';
echo '<div class="feca-control-row">';
echo '<div class="feca-control feca-control-min-360">';
echo '<label for="mst_mailshot_id"><strong>Mailshot</strong></label>';
echo '<select id="mst_mailshot_id" name="mailshot_id">';
foreach ($mailshots as $m) {
$id = (int) ($m['id'] ?? 0);
$sel = $id === $selectedMailshotId ? ' selected' : '';
$label = trim((string) ($m['Purpose'] ?? ''));
if ($label === '') {
$label = 'Mailshot #' . $id;
}
echo '<option value="' . $id . '"' . $sel . '>' . htmlspecialchars($label) . '</option>';
}
echo '</select></div>';
echo '</div>';
echo '</form>';
echo '<script>(function(){var select=document.getElementById("mst_mailshot_id");var form=document.getElementById("mst-mailshot-select-form");if(select&&form){select.addEventListener("change",function(){form.submit();});}})();</script>';
echo '</div>';
if (!empty($preview['errors'])) {
echo '<div class="feca-banner feca-banner-error">' . htmlspecialchars(implode('; ', $preview['errors'])) . '</div>';
}
echo '<div id="mst-inline-result"></div>';
echo '<form method="post" action="' . $action . '" class="feca-form" id="mst-action-form">';
echo '<h2 class="feca-section-title">2. Render / Send Test</h2>';
echo $this->hiddenNonceField(self::NONCE_ACTION);
echo '<input type="hidden" name="mailshot_id" value="' . $selectedMailshotId . '">';
echo '<div class="feca-control-row">';
echo '<div class="feca-control feca-control-min-360">';
echo '<label for="mst_recipient_index"><strong>Recipient row</strong></label>';
echo '<select id="mst_recipient_index" name="recipient_index" data-recipient-count="' . count($rows) . '">';
$allSel = $selectedRecipientIndex === -1 ? ' selected' : '';
echo '<option value="-1"' . $allSel . '>All recipients (send test email to address below for every row)</option>';
foreach ($rows as $r) {
$idx = (int) ($r['index'] ?? 0);
$sel = $idx === $selectedRecipientIndex ? ' selected' : '';
$email = (string) ($r['recipient_email'] ?? '');
$key = (string) ($r['recipient_key'] ?? '');
$text = $email !== '' ? $email : '(no email)';
if ($key !== '' && strcasecmp($key, $email) !== 0) {
$text .= ' | ' . $key;
}
echo '<option value="' . $idx . '"' . $sel . '>' . htmlspecialchars($text) . '</option>';
}
echo '</select></div>';
echo '<div class="feca-control feca-control-min-420">';
echo '<label for="mst_test_email"><strong>Test email address</strong></label>';
echo '<input id="mst_test_email" class="regular-text" type="email" name="test_email" value="' . htmlspecialchars($testEmail, ENT_QUOTES) . '">';
echo '</div>';
echo '<div class="feca-control"><label>&nbsp;</label><button class="button" type="submit" name="action" value="feca_mailshots_test_render_ui">Render Test (No Send)</button></div>';
echo '<div class="feca-control"><label>&nbsp;</label><button class="button button-primary" type="submit" name="action" value="feca_mailshots_test_send_ui" id="mst-send-test-button" onclick="return window.fecaConfirmTestSend ? window.fecaConfirmTestSend() : confirm(\'Send one test email to the entered address?\');">Send Test Email</button></div>';
echo '</div>';
echo '</form>';
echo '<script>(function(){';
echo 'var apiUrl=' . json_encode($this->wp->adminUrl('admin-post.php?action=feca_mailshots_test_api&op=send_test'), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
echo 'var form=document.getElementById("mst-action-form");';
echo 'var inlineResult=document.getElementById("mst-inline-result");';
echo 'var sendButton=document.getElementById("mst-send-test-button");';
echo 'function showResult(ok,title,lines){if(!inlineResult){return;}var wrap=document.createElement("div");wrap.className="feca-banner "+(ok?"feca-banner-success":"feca-banner-error");var strong=document.createElement("strong");strong.textContent=title;wrap.appendChild(strong);(lines||[]).forEach(function(line){var p=document.createElement("p");p.className="feca-banner-note";p.textContent=String(line||"");wrap.appendChild(p);});inlineResult.innerHTML="";inlineResult.appendChild(wrap);wrap.scrollIntoView({block:"nearest"});}';
echo 'if(form){form.addEventListener("submit",function(ev){var submitter=ev.submitter;if(!submitter||String(submitter.value||"")!=="feca_mailshots_test_send_ui"){return;}ev.preventDefault();var payload=new URLSearchParams(new FormData(form));payload.delete("action");if(sendButton){sendButton.disabled=true;}showResult(true,"Sending test email...",["Rendering PDF attachment and sending message."]);fetch(apiUrl,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:payload.toString()}).then(function(response){return response.text().then(function(text){var data=null;try{data=JSON.parse(text);}catch(e){return {ok:false,errors:["Send test failed: server returned HTTP "+response.status+" instead of JSON. Check the WordPress/PHP error log for the underlying fatal error."]};}if(!response.ok&&data&&data.ok!==false){data.ok=false;}return data;});}).then(function(data){if(data&&data.ok){var lines=[];if(data.sent_to){lines.push("Sent to: "+data.sent_to);}if(data.sent_at){lines.push("Sent at: "+data.sent_at);}if(data.warnings&&data.warnings.length){lines=lines.concat(data.warnings.map(function(w){return "Warning: "+w;}));}showResult(true,"Test action succeeded.",lines.length?lines:["Sent."]);return;}var errors=(data&&data.errors&&data.errors.length)?data.errors:[(data&&data.error)?data.error:"Unknown send-test failure."];showResult(false,"Test action failed.",errors);}).catch(function(error){showResult(false,"Test action failed.",[error&&error.message?error.message:"Request failed."]);}).finally(function(){if(sendButton){sendButton.disabled=false;}});});}';
echo 'window.fecaConfirmTestSend=function(){';
echo 'var select=document.getElementById("mst_recipient_index");';
echo 'if(!select){return confirm("Send one test email to the entered address?");}';
echo 'var value=String(select.value||"0");';
echo 'if(value==="-1"){';
echo 'var count=parseInt(select.getAttribute("data-recipient-count")||"0",10);';
echo 'if(!Number.isFinite(count)||count<0){count=0;}';
echo 'return confirm("Send "+count+" emails to the entered address?");';
echo '}';
echo 'return confirm("Send one test email to the entered address?");';
echo '};';
echo '})();</script>';
if ($rows !== []) {
$sampleColumns = $this->sampleColumns($rows, 4);
echo '<div class="feca-panel">';
echo '<h2 class="feca-section-title">Recipient Sample (First 20)</h2>';
echo '<div class="feca-scroll-frame"><div class="feca-scroll-pane">';
echo '<table class="widefat striped"><thead><tr><th>#</th><th>Key</th><th>Email</th>';
foreach ($sampleColumns as $col) {
echo '<th>' . htmlspecialchars($col) . '</th>';
}
echo '</tr></thead><tbody>';
foreach (array_slice($rows, 0, 20) as $r) {
$rowData = is_array($r['row'] ?? null) ? $r['row'] : [];
echo '<tr><td>' . (int) ($r['index'] ?? 0) . '</td><td>' . htmlspecialchars((string) ($r['recipient_key'] ?? '')) . '</td><td>' . htmlspecialchars((string) ($r['recipient_email'] ?? '')) . '</td>';
foreach ($sampleColumns as $col) {
echo '<td>' . htmlspecialchars($this->displayCellValue($rowData[$col] ?? null)) . '</td>';
}
echo '</tr>';
}
echo '</tbody></table>';
echo '</div></div>';
echo '</div>';
}
if ($result !== null && ($result['ui_action'] ?? '') === 'render' && !empty($result['ok']) && is_array($result['rendered'] ?? null)) {
$rendered = $result['rendered'];
$recipient = $this->selectedRecipientSummary($rows, $selectedRecipientIndex);
$recipientText = $recipient['email'] ?? '';
if (($recipient['key'] ?? '') !== '' && strcasecmp((string) ($recipient['key'] ?? ''), (string) ($recipient['email'] ?? '')) !== 0) {
$recipientText .= ($recipientText !== '' ? ' | ' : '') . (string) $recipient['key'];
}
if ($recipientText === '') {
$recipientText = '(recipient unavailable)';
}
$subject = (string) ($rendered['subject'] ?? '');
$messageHtml = (string) ($rendered['message'] ?? '');
$pdfHtml = (string) ($rendered['pdf_attachment'] ?? '');
echo '<div id="ms-render-preview-modal" class="feca-modal-overlay-high">';
echo '<div class="feca-modal-shell feca-modal-shell-wide">';
echo '<h2 class="feca-modal-title">Render Preview</h2>';
echo '<p class="feca-modal-subtitle"><strong>Recipient:</strong> ' . htmlspecialchars($recipientText) . '</p>';
echo '<p class="feca-modal-subtitle"><strong>Subject:</strong> ' . htmlspecialchars($subject) . '</p>';
echo '<div class="feca-preview-grid">';
echo '<div><h3 class="feca-modal-subtitle">Message (HTML)</h3><iframe sandbox="" class="feca-preview-iframe" id="ms-render-message-frame"></iframe></div>';
echo '<div><h3 class="feca-modal-subtitle">PDF Attachment (HTML)</h3><iframe sandbox="" class="feca-preview-iframe" id="ms-render-pdf-frame"></iframe></div>';
echo '</div>';
echo '<p class="feca-button-row"><button type="button" class="button button-primary" id="ms-close-render-preview">Close</button></p>';
echo '</div></div>';
echo '<script>(function(){';
echo 'var messageHtml=' . json_encode($messageHtml, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
echo 'var pdfHtml=' . json_encode($pdfHtml, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
echo 'var modal=document.getElementById("ms-render-preview-modal");';
echo 'var open=document.getElementById("ms-open-render-preview");';
echo 'var close=document.getElementById("ms-close-render-preview");';
echo 'var messageFrame=document.getElementById("ms-render-message-frame");';
echo 'var pdfFrame=document.getElementById("ms-render-pdf-frame");';
echo 'if(messageFrame){messageFrame.srcdoc=messageHtml;}';
echo 'if(pdfFrame){pdfFrame.srcdoc=pdfHtml;}';
echo 'if(!modal){return;}';
echo 'var show=function(){modal.style.display="block";};';
echo 'var hide=function(){modal.style.display="none";};';
echo 'if(open){open.addEventListener("click",show);}';
echo 'if(close){close.addEventListener("click",hide);}';
echo 'modal.addEventListener("click",function(ev){if(ev.target===modal){hide();}});';
echo 'document.addEventListener("keydown",function(ev){if(ev.key==="Escape"&&modal.style.display==="block"){hide();}});';
echo 'show();';
echo '})();</script>';
}
echo '</div>';
}
public function handleApi(): void
{
if (!$this->enforceCapabilityOrJson(self::CAPABILITY)) {
return;
}
$op = (string) ($this->wp->requestParam('op', '') ?? '');
$service = $this->runService();
try {
if ($op === 'default_test_email') {
$this->wp->sendJson(['ok' => true] + $service->defaultTestEmail());
return;
}
if ($op === 'render_test') {
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
return;
}
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
if ($idx < 0) {
$this->wp->sendJson(['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']]);
return;
}
$this->wp->sendJson($service->renderTest($mailshotId, $idx));
return;
}
if ($op === 'send_test') {
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
return;
}
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
$to = (string) ($this->wp->requestParam('test_email', '') ?? '');
if ($idx < 0) {
$this->wp->sendJson($service->sendTestAll($mailshotId, $to));
return;
}
$this->wp->sendJson($service->sendTest($mailshotId, $idx, $to));
return;
}
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400);
} catch (\Throwable $e) {
$this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
}
}
public function handleRenderUi(): void
{
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
return;
}
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
if ($idx < 0) {
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
return;
}
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
}
public function handleSendUi(): void
{
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
return;
}
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
$email = (string) ($this->wp->requestParam('test_email', '') ?? '');
try {
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
if ($idx < 0) {
$result = $this->runService()->sendTestAll($mailshotId, $email);
} else {
$result = $this->runService()->sendTest($mailshotId, $idx, $email);
}
} catch (\Throwable $e) {
$result = ['ok' => false, 'errors' => ['Send test failed: ' . $e->getMessage()]];
}
unset($result['rendered']);
$result['ui_action'] = 'send';
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
$this->redirect($mailshotId, $idx, $email);
}
private function redirect(int $mailshotId, int $recipientIndex, string $testEmail, bool $renderTest = false): void
{
$url = $this->wp->adminUrl('admin.php?page=feca-mailshots-test&mailshot_id=' . $mailshotId . '&recipient_index=' . $recipientIndex . '&test_email=' . rawurlencode($testEmail));
if ($renderTest) {
$url .= '&render_test=1';
}
if (!headers_sent()) {
header('Location: ' . $url, true, 302);
exit;
}
}
/** @return array<string,mixed>|null */
private function result(): ?array
{
$raw = $this->wp->getOption(self::RESULT_OPTION_KEY, null);
$this->wp->deleteOption(self::RESULT_OPTION_KEY);
return is_array($raw) ? $raw : null;
}
private function maybeRaiseMemoryLimit(string $target): void
{
if ($target === '' || !function_exists('ini_get') || !function_exists('ini_set')) {
return;
}
$current = (string) ini_get('memory_limit');
$currentBytes = $this->memoryLimitToBytes($current);
$targetBytes = $this->memoryLimitToBytes($target);
if ($currentBytes < 0 || $targetBytes <= 0 || $currentBytes >= $targetBytes) {
return;
}
@ini_set('memory_limit', $target);
}
private function resolveDownloadMemoryLimitTarget(): string
{
$optionValue = $this->wp->getOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, '');
if (is_string($optionValue)) {
$value = trim($optionValue);
if ($this->memoryLimitToBytes($value) > 0) {
return $value;
}
}
$envValue = getenv(self::DOWNLOAD_MEMORY_LIMIT_ENV);
if (is_string($envValue)) {
$value = trim($envValue);
if ($this->memoryLimitToBytes($value) > 0) {
return $value;
}
}
return '';
}
private function memoryLimitToBytes(string $limit): int
{
$value = trim($limit);
if ($value === '') {
return 0;
}
if ($value === '-1') {
return -1;
}
$unit = strtolower(substr($value, -1));
if (ctype_alpha($unit)) {
$number = (float) substr($value, 0, -1);
switch ($unit) {
case 'g':
return (int) ($number * 1024 * 1024 * 1024);
case 'm':
return (int) ($number * 1024 * 1024);
case 'k':
return (int) ($number * 1024);
default:
return (int) $number;
}
}
return (int) $value;
}
private function runService(): MailshotRunService
{
return ($this->runServiceFactory)();
}
private function mailshotService(): MailshotService
{
return ($this->mailshotServiceFactory)();
}
/**
* @param list<array<string,mixed>> $rows
* @return list<string>
*/
private function sampleColumns(array $rows, int $maxColumns): array
{
if ($rows === [] || $maxColumns <= 0) {
return [];
}
$firstRow = is_array($rows[0]['row'] ?? null) ? $rows[0]['row'] : [];
if ($firstRow === []) {
return [];
}
$firstRecipientKey = trim((string) ($rows[0]['recipient_key'] ?? ''));
$firstRecipientEmail = trim((string) ($rows[0]['recipient_email'] ?? ''));
$selected = [];
$seenValues = [];
foreach (array_keys($firstRow) as $key) {
$name = (string) $key;
if ($name === '') {
continue;
}
if ($this->isIdLikeField($name) || $this->isEmailLikeField($name) || $this->isKeyLikeField($name)) {
continue;
}
$sampleValue = trim($this->displayCellValue($firstRow[$name] ?? null));
if ($sampleValue !== '') {
$valueKey = strtolower($sampleValue);
if ($valueKey === strtolower($firstRecipientKey) || $valueKey === strtolower($firstRecipientEmail)) {
continue;
}
if (isset($seenValues[$valueKey])) {
continue;
}
$seenValues[$valueKey] = true;
}
$selected[] = $name;
if (count($selected) >= $maxColumns) {
break;
}
}
return $selected;
}
private function isIdLikeField(string $field): bool
{
$name = strtolower($field);
return $name === 'id'
|| str_ends_with($name, '.id')
|| str_ends_with($name, '_id')
|| str_contains($name, 'accountid');
}
private function isEmailLikeField(string $field): bool
{
$name = strtolower($field);
return $name === 'email' || str_ends_with($name, '.email') || str_contains($name, 'email');
}
private function isKeyLikeField(string $field): bool
{
$name = strtolower($field);
return $name === 'key'
|| str_ends_with($name, '.key')
|| str_contains($name, 'recipient_key')
|| str_contains($name, 'contact_key');
}
/** @param mixed $value */
private function displayCellValue($value): string
{
if ($value === null) {
return '';
}
if (is_scalar($value)) {
return trim((string) $value);
}
return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
}
/**
* @param list<array<string,mixed>> $rows
* @return array{index:int,key:string,email:string}|array{}
*/
private function selectedRecipientSummary(array $rows, int $recipientIndex): array
{
foreach ($rows as $row) {
$idx = (int) ($row['index'] ?? -1);
if ($idx !== $recipientIndex) {
continue;
}
return [
'index' => $idx,
'key' => trim((string) ($row['recipient_key'] ?? '')),
'email' => trim((string) ($row['recipient_email'] ?? '')),
];
}
return [];
}
}