Passed
Push — master ( 5aa9a6...90bef3 )
by Sebastian
02:25
created

Mailcode_Parser_Safeguard   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 505
Duplicated Lines 0 %

Importance

Changes 12
Bugs 0 Features 1
Metric Value
eloc 147
c 12
b 0
f 1
dl 0
loc 505
rs 8.72
wmc 46

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getOriginalString() 0 3 1
A getDelimiter() 0 3 1
A setDelimiter() 0 14 2
A __construct() 0 4 1
A makeSafe() 0 5 1
A makeSafePartial() 0 13 2
A getFormatter() 0 3 1
A hasPlaceholders() 0 5 1
A isValid() 0 3 1
A getPlaceholderByID() 0 19 3
A getCollection() 0 10 2
A getPlaceholderByString() 0 19 3
A requireValidCollection() 0 15 2
A makeWholePartial() 0 6 1
A getPlaceholders() 0 23 3
A selectHTMLHighlightingFormatter() 0 10 2
A getReplaces() 0 4 1
A makeHighlightedPartial() 0 6 1
A getPlaceholderStrings() 0 17 3
A restore() 0 27 6
A selectSingleLinesFormatter() 0 10 2
A makeWhole() 0 6 1
A makeHighlighted() 0 6 1
A selectFormatter() 0 19 2
A isPlaceholder() 0 5 1
A exceptionWrongFormatter() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like Mailcode_Parser_Safeguard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mailcode_Parser_Safeguard, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * File containing the {@see Mailcode_Parser_Safeguard} class.
4
 *
5
 * @package Mailcode
6
 * @subpackage Parser
7
 * @see Mailcode_Parser_Safeguard
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
/**
15
 * Command safeguarder: used to replace the mailcode commands
16
 * in a string with placeholders, to allow safe text transformation
17
 * and filtering operations on strings, without risking to break 
18
 * any of the contained commands (if any).
19
 * 
20
 * Usage:
21
 * 
22
 * <pre>
23
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
24
 * 
25
 * // replace all commands with placeholders
26
 * $workString = $safeguard->makeSafe();
27
 * 
28
 * // dome something with the work string - filtering, parsing...
29
 * 
30
 * // restore all command placeholders
31
 * $resultString = $safeguard->makeWhole($workString);
32
 * </pre>
33
 * 
34
 * Note that by default, the placeholders are delimited with
35
 * two underscores, e.g. <code>__PCH0001__</code>. If the text
36
 * transformations include replacing or modifying underscores,
37
 * you should use a different delimiter:
38
 * 
39
 * <pre>
40
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
41
 * 
42
 * // change the delimiter to %%. Can be any arbitrary string.
43
 * $safeguard->setDelimiter('%%');
44
 * </pre>
45
 *
46
 * @package Mailcode
47
 * @subpackage Parser
48
 * @author Sebastian Mordziol <[email protected]>
49
 */
50
class Mailcode_Parser_Safeguard
51
{
52
    const ERROR_INVALID_COMMANDS = 47801;
53
    
54
    const ERROR_COMMAND_PLACEHOLDER_MISSING = 47802;
55
    
56
    const ERROR_EMPTY_DELIMITER = 47803;
57
    
58
    const ERROR_PLACEHOLDER_NOT_FOUND = 47804;
59
    
60
    const ERROR_UNKNOWN_FORMATTER = 47805;
61
    
62
    const ERROR_NOT_A_SINGLE_LINES_FORMATTER = 47806;
63
    
64
   /**
65
    * @var Mailcode_Parser
66
    */
67
    protected $parser;
68
    
69
   /**
70
    * @var Mailcode_Collection
71
    */
72
    protected $commands;
73
    
74
   /**
75
    * @var string
76
    */
77
    protected $originalString;
78
    
79
   /**
80
    * @var Mailcode_Collection
81
    */
82
    protected $collection;
83
    
84
   /**
85
    * Counter for the placeholders, global for all placeholders.
86
    * @var integer
87
    */
88
    private static $counter = 0;
89
    
90
   /**
91
    * @var Mailcode_Parser_Safeguard_Placeholder[]
92
    */
93
    protected $placeholders;
94
    
95
   /**
96
    * @var string
97
    */
98
    protected $delimiter = '__';
99
    
100
   /**
101
    * @var string[]|NULL
102
    */
103
    protected $placeholderStrings;
104
    
105
   /**
106
    * @var Mailcode_Parser_Safeguard_Formatter
107
    */
108
    protected $formatter;
109
    
110
    public function __construct(Mailcode_Parser $parser, string $subject)
111
    {
112
        $this->parser = $parser;
113
        $this->originalString = $subject;
114
    }
115
    
116
   /**
117
    * Retrieves the string the safeguard was created for.
118
    * 
119
    * @return string
120
    */
121
    public function getOriginalString() : string
122
    {
123
        return $this->originalString;
124
    }
125
    
126
   /**
127
    * Sets the delimiter character sequence used to prepend
128
    * and append to the placeholders.
129
    * 
130
    * The delimiter's default is "__" (two underscores).
131
    * 
132
    * @param string $delimiter
133
    * @return Mailcode_Parser_Safeguard
134
    */
135
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
136
    {
137
        if(empty($delimiter))
138
        {
139
            throw new Mailcode_Exception(
140
                'Empty delimiter',
141
                'Delimiters may not be empty.',
142
                self::ERROR_EMPTY_DELIMITER
143
            );
144
        }
145
        
146
        $this->delimiter = $delimiter;
147
        
148
        return $this;
149
    }
150
    
151
    public function getDelimiter() : string
152
    {
153
        return $this->delimiter;
154
    }
155
    
156
   /**
157
    * Retrieves the safe string in which all commands have been replaced
158
    * by placeholder strings.
159
    *
160
    * @return string
161
    * @throws Mailcode_Exception 
162
    *
163
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
164
    */
165
    public function makeSafe() : string
166
    {
167
        $this->requireValidCollection();
168
        
169
        return $this->makeSafePartial();
170
    }
171
    
172
   /**
173
    * Like makeSafe(), but allows partial (invalid) commands: use this
174
    * if the subject string may contain only part of the whole set of
175
    * commands. 
176
    * 
177
    * Example: parsing a text with an opening if statement, without the 
178
    * matching end statement.
179
    * 
180
    * @return string
181
    */
182
    public function makeSafePartial() : string
183
    {
184
        $replaces = $this->getReplaces();
185
        
186
        $safe = str_replace(array_values($replaces), array_keys($replaces), $this->originalString);
187
188
        // If a formatter has been selected, let it modify the string.
189
        if(isset($this->formatter))
190
        {
191
            $safe = $this->formatter->format($safe);
192
        }
193
        
194
        return $safe;
195
    }
196
    
197
    public function getFormatter() : ?Mailcode_Parser_Safeguard_Formatter
198
    {
199
        return $this->formatter;
200
    }
201
    
202
    public function selectFormatter(string $formatterID) : Mailcode_Parser_Safeguard_Formatter
203
    {
204
        $class = 'Mailcode\Mailcode_Parser_Safeguard_Formatter_'.$formatterID;
205
        
206
        if(class_exists($class))
207
        {
208
            $this->formatter = new $class($this);
209
            
210
            return $this->formatter;
211
        }
212
        
213
        throw new Mailcode_Exception(
214
            'Unknown safeguard formatter.',
215
            sprintf(
216
                'The formatter [%s] does not exist, could not find class [%s].',
217
                $formatterID,
218
                $class
219
            ),
220
            self::ERROR_UNKNOWN_FORMATTER
221
        );
222
    }
223
    
224
    public function selectHTMLHighlightingFormatter() : Mailcode_Parser_Safeguard_Formatter_HTMLHighlighting
225
    {
226
        $formatter = $this->selectFormatter('HTMLHighlighting');
227
        
228
        if($formatter instanceof Mailcode_Parser_Safeguard_Formatter_HTMLHighlighting)
0 ignored issues
show
introduced by
$formatter is always a sub-type of Mailcode\Mailcode_Parser...matter_HTMLHighlighting.
Loading history...
229
        {
230
            return $formatter;
231
        }
232
        
233
        throw $this->exceptionWrongFormatter(Mailcode_Parser_Safeguard_Formatter_HTMLHighlighting::class);
234
    }
235
    
236
   /**
237
    * Enables the formatter that ensures that all commands that
238
    * @return Mailcode_Parser_Safeguard_Formatter_SingleLines
239
    */
240
    public function selectSingleLinesFormatter() : Mailcode_Parser_Safeguard_Formatter_SingleLines
241
    {
242
        $formatter = $this->selectFormatter('SingleLines');
243
        
244
        if($formatter instanceof Mailcode_Parser_Safeguard_Formatter_SingleLines)
0 ignored issues
show
introduced by
$formatter is always a sub-type of Mailcode\Mailcode_Parser...d_Formatter_SingleLines.
Loading history...
245
        {
246
            return $formatter;
247
        }
248
        
249
        throw $this->exceptionWrongFormatter(Mailcode_Parser_Safeguard_Formatter_SingleLines::class);
250
    }
251
    
252
    private function exceptionWrongFormatter(string $expectedClass) : Mailcode_Exception
253
    {
254
        return new Mailcode_Exception(
255
            'Not the expected formatter',
256
            sprintf(
257
                'The formatter is not an instance of [%s].',
258
                $expectedClass
259
            ),
260
            self::ERROR_NOT_A_SINGLE_LINES_FORMATTER
261
        );
262
    }
263
    
264
   /**
265
    * Retrieves an associative array with pairs of
266
    * [placeholder string => replacement text]. 
267
    * 
268
    * @param bool $highlighted
269
    * @return string[]string
270
    */
271
    protected function getReplaces(bool $highlighted=false, bool $normalize=false) : array
272
    {
273
        $restorer = new Mailcode_Parser_Safeguard_Restorer($this, $highlighted, $normalize);
274
        return $restorer->getReplaces();
275
    }
276
    
277
   /**
278
    * Retrieves all placeholders that have to be added to
279
    * the subject text.
280
    * 
281
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
282
    */
283
    public function getPlaceholders()
284
    {
285
        if(isset($this->placeholders))
286
        {
287
            return $this->placeholders;
288
        }
289
        
290
        $this->placeholders = array();
291
        
292
        $cmds = $this->getCollection()->getGroupedByHash();
293
        
294
        foreach($cmds as $command)
295
        {
296
            self::$counter++;
297
            
298
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
299
                self::$counter,
300
                $command,
301
                $this
302
            );
303
        }
304
305
        return $this->placeholders;
306
    }
307
    
308
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
309
    {
310
        if(!$partial)
311
        {
312
            $this->requireValidCollection();
313
        }
314
        
315
        $replaces = $this->getReplaces($highlighted, true);
316
        
317
        $placeholderStrings = array_keys($replaces);
318
        
319
        foreach($placeholderStrings as $search)
320
        {
321
            if(!$partial && !$highlighted && !strstr($string, $search))
322
            {
323
                throw new Mailcode_Exception(
324
                    'Command placeholder not found',
325
                    sprintf(
326
                        'A placeholder for a command could not be found in the string to restore: [%s].',
327
                        $search
328
                    ),
329
                    self::ERROR_COMMAND_PLACEHOLDER_MISSING
330
                );
331
            }
332
        }
333
        
334
        return str_replace($placeholderStrings, array_values($replaces), $string);
335
    }
336
    
337
   /**
338
    * Makes the string whole again after transforming or filtering it,
339
    * by replacing the command placeholders with the original commands.
340
    *
341
    * @param string $string
342
    * @return string
343
    * @throws Mailcode_Exception
344
    *
345
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
346
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
347
    */
348
    public function makeWhole(string $string) : string
349
    {
350
        return $this->restore(
351
            $string, 
352
            false, // partial? 
353
            false // highlight?
354
        );
355
    }
356
    
357
   /**
358
    * Like `makeWhole()`, but ignores missing command placeholders.
359
    *
360
    * @param string $string
361
    * @return string
362
    * @throws Mailcode_Exception
363
    *
364
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
365
    */
366
    public function makeWholePartial(string $string) : string
367
    {
368
        return $this->restore(
369
            $string,
370
            true, // partial?
371
            false // highlight?
372
        );
373
    }
374
375
   /**
376
    * Like `makeWhole()`, but replaces the commands with a syntax
377
    * highlighted version, meant for human readable texts only.
378
    * 
379
    * Note: the commands lose their functionality (They cannot be 
380
    * parsed from that string again).
381
    *
382
    * @param string $string
383
    * @return string
384
    * @throws Mailcode_Exception
385
    *
386
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
387
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
388
    */
389
    public function makeHighlighted(string $string) : string
390
    {
391
        return $this->restore(
392
            $string, 
393
            false, // partial? 
394
            true // highlighted?
395
        );
396
    }
397
    
398
   /**
399
    * Like `makeHighlighted()`, but ignores missing command placeholders.
400
    * 
401
    * @param string $string
402
    * @return string
403
    * @throws Mailcode_Exception
404
    *
405
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
406
    */
407
    public function makeHighlightedPartial(string $string) : string
408
    {
409
        return $this->restore(
410
            $string, 
411
            true, // partial? 
412
            true // highlight?
413
        );
414
    }
415
    
416
   /**
417
    * Retrieves the commands collection contained in the string.
418
    * 
419
    * @return Mailcode_Collection
420
    */
421
    public function getCollection() : Mailcode_Collection
422
    {
423
        if(isset($this->collection))
424
        {
425
            return $this->collection;
426
        }
427
        
428
        $this->collection = $this->parser->parseString($this->originalString);
429
        
430
        return $this->collection;
431
    }
432
    
433
    public function isValid() : bool
434
    {
435
        return $this->getCollection()->isValid();
436
    }
437
    
438
   /**
439
    * @throws Mailcode_Exception
440
    * 
441
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
442
    */
443
    protected function requireValidCollection() : void
444
    {
445
        if($this->getCollection()->isValid())
446
        {
447
            return;
448
        }
449
        
450
        throw new Mailcode_Exception(
451
            'Cannot safeguard invalid commands',
452
            sprintf(
453
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
454
                'Source string: [%s]',
455
                $this->originalString
456
            ),
457
            self::ERROR_INVALID_COMMANDS
458
        );
459
    }
460
    
461
   /**
462
    * Retrieves a list of all placeholder IDs used in the text.
463
    * 
464
    * @return string[]
465
    */
466
    public function getPlaceholderStrings() : array
467
    {
468
        if(is_array($this->placeholderStrings))
469
        {
470
            return $this->placeholderStrings;
471
        }
472
        
473
        $placeholders = $this->getPlaceholders();
474
        
475
        $this->placeholderStrings = array();
476
        
477
        foreach($placeholders as $placeholder)
478
        {
479
            $this->placeholderStrings[] = $placeholder->getReplacementText();
480
        }
481
        
482
        return $this->placeholderStrings;
483
    }
484
    
485
    public function isPlaceholder(string $subject) : bool
486
    {
487
        $ids = $this->getPlaceholderStrings();
488
        
489
        return in_array($subject, $ids);
490
    }
491
    
492
   /**
493
    * Retrieves a placeholder instance by its ID.
494
    * 
495
    * @param int $id
496
    * @throws Mailcode_Exception If the placeholder was not found.
497
    * @return Mailcode_Parser_Safeguard_Placeholder
498
    */
499
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
500
    {
501
        $placeholders = $this->getPlaceholders();
502
        
503
        foreach($placeholders as $placeholder)
504
        {
505
            if($placeholder->getID() === $id)
506
            {
507
                return $placeholder;
508
            }
509
        }
510
        
511
        throw new Mailcode_Exception(
512
            'No such safeguard placeholder.',
513
            sprintf(
514
                'The placeholder ID [%s] is not present in the safeguard instance.',
515
                $id
516
            ),
517
            self::ERROR_PLACEHOLDER_NOT_FOUND
518
        );
519
    }
520
    
521
   /**
522
    * Retrieves a placeholder instance by its replacement text.
523
    * 
524
    * @param string $string
525
    * @throws Mailcode_Exception
526
    * @return Mailcode_Parser_Safeguard_Placeholder
527
    */
528
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
529
    {
530
        $placeholders = $this->getPlaceholders();
531
        
532
        foreach($placeholders as $placeholder)
533
        {
534
            if($placeholder->getReplacementText() === $string)
535
            {
536
                return $placeholder;
537
            }
538
        }
539
        
540
        throw new Mailcode_Exception(
541
            'No such safeguard placeholder.',
542
            sprintf(
543
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
544
                $string
545
            ),
546
            self::ERROR_PLACEHOLDER_NOT_FOUND
547
        );
548
    }
549
    
550
    public function hasPlaceholders() : bool
551
    {
552
        $placeholders = $this->getPlaceholders();
553
        
554
        return !empty($placeholders);
555
    }
556
}
557