PlaceholderMailer   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 60
c 1
b 0
f 0
dl 0
loc 144
rs 10
wmc 15

5 Methods

Rating   Name   Duplication   Size   Complexity  
A loadTemplate() 0 23 3
A processContent() 0 13 2
A __construct() 0 12 4
A processConditionals() 0 11 2
A placeholderMessage() 0 23 4
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita\Mail
6
 */
7
namespace BEdita\Mail\Mailer;
8
9
use Cake\Mailer\Mailer;
10
use Cake\Utility\Hash;
11
12
/**
13
 * Mailer class to handle message body with placeholders.
14
 */
15
class PlaceholderMailer extends BaseMailer
16
{
17
    /**
18
     * @inheritDoc
19
     */
20
    public static string $name = 'placeholder';
21
22
    /**
23
     * Default template placeholder options.
24
     *
25
     * @var array
26
     */
27
    protected array $placeholderOptions = [
28
        'objectType' => 'mail_templates',
29
        'contentField' => 'body',
30
        'subjectField' => 'title',
31
    ];
32
33
    /**
34
     * @inheritDoc
35
     */
36
    public function __construct(array|string $config)
37
    {
38
        $placeholdeConfig = (array)static::getConfig(static::$name);
39
        if (empty($config) && !empty($placeholdeConfig)) {
40
            $config = $placeholdeConfig;
41
        } elseif (is_string($config)) {
42
            $config = (array)static::getConfig($config);
43
        }
44
        // override default placeholder options from config
45
        $options = (array)Hash::get((array)$config, 'placeholderOptions');
46
        $this->placeholderOptions = array_merge($this->placeholderOptions, $options);
47
        parent::__construct($config);
48
    }
49
50
    /**
51
     * Process text containing placeholders.
52
     *
53
     * @param string $text The text content.
54
     * @param array $vars The vars.
55
     * @return string
56
     */
57
    protected function processContent(string $text, array $vars): string
58
    {
59
        $matches = [];
60
        $text = $this->processConditionals($text, $vars);
61
        // Extract placeholders from content - format: {{placeholder}} {{object.attribute}}
62
        preg_match_all('/\{\{\s*[a-zA-Z1-9_\.]+\s*\}\}/', $text, $matches);
63
        foreach ($matches[0] as $match) {
64
            $key = trim(strtolower(str_replace(['{{', '}}'], '', $match)));
65
            $value = (string)Hash::get($vars, $key);
66
            $text = str_replace($match, $value, $text);
67
        }
68
69
        return $text;
70
    }
71
72
    /**
73
     * Process conditional logic in text content.
74
     *
75
     * @param string $text The text content.
76
     * @param array $vars The vars.
77
     * @return string
78
     */
79
    protected function processConditionals(string $text, array $vars): string
80
    {
81
        $pattern = '/\{\%\s*if\s+([a-zA-Z1-9_\.]+)\s*\%\}(.*?)\{\%\s*endif\s*\%\}/s';
82
83
        return preg_replace_callback($pattern, function ($matches) use ($vars) {
84
            $key = trim($matches[1]);
85
            $content = $matches[2];
86
            $value = Hash::get($vars, $key);
87
88
            return !empty($value) ? $content : '';
89
        }, $text);
90
    }
91
92
    /**
93
     * Load template elements in an assoc array with keys:
94
     *  - 'content': message content with placeholders
95
     *  - 'subject': message subject
96
     *
97
     * @param string $id Uname or ID of an object
98
     * @param array $options Template load options: 'objectType', 'contentField', 'subjectField'
99
     * @return array
100
     */
101
    protected function loadTemplate(string $id, array $options = []): array
102
    {
103
        $options = array_merge($this->placeholderOptions, $options);
104
        $lang = Hash::get($options, 'lang');
105
        $query = $this->fetchTable($options['objectType'])
106
            ->find('unameId', [$id])
107
            ->find('available');
108
        if ($lang) {
109
            $query->find('translations', compact('lang'));
110
        }
111
        $object = $query->firstOrFail();
112
        $content = (string)Hash::get($object, $options['contentField']);
113
        $subject = (string)Hash::get($object, $options['subjectField']);
114
115
        if (empty($lang)) {
116
            return compact('content', 'subject');
117
        }
118
        $contentPath = sprintf('translations.0.translated_fields.%s', $options['contentField']);
119
        $subjectPath = sprintf('translations.0.translated_fields.%s', $options['subjectField']);
120
121
        return [
122
            'content' => (string)Hash::get($object, $contentPath, $content),
123
            'subject' => (string)Hash::get($object, $subjectPath, $subject),
124
        ];
125
    }
126
127
    /**
128
     * Handle default placeholder message.
129
     *
130
     * @param string $name Template object unique name.
131
     * @param array $data The data.
132
     * @param array $config Email message config, including 'placeholderOptions'.
133
     * @param array $templateData The template data.
134
     * @return \Cake\Mailer\Mailer
135
     */
136
    public function placeholderMessage(string $name, array $data, array $config = [], array $templateData = []): Mailer
137
    {
138
        $nameConfig = (array)static::getConfig($name);
139
        $config = array_merge($nameConfig, $config);
140
141
        $options = (array)Hash::get($config, 'placeholderOptions');
142
        $options = array_merge($this->placeholderOptions, $options);
143
        $items = !empty($templateData) ? $templateData : $this->loadTemplate($name, $options);
144
145
        $body = $this->processContent($items['content'], $data);
146
        $this->setViewVars(compact('body'));
147
        // Use the placeholder template from the BEdita/Mail plugin (templates/email/placeholder)
148
        $this->viewBuilder()->setPlugin('BEdita/Mail')->setTemplate('placeholder');
149
        $subject = $this->processContent($items['subject'], $data);
150
        if (!empty($subject)) {
151
            $config['subject'] = $subject;
152
        }
153
154
        if (empty($config['transport'])) {
155
            $config['transport'] = Hash::get((array)static::getConfig('default'), 'transport', 'default');
156
        }
157
158
        return $this->setProfile($config);
159
    }
160
}
161