Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.45% covered (success)
99.45%
180 / 181
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
EmailBlockRegistry
99.44% covered (success)
99.44%
179 / 180
75.00% covered (warning)
75.00%
3 / 4
5
0.00% covered (danger)
0.00%
0 / 1
 getAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPasswordResetDefaultBlocks
100.00% covered (success)
100.00%
177 / 177
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Mail\Domain\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Domain Registry for Email Template Block Definitions.
13 *
14 * Exclusively defines block types with the "email_" prefix tailored for email clients.
15 *
16 * @package App\Modules\Mail\Domain\Service
17 */
18final class EmailBlockRegistry
19{
20    private const string PRIMARY_ACCENT_COLOR = '#206bc4';
21
22    /** @var array<string, array<string, mixed>> */
23    private static array $definitions = [
24        'email_preheader' => [
25            'type'        => 'email_preheader',
26            'label'       => 'Preheader (Preview Text)',
27            'icon'        => 'bi bi-eye',
28            'category'    => 'header',
29            'description' => 'Hidden snippet text shown next to subject line in inbox',
30            'defaultProps' => [
31                'text' => 'Ważna wiadomość z systemu Ammonly...',
32            ],
33        ],
34        'email_header_logo' => [
35            'type'        => 'email_header_logo',
36            'label'       => 'Logo & Branding',
37            'icon'        => 'bi bi-image',
38            'category'    => 'header',
39            'description' => 'Branded header with application name and logo',
40            'defaultProps' => [
41                'logo_url' => '',
42                'app_name' => 'Ammonly',
43                'align'    => 'center',
44            ],
45        ],
46        'email_heading' => [
47            'type'        => 'email_heading',
48            'label'       => 'Tytuł (Heading)',
49            'icon'        => 'bi bi-type-h1',
50            'category'    => 'content',
51            'description' => 'Main headline title with email-safe typography',
52            'defaultProps' => [
53                'text'        => 'Resetowanie hasła',
54                'level'       => 'h2',
55                'align'       => 'left',
56                'color'       => '#1e293b',
57            ],
58        ],
59        'email_paragraph' => [
60            'type'        => 'email_paragraph',
61            'label'       => 'Treść tekstowa (Paragraf)',
62            'icon'        => 'bi bi-text-paragraph',
63            'category'    => 'content',
64            'description' => 'Multi-line formatted text content with placeholders',
65            'defaultProps' => [
66                'content' => 'Otrzymaliśmy prośbę o zresetowanie hasła dla Twojego konta.',
67                'align'   => 'left',
68                'color'   => '#334155',
69            ],
70        ],
71        'email_cta_button' => [
72            'type'        => 'email_cta_button',
73            'label'       => 'Przycisk akcji (CTA)',
74            'icon'        => 'bi bi-cursor',
75            'category'    => 'action',
76            'description' => 'Bulletproof button with link, compatible with Outlook and mobile',
77            'defaultProps' => [
78                'text'             => 'Zresetuj hasło',
79                'url'              => '{{ reset_url }}',
80                'align'            => 'center',
81                'background_color' => self::PRIMARY_ACCENT_COLOR,
82                'text_color'       => '#ffffff',
83                'border_radius'    => '6px',
84            ],
85        ],
86        'email_callout_box' => [
87            'type'        => 'email_callout_box',
88            'label'       => 'Wyróżniony komunikat (Callout)',
89            'icon'        => 'bi bi-info-circle',
90            'category'    => 'content',
91            'description' => 'Notice box with background tint and accent border',
92            'defaultProps' => [
93                'title'            => 'Ważna informacja',
94                'content'          => 'Link jest ważny przez {{ token_lifetime_minutes }} minut.',
95                'box_type'         => 'info',
96                'background_color' => '#f0f6fc',
97                'border_color'     => self::PRIMARY_ACCENT_COLOR,
98            ],
99        ],
100        'email_divider' => [
101            'type'        => 'email_divider',
102            'label'       => 'Linia rozdzielająca',
103            'icon'        => 'bi bi-hr',
104            'category'    => 'layout',
105            'description' => 'Subtle horizontal rule dividing email sections',
106            'defaultProps' => [
107                'color'  => '#e2e8f0',
108                'margin' => '20px',
109            ],
110        ],
111        'email_key_value_list' => [
112            'type'        => 'email_key_value_list',
113            'label'       => 'Lista parametrów (Klucz-Wartość)',
114            'icon'        => 'bi bi-list-ul',
115            'category'    => 'content',
116            'description' => 'Clean two-column table for metadata and parameters',
117            'defaultProps' => [
118                'items' => [
119                    ['label' => 'Użytkownik', 'value' => '{{ user_name }}'],
120                    ['label' => 'Adres IP', 'value' => '{{ ip_address }}'],
121                    ['label' => 'Ważny do', 'value' => '{{ expires_at }} UTC'],
122                ],
123            ],
124        ],
125        'email_spacer' => [
126            'type'        => 'email_spacer',
127            'label'       => 'Odstęp pionowy (Spacer)',
128            'icon'        => 'bi bi-distribute-vertical',
129            'category'    => 'layout',
130            'description' => 'Vertical spacing between blocks for aesthetic whitespace',
131            'defaultProps' => [
132                'height' => '24px',
133            ],
134        ],
135        'email_footer_unsubscribe' => [
136            'type'        => 'email_footer_unsubscribe',
137            'label'       => 'Stopka wiadomości',
138            'icon'        => 'bi bi-file-text',
139            'category'    => 'footer',
140            'description' => 'Legal footer with sender details, copyright, and security info',
141            'defaultProps' => [
142                'company_name' => 'Ammonly Platform',
143                'notice_text'  => 'Wiadomość wygenerowana automatycznie. Jeśli nie prosiłeś o reset, zignoruj ją.',
144            ],
145        ],
146    ];
147
148    /**
149     * Returns all registered email block definitions.
150     *
151     * @return array<string, array<string, mixed>>
152     */
153    public static function getAll(): array
154    {
155        return self::$definitions;
156    }
157
158    /**
159     * Checks if a block type is a valid email block.
160     *
161     * @param string $type Block type identifier.
162     * @return bool
163     */
164    public static function has(string $type): bool
165    {
166        return isset(self::$definitions[$type]);
167    }
168
169    /**
170     * Obtains definition for specific block type.
171     *
172     * @param string $type Block type.
173     * @return array<string, mixed>|null
174     */
175    public static function get(string $type): ?array
176    {
177        return self::$definitions[$type] ?? null;
178    }
179
180    /**
181     * Generates default blocks for a password reset email.
182     *
183     * @param string $lang Target ISO language code ('pl' or 'en').
184     * @return array<int, array<string, mixed>>
185     */
186    public static function getPasswordResetDefaultBlocks(string $lang = 'pl'): array
187    {
188        if (strtolower($lang) === 'en') {
189            return [
190                [
191                    'id'    => 'eml_preheader',
192                    'type'  => 'email_preheader',
193                    'sort'  => 0,
194                    'props' => [
195                        'text' => 'Password reset instructions for Ammonly',
196                    ],
197                ],
198                [
199                    'id'    => 'eml_header',
200                    'type'  => 'email_header_logo',
201                    'sort'  => 10,
202                    'props' => [
203                        'app_name' => 'Ammonly',
204                        'align'    => 'center',
205                    ],
206                ],
207                [
208                    'id'    => 'eml_heading',
209                    'type'  => 'email_heading',
210                    'sort'  => 20,
211                    'props' => [
212                        'text'  => 'Reset Your Password',
213                        'level' => 'h2',
214                        'align' => 'left',
215                        'color' => '#1e293b',
216                    ],
217                ],
218                [
219                    'id'    => 'eml_paragraph',
220                    'type'  => 'email_paragraph',
221                    'sort'  => 30,
222                    'props' => [
223                        'content' => 'Hello {{ user_name }},<br><br>'
224                            . 'We received a request to reset the password for your account in {{ app_name }}. '
225                            . 'To set a new password, click the button below:',
226                        'align'   => 'left',
227                        'color'   => '#334155',
228                    ],
229                ],
230                [
231                    'id'    => 'eml_cta',
232                    'type'  => 'email_cta_button',
233                    'sort'  => 40,
234                    'props' => [
235                        'text'             => 'Reset Password',
236                        'url'              => '{{ reset_url }}',
237                        'align'            => 'center',
238                        'background_color' => '#206bc4',
239                        'text_color'       => '#ffffff',
240                        'border_radius'    => '6px',
241                    ],
242                ],
243                [
244                    'id'    => 'eml_callout',
245                    'type'  => 'email_callout_box',
246                    'sort'  => 50,
247                    'props' => [
248                        'title'            => 'Security Notice',
249                        'content'          => 'This link is valid for {{ token_lifetime_minutes }} minutes '
250                            . '(until {{ expires_at }} UTC). After this time it will expire and a new '
251                            . 'request will be required.',
252                        'box_type'         => 'info',
253                        'background_color' => '#f0f6fc',
254                        'border_color'     => '#206bc4',
255                    ],
256                ],
257                [
258                    'id'    => 'eml_divider',
259                    'type'  => 'email_divider',
260                    'sort'  => 60,
261                    'props' => [
262                        'color'  => '#e2e8f0',
263                        'margin' => '24px',
264                    ],
265                ],
266                [
267                    'id'    => 'eml_footer',
268                    'type'  => 'email_footer_unsubscribe',
269                    'sort'  => 70,
270                    'props' => [
271                        'company_name' => 'Ammonly Platform',
272                        'notice_text'  => 'If you did not request a password reset, you can safely ignore this '
273                            . 'email — your current password remains unchanged.',
274                    ],
275                ],
276            ];
277        }
278
279        return [
280            [
281                'id'    => 'eml_preheader',
282                'type'  => 'email_preheader',
283                'sort'  => 0,
284                'props' => [
285                    'text' => 'Instrukcja resetowania hasła w systemie Ammonly',
286                ],
287            ],
288            [
289                'id'    => 'eml_header',
290                'type'  => 'email_header_logo',
291                'sort'  => 10,
292                'props' => [
293                    'app_name' => 'Ammonly',
294                    'align'    => 'center',
295                ],
296            ],
297            [
298                'id'    => 'eml_heading',
299                'type'  => 'email_heading',
300                'sort'  => 20,
301                'props' => [
302                    'text'  => 'Resetowanie Twojego hasła',
303                    'level' => 'h2',
304                    'align' => 'left',
305                    'color' => '#1e293b',
306                ],
307            ],
308            [
309                'id'    => 'eml_paragraph',
310                'type'  => 'email_paragraph',
311                'sort'  => 30,
312                'props' => [
313                    'content' => 'Witaj {{ user_name }},<br><br>'
314                        . 'Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta w systemie {{ app_name }}. '
315                        . 'Aby ustawić nowe hasło, kliknij poniższy przycisk:',
316                    'align'   => 'left',
317                    'color'   => '#334155',
318                ],
319            ],
320            [
321                'id'    => 'eml_cta',
322                'type'  => 'email_cta_button',
323                'sort'  => 40,
324                'props' => [
325                    'text'             => 'Ustaw nowe hasło',
326                    'url'              => '{{ reset_url }}',
327                    'align'            => 'center',
328                    'background_color' => self::PRIMARY_ACCENT_COLOR,
329                    'text_color'       => '#ffffff',
330                    'border_radius'    => '6px',
331                ],
332            ],
333            [
334                'id'    => 'eml_callout',
335                'type'  => 'email_callout_box',
336                'sort'  => 50,
337                'props' => [
338                    'title'            => 'Bezpieczeństwo',
339                    'content'          => 'Powyższy link jest aktywny przez {{ token_lifetime_minutes }} minut '
340                        . '(do {{ expires_at }} UTC). Po tym czasie wygaśnie i konieczne będzie ponowne '
341                        . 'zgłoszenie prośby o reset.',
342                    'box_type'         => 'info',
343                    'background_color' => '#f0f6fc',
344                    'border_color'     => self::PRIMARY_ACCENT_COLOR,
345                ],
346            ],
347            [
348                'id'    => 'eml_divider',
349                'type'  => 'email_divider',
350                'sort'  => 60,
351                'props' => [
352                    'color'  => '#e2e8f0',
353                    'margin' => '24px',
354                ],
355            ],
356            [
357                'id'    => 'eml_footer',
358                'type'  => 'email_footer_unsubscribe',
359                'sort'  => 70,
360                'props' => [
361                    'company_name' => 'Ammonly Platform',
362                    'notice_text'  => 'Jeżeli nie zlecałeś resetowania hasła, możesz bezpiecznie zignorować tę '
363                        . 'wiadomość — Twoje dotychczasowe hasło pozostaje niezmienione.',
364                ],
365            ],
366        ];
367    }
368}