Test Failed
Push — master ( 36c5b6...af11e6 )
by Sebastian
04:51
created

Mailcode_Parser_PreParser::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
/**
3
 * File containing the class {\Mailcode\Mailcode_Parser_StringPreProcessor}.
4
 *
5
 * @package Mailcode
6
 * @subpackage Parser
7
 * @see \Mailcode\Mailcode_Parser_StringPreProcessor
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
use function AppUtils\sb;
15
16
/**
17
 * Does a first parsing pass of the subject string, to
18
 * handle all commands that can have content (like the
19
 * `code` command).
20
 *
21
 * These are handled differently to avoid their contents
22
 * from being parsed by the main parser. To ensure that
23
 * no changes are made to their contents, this is the very
24
 * first step in the parsing process.
25
 *
26
 * This makes it possible for these commands to contain
27
 * Mailcode or any other form of content, without interfering
28
 * in any way with the rest of the Mailcode in the document.
29
 *
30
 * In oder for the parsing of these commands to work, their
31
 * syntax is slightly different. The closing tag must use
32
 * the same name as the command, for example:
33
 *
34
 * <pre>
35
 * {code: "ApacheVelocity}
36
 *     (Some velocity code here)
37
 * {code}
38
 * </pre>
39
 *
40
 * The preprocessor extracts the information, and replaces
41
 * the command in the string with a regular command that the
42
 * main parser can parse as per normal.
43
 *
44
 * @package Mailcode
45
 * @subpackage Parser
46
 * @author Sebastian Mordziol <[email protected]>
47
 */
48
class Mailcode_Parser_PreParser
49
{
50
    public const ERROR_CONTENT_ID_NOT_FOUND = 101401;
51
52
    private string $subject;
53
    private Mailcode_Collection $collection;
54
    private static int $contentCounter = 0;
55
    private bool $debug = false;
56
57
    /**
58
     * @var Mailcode_PreParser_CommandDef[]
59
     */
60
    private array $commands = array();
61
62
    /**
63
     * @var array<int,string>
64
     */
65
    private static array $contents = array();
66
67
    public function __construct(string $subject, Mailcode_Collection $collection)
68
    {
69
        $this->subject = $subject;
70
        $this->collection = $collection;
71
    }
72
73
    /**
74
     * @return Mailcode_Collection
75
     */
76
    public function getCollection() : Mailcode_Collection
77
    {
78
        return $this->collection;
79
    }
80
81
    public function isValid() : bool
82
    {
83
        return $this->collection->isValid();
84
    }
85
86
    public function enableDebug(bool $enable) : self
87
    {
88
        $this->debug = $enable;
89
        return $this;
90
    }
91
92
    /**
93
     * Resets the stored contents and content counter.
94
     *
95
     * NOTE: Used primarily for the unit tests.
96
     */
97
    public static function reset() : void
98
    {
99
        self::$contentCounter = 0;
100
        self::$contents = array();
101
    }
102
103
    /**
104
     * Fetches the content string stored under the specified ID.
105
     *
106
     * @param int $id
107
     * @return string
108
     *
109
     * @throws Mailcode_Parser_Exception
110
     * @see Mailcode_Parser_PreParser::ERROR_CONTENT_ID_NOT_FOUND
111
     */
112
    public static function getContent(int $id) : string
113
    {
114
        if(isset(self::$contents[$id]))
115
        {
116
            return self::$contents[$id];
117
        }
118
119
        throw new Mailcode_Parser_Exception(
120
            'Command content not found',
121
            sprintf(
122
                'The content stored under ID [%s] does not exist.',
123
                $id
124
            ),
125
            self::ERROR_CONTENT_ID_NOT_FOUND
126
        );
127
    }
128
129
    /**
130
     * Removes the content string stored under the specified ID, if it exists.
131
     *
132
     * @param int $id
133
     * @return void
134
     */
135
    public static function clearContent(int $id) : void
136
    {
137
        if(isset(self::$contents[$id]))
138
        {
139
            unset(self::$contents[$id]);
140
        }
141
    }
142
143
    public static function getContentCounter() : int
144
    {
145
        return self::$contentCounter;
146
    }
147
148
    /**
149
     * @return $this
150
     * @throws Mailcode_Exception
151
     */
152
    public function parse() : self
153
    {
154
        $this->subject = self::safeguardBrackets($this->subject);
155
156
        $names = $this->getContentCommandNames();
157
158
        foreach($names as $name)
159
        {
160
            $this->collapseContentCommand($name);
161
        }
162
163
        $this->subject = self::restoreBrackets($this->subject);
0 ignored issues
show
Bug introduced by
It seems like $this->subject can also be of type array; however, parameter $subject of Mailcode\Mailcode_Parser...rser::restoreBrackets() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
        $this->subject = self::restoreBrackets(/** @scrutinizer ignore-type */ $this->subject);
Loading history...
164
165
        return $this;
166
    }
167
168
    /**
169
     * @return string[]
170
     * @throws Mailcode_Exception
171
     */
172
    private function getContentCommandNames() : array
173
    {
174
        $commands = Mailcode::create()->getCommands()->getContentCommands();
175
        $result = array();
176
177
        foreach($commands as $command)
178
        {
179
            $result[] = $command->getName();
180
        }
181
182
        return $result;
183
    }
184
185
    /**
186
     * @var array<string,string>
187
     */
188
    private static array $escapeChars = array(
189
        '\{' => '__BRACKET_OPEN__',
190
        '\}' => '__BRACKET_CLOSE__'
191
    );
192
193
    public static function safeguardBrackets(string $subject) : string
194
    {
195
        return str_replace(
196
            array_keys(self::$escapeChars),
197
            array_values(self::$escapeChars),
198
            $subject
199
        );
200
    }
201
202
    public static function restoreBrackets(string $subject) : string
203
    {
204
        return str_replace(
205
            array_values(self::$escapeChars),
206
            array_keys(self::$escapeChars),
207
            $subject
208
        );
209
    }
210
211
    public static function unescapeBrackets(string $subject) : string
212
    {
213
        return str_replace(
214
            array(
215
                '\{',
216
                '\}'
217
            ),
218
            array(
219
                '{',
220
                '}'
221
            ),
222
            $subject
223
        );
224
    }
225
226
    public function countCommands() : int
227
    {
228
        return count($this->commands);
229
    }
230
231
    public function getString() : string
232
    {
233
        return $this->subject;
234
    }
235
236
    /**
237
     * @return Mailcode_PreParser_CommandDef[]
238
     */
239
    public function getCommands() : array
240
    {
241
        return $this->commands;
242
    }
243
244
    private function collapseContentCommand(string $name) : void
245
    {
246
        $this->commands = $this->detectCommands($name);
247
248
        foreach($this->commands as $commandDef)
249
        {
250
            $this->processCommand($commandDef);
251
        }
252
253
        $this->validateCommandContents();
254
    }
255
256
    private function validateCommandContents() : void
257
    {
258
        foreach($this->commands as $command)
259
        {
260
            if(!$command->validateContent($this, $this->collection))
261
            {
262
                return;
263
            }
264
        }
265
    }
266
267
    /**
268
     * @return Mailcode_PreParser_CommandDef[]
269
     */
270
    private function detectCommands(string $name) : array
271
    {
272
        $openingCommands = $this->detectOpeningCommands($name);
273
274
        if(empty($openingCommands))
275
        {
276
            return array();
277
        }
278
279
        $closingCommands = $this->detectClosingCommands($name);
280
281
        if(count($closingCommands) !== count($openingCommands))
282
        {
283
            $this->addClosingError($name);
284
            return array();
285
        }
286
287
        $result = array();
288
289
        foreach($openingCommands as $idx => $def)
290
        {
291
            $result[] = new Mailcode_PreParser_CommandDef(
292
                $name,
293
                $def['matchedText'],
294
                $def['parameters'],
295
                $closingCommands[$idx]
296
            );
297
        }
298
299
        return $result;
300
    }
301
302
    /**
303
     * @param string $name
304
     * @return array<int,array{matchedText:string,parameters:string}>
305
     */
306
    private function detectOpeningCommands(string $name) : array
307
    {
308
        preg_match_all('/{\s*'.$name.'\s*:([^}]+)}/sixU', $this->subject, $matches);
309
310
        $result = array();
311
312
        foreach ($matches[0] as $idx => $matchedText)
313
        {
314
            $result[(int)$idx] = array(
315
                'matchedText' => (string)$matchedText,
316
                'parameters' => trim((string)$matches[1][$idx])
317
            );
318
        }
319
320
        $this->debugOpeningCommands($matches);
321
322
        return $result;
323
    }
324
325
    /**
326
     * @param array<int,array<int,string>> $matches
327
     * @return void
328
     */
329
    private function debugOpeningCommands(array $matches) : void
330
    {
331
        if($this->debug === false)
332
        {
333
            return;
334
        }
335
336
        echo 'Opening command matches:'.PHP_EOL;
337
        print_r($matches);
338
    }
339
340
    /**
341
     * @param string $name
342
     * @return array<int,string>
343
     */
344
    private function detectClosingCommands(string $name) : array
345
    {
346
        preg_match_all('/{\s*'.$name.'\s*}/sixU', $this->subject, $matches);
347
348
        return $matches[0];
349
    }
350
351
    private function addClosingError(string $name) : void
352
    {
353
        $this->collection->addErrorMessage(
354
            '',
355
            (string)sb()
356
                ->t(
357
                    'Incorrectly closed %1$s command:',
358
                    sb()->code($name)
359
                )
360
                ->t(
361
                    'Please ensure that each of the commands has a matching %1$s closing tag.',
362
                    sb()->code('{'.$name.'}')
363
                ),
364
            Mailcode_Commands_CommonConstants::VALIDATION_MISSING_CONTENT_CLOSING_TAG
365
        );
366
    }
367
368
    private function processCommand(Mailcode_PreParser_CommandDef $commandDef) : void
369
    {
370
        $commandDef->extractContent($this->subject);
371
372
        $this->debugCommandDef($commandDef);
373
374
        // Replace the original command and content with the replacement command
375
        $this->subject = substr_replace(
376
            $this->subject,
377
            $commandDef->getReplacementCommand(),
378
            $commandDef->getStartPos(),
379
            $commandDef->getLength()
380
        );
381
    }
382
383
    private function debugCommandDef(Mailcode_PreParser_CommandDef $commandDef) : void
384
    {
385
        if($this->debug === true)
386
        {
387
            echo 'Command definition:'.PHP_EOL;
388
            print_r($commandDef->toArray());
389
        }
390
    }
391
392
    /**
393
     * Stores the content of the command. The command will retrieve
394
     * it using {@see Mailcode_Parser_PreParser::getContent()} when
395
     * it is created by the main parser.
396
     *
397
     * @param string $content
398
     * @return int
399
     */
400
    public static function storeContent(string $content) : int
401
    {
402
        self::$contentCounter++;
403
404
        self::$contents[self::$contentCounter] = self::restoreBrackets($content);
405
406
        return self::$contentCounter;
407
    }
408
}
409