HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
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'),
        ];
    }
}