File: /home/xedaptot/ai.naniguide.com/app/Services/PersonalizationTagCatalog.php
<?php
namespace App\Services;
use App\Model\Campaign;
use App\Model\MailList;
/**
* Lists available personalization tag keys + metadata for UI autosuggest.
*
* Complement to App\Library\Contracts\TagResolverInterface:
* - TagResolverInterface::getResolvedTags() resolves VALUES (runtime send pipeline)
* - PersonalizationTagCatalog lists KEYS + metadata (design-time UI)
*
* When editing Campaign::getResolvedTags() / Subscriber::getResolvedTags() /
* MailList::getResolvedTags() / GeneralTagResolver / AdvancedSubscriberTagResolver,
* update this catalog in the SAME PR. A drift test asserts keys stay in sync.
*
* @phpstan-type TagCatalogItem array{key: string, label: string, description: string, snippet: string, group: string}
*/
class PersonalizationTagCatalog
{
/**
* Tags available in campaign subject / content.
*
* Without list: only campaign-scoped tags + general (date/time) tags.
* With list: adds list.*, subscriber.* (core + custom fields), global aliases, advanced URLs.
*
* @return array<int, TagCatalogItem>
*/
public function campaignTags(?MailList $list = null): array
{
$items = array_merge(
$this->campaignScopeItems(),
$this->generalScopeItems(),
);
if ($list !== null) {
$items = array_merge(
$items,
$this->listScopeItems(),
$this->subscriberScopeItems(),
$this->subscriberCustomFieldItems($list),
$this->advancedUrlItems(),
);
}
return $items;
}
/**
* Tags available in a MailList context (e.g. form autoresponders).
*
* @return array<int, TagCatalogItem>
*/
public function listTags(MailList $list): array
{
return array_merge(
$this->listScopeItems(),
$this->subscriberScopeItems(),
$this->subscriberCustomFieldItems($list),
$this->generalScopeItems(),
);
}
/**
* Shape an item for autosuggest consumption.
*
* @return TagCatalogItem
*/
private function item(string $key, string $label, string $description, ?string $group = null): array
{
if ($group === null) {
$group = str_contains($key, '.') ? explode('.', $key, 2)[0] : 'global';
}
return [
'key' => $key,
'label' => $label,
'description' => $description,
'snippet' => '{{' . $key . '}}',
'group' => $group,
];
}
/**
* Mirrors Campaign::getResolvedTags() keys.
*
* @return array<int, TagCatalogItem>
*/
private function campaignScopeItems(): array
{
return [
$this->item('campaign.name', 'Campaign name', 'The name of this campaign'),
$this->item('campaign.uid', 'Campaign UID', 'Unique campaign identifier'),
$this->item('campaign.from_email', 'From email', 'Sender email address'),
$this->item('campaign.from_name', 'From name', 'Sender display name'),
$this->item('campaign.subject', 'Subject', 'The campaign subject line'),
$this->item('campaign.reply_to', 'Reply-to', 'Reply-to address'),
];
}
/**
* Mirrors MailList::getResolvedTags() keys.
*
* @return array<int, TagCatalogItem>
*/
private function listScopeItems(): array
{
return [
$this->item('list.name', 'List name', 'The mailing list name'),
$this->item('list.uid', 'List UID', 'Unique list identifier'),
$this->item('list.from_email', 'List from email', 'Default sender email for this list'),
$this->item('list.from_name', 'List from name', 'Default sender name for this list'),
];
}
/**
* Mirrors Subscriber::getResolvedTags() core (non-custom) keys + global aliases.
*
* @return array<int, TagCatalogItem>
*/
private function subscriberScopeItems(): array
{
return [
$this->item('subscriber.uid', 'Subscriber UID', 'Unique subscriber identifier'),
$this->item('subscriber.email', 'Subscriber email', 'Subscriber email address'),
$this->item('subscriber.name', 'Full name', 'First and last name combined'),
$this->item('subscriber.full_name', 'Full name (alias)','Alias for subscriber.name'),
$this->item('subscriber.fullname', 'Full name (alias)','Alias for subscriber.name'),
// Global aliases exposed unprefixed by Subscriber::getResolvedTags()
$this->item('email', 'Email', 'Subscriber email (global alias)', 'global'),
$this->item('name', 'Name', 'Subscriber full name (global alias)', 'global'),
$this->item('full_name', 'Full name', 'Subscriber full name (global alias)', 'global'),
$this->item('fullname', 'Fullname', 'Subscriber full name (global alias)', 'global'),
$this->item('first_name', 'First name', 'Subscriber first name (global alias)', 'global'),
$this->item('last_name', 'Last name', 'Subscriber last name (global alias)', 'global'),
];
}
/**
* List custom fields, skipping tags already covered by core subscriber keys.
*
* @return array<int, TagCatalogItem>
*/
/**
* @return array<int, TagCatalogItem>
*/
private function subscriberCustomFieldItems(MailList $list): array
{
// Skip only those already covered by the static subscriber scope items above.
$alreadyCovered = ['UID', 'EMAIL'];
$items = [];
foreach ($list->fields as $field) {
if (empty($field->tag)) {
continue;
}
$upper = strtoupper($field->tag);
if (in_array($upper, $alreadyCovered, true)) {
continue;
}
$key = 'subscriber.' . strtolower($field->tag);
$label = $field->label !== null && $field->label !== '' ? $field->label : $field->tag;
$items[] = $this->item($key, $label, 'Custom list field: ' . $label);
}
return $items;
}
/**
* Mirrors GeneralTagResolver::getResolvedTags() keys.
*
* @return array<int, TagCatalogItem>
*/
private function generalScopeItems(): array
{
return [
$this->item('current_year', 'Current year', 'Current year (YYYY)', 'global'),
$this->item('current_month', 'Current month', 'Current month (MM)', 'global'),
$this->item('current_month_name', 'Current month name', 'Current month name (January, …)', 'global'),
$this->item('current_day', 'Current day', 'Current day of month (DD)', 'global'),
$this->item('current_weekday', 'Current weekday', 'Current weekday name', 'global'),
$this->item('current_date', 'Current date', 'Current date (YYYY-MM-DD)', 'global'),
$this->item('current_time', 'Current time', 'Current time (HH:MM:SS)', 'global'),
$this->item('current_datetime', 'Current datetime', 'Current datetime (YYYY-MM-DD …)', 'global'),
];
}
/**
* Mirrors extra keys added by AdvancedSubscriberTagResolver.
*
* @return array<int, TagCatalogItem>
*/
private function advancedUrlItems(): array
{
return [
$this->item('unsubscribe_url', 'Unsubscribe URL', 'Per-recipient unsubscribe link', 'global'),
$this->item('update_profile_url', 'Update profile URL', 'Link to subscriber profile form', 'global'),
$this->item('web_view_url', 'Web view URL', 'Link to view email on the web', 'global'),
];
}
}