Passed
Push — master ( c5c69e...a80c4b )
by Sebastian
02:54
created

Mailcode_Parser_Safeguard   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 1
Metric Value
eloc 149
c 10
b 0
f 1
dl 0
loc 503
rs 8.96
wmc 43

23 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 getPlaceholders() 0 23 3
A getReplaces() 0 23 3
A selectSingleLinesFormatter() 0 16 2
A makeSafe() 0 5 1
A selectFormatter() 0 19 2
A makeSafePartial() 0 13 2
A hasPlaceholders() 0 5 1
A isValid() 0 3 1
A getPlaceholderByID() 0 19 3
A getCollection() 0 10 2
A requireValidCollection() 0 15 2
A getPlaceholderByString() 0 19 3
A makeWholePartial() 0 6 1
A makeHighlightedPartial() 0 6 1
A restore() 0 27 5
A getPlaceholderStrings() 0 17 3
A makeWhole() 0 6 1
A makeHighlighted() 0 6 1
A isPlaceholder() 0 5 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 selectFormatter(string $formatterID) : Mailcode_Parser_Safeguard_Formatter
198
    {
199
        $class = 'Mailcode\Mailcode_Parser_Safeguard_Formatter_'.$formatterID;
200
        
201
        if(class_exists($class))
202
        {
203
            $this->formatter = new $class($this);
204
            
205
            return $this->formatter;
206
        }
207
        
208
        throw new Mailcode_Exception(
209
            'Unknown safeguard formatter.',
210
            sprintf(
211
                'The formatter [%s] does not exist, could not find class [%s].',
212
                $formatterID,
213
                $class
214
            ),
215
            self::ERROR_UNKNOWN_FORMATTER
216
        );
217
    }
218
    
219
   /**
220
    * Enables the formatter that ensures that all commands that
221
    * @return Mailcode_Parser_Safeguard_Formatter_SingleLines
222
    */
223
    public function selectSingleLinesFormatter() : Mailcode_Parser_Safeguard_Formatter_SingleLines
224
    {
225
        $formatter = $this->selectFormatter('SingleLines');
226
        
227
        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...
228
        {
229
            return $formatter;
230
        }
231
        
232
        throw new Mailcode_Exception(
233
            'Not the expected formatter',
234
            sprintf(
235
                'The formatter is not an instance of [%s].',
236
                Mailcode_Parser_Safeguard_Formatter_SingleLines::class
237
            ),
238
            self::ERROR_NOT_A_SINGLE_LINES_FORMATTER
239
        );
240
    }
241
    
242
   /**
243
    * Retrieves an associative array with pairs of
244
    * [placeholder string => replacement text]. 
245
    * 
246
    * @param bool $highlighted
247
    * @return string[]string
248
    */
249
    protected function getReplaces(bool $highlighted=false) : array
250
    {
251
        $placeholders = $this->getPlaceholders();
252
        
253
        $replaces = array();
254
        
255
        foreach($placeholders as $placeholder)
256
        {
257
            $replace = '';
258
            
259
            if($highlighted)
260
            {
261
                $replace = $placeholder->getHighlightedText();
262
            }
263
            else 
264
            {
265
                $replace = $placeholder->getOriginalText();
266
            }
267
            
268
            $replaces[$placeholder->getReplacementText()] = $replace;
269
        }
270
        
271
        return $replaces;
272
    }
273
    
274
    
275
   /**
276
    * Retrieves all placeholders that have to be added to
277
    * the subject text.
278
    * 
279
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
280
    */
281
    public function getPlaceholders()
282
    {
283
        if(isset($this->placeholders))
284
        {
285
            return $this->placeholders;
286
        }
287
        
288
        $this->placeholders = array();
289
        
290
        $cmds = $this->getCollection()->getGroupedByHash();
291
        
292
        foreach($cmds as $command)
293
        {
294
            self::$counter++;
295
            
296
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
297
                self::$counter,
298
                $command,
299
                $this
300
            );
301
        }
302
303
        return $this->placeholders;
304
    }
305
    
306
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
307
    {
308
        if(!$partial)
309
        {
310
            $this->requireValidCollection();
311
        }
312
        
313
        $replaces = $this->getReplaces($highlighted);
314
        
315
        $placeholderStrings = array_keys($replaces);
316
        
317
        foreach($placeholderStrings as $search)
318
        {
319
            if(!$partial && !strstr($string, $search))
320
            {
321
                throw new Mailcode_Exception(
322
                    'Command placeholder not found',
323
                    sprintf(
324
                        'A placeholder for a command could not be found in the string to restore: [%s].',
325
                        $search
326
                    ),
327
                    self::ERROR_COMMAND_PLACEHOLDER_MISSING
328
                );
329
            }
330
        }
331
        
332
        return str_replace($placeholderStrings, array_values($replaces), $string);
333
    }
334
    
335
   /**
336
    * Makes the string whole again after transforming or filtering it,
337
    * by replacing the command placeholders with the original commands.
338
    *
339
    * @param string $string
340
    * @return string
341
    * @throws Mailcode_Exception
342
    *
343
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
344
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
345
    */
346
    public function makeWhole(string $string) : string
347
    {
348
        return $this->restore(
349
            $string, 
350
            false, // partial? 
351
            false // highlight?
352
        );
353
    }
354
    
355
   /**
356
    * Like `makeWhole()`, but ignores missing command placeholders.
357
    *
358
    * @param string $string
359
    * @return string
360
    * @throws Mailcode_Exception
361
    *
362
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
363
    */
364
    public function makeWholePartial(string $string) : string
365
    {
366
        return $this->restore(
367
            $string,
368
            true, // partial?
369
            false // highlight?
370
        );
371
    }
372
373
   /**
374
    * Like `makeWhole()`, but replaces the commands with a syntax
375
    * highlighted version, meant for human readable texts only.
376
    * 
377
    * Note: the commands lose their functionality (They cannot be 
378
    * parsed from that string again).
379
    *
380
    * @param string $string
381
    * @return string
382
    * @throws Mailcode_Exception
383
    *
384
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
385
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
386
    */
387
    public function makeHighlighted(string $string) : string
388
    {
389
        return $this->restore(
390
            $string, 
391
            false, // partial? 
392
            true // highlighted?
393
        );
394
    }
395
    
396
   /**
397
    * Like `makeHighlighted()`, but ignores missing command placeholders.
398
    * 
399
    * @param string $string
400
    * @return string
401
    * @throws Mailcode_Exception
402
    *
403
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
404
    */
405
    public function makeHighlightedPartial(string $string) : string
406
    {
407
        return $this->restore(
408
            $string, 
409
            true, // partial? 
410
            true // highlight?
411
        );
412
    }
413
    
414
   /**
415
    * Retrieves the commands collection contained in the string.
416
    * 
417
    * @return Mailcode_Collection
418
    */
419
    public function getCollection() : Mailcode_Collection
420
    {
421
        if(isset($this->collection))
422
        {
423
            return $this->collection;
424
        }
425
        
426
        $this->collection = $this->parser->parseString($this->originalString);
427
        
428
        return $this->collection;
429
    }
430
    
431
    public function isValid() : bool
432
    {
433
        return $this->getCollection()->isValid();
434
    }
435
    
436
   /**
437
    * @throws Mailcode_Exception
438
    * 
439
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
440
    */
441
    protected function requireValidCollection() : void
442
    {
443
        if($this->getCollection()->isValid())
444
        {
445
            return;
446
        }
447
        
448
        throw new Mailcode_Exception(
449
            'Cannot safeguard invalid commands',
450
            sprintf(
451
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
452
                'Source string: [%s]',
453
                $this->originalString
454
            ),
455
            self::ERROR_INVALID_COMMANDS
456
        );
457
    }
458
    
459
   /**
460
    * Retrieves a list of all placeholder IDs used in the text.
461
    * 
462
    * @return string[]
463
    */
464
    public function getPlaceholderStrings() : array
465
    {
466
        if(is_array($this->placeholderStrings))
467
        {
468
            return $this->placeholderStrings;
469
        }
470
        
471
        $placeholders = $this->getPlaceholders();
472
        
473
        $this->placeholderStrings = array();
474
        
475
        foreach($placeholders as $placeholder)
476
        {
477
            $this->placeholderStrings[] = $placeholder->getReplacementText();
478
        }
479
        
480
        return $this->placeholderStrings;
481
    }
482
    
483
    public function isPlaceholder(string $subject) : bool
484
    {
485
        $ids = $this->getPlaceholderStrings();
486
        
487
        return in_array($subject, $ids);
488
    }
489
    
490
   /**
491
    * Retrieves a placeholder instance by its ID.
492
    * 
493
    * @param int $id
494
    * @throws Mailcode_Exception If the placeholder was not found.
495
    * @return Mailcode_Parser_Safeguard_Placeholder
496
    */
497
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
498
    {
499
        $placeholders = $this->getPlaceholders();
500
        
501
        foreach($placeholders as $placeholder)
502
        {
503
            if($placeholder->getID() === $id)
504
            {
505
                return $placeholder;
506
            }
507
        }
508
        
509
        throw new Mailcode_Exception(
510
            'No such safeguard placeholder.',
511
            sprintf(
512
                'The placeholder ID [%s] is not present in the safeguard instance.',
513
                $id
514
            ),
515
            self::ERROR_PLACEHOLDER_NOT_FOUND
516
        );
517
    }
518
    
519
   /**
520
    * Retrieves a placeholder instance by its replacement text.
521
    * 
522
    * @param string $string
523
    * @throws Mailcode_Exception
524
    * @return Mailcode_Parser_Safeguard_Placeholder
525
    */
526
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
527
    {
528
        $placeholders = $this->getPlaceholders();
529
        
530
        foreach($placeholders as $placeholder)
531
        {
532
            if($placeholder->getReplacementText() === $string)
533
            {
534
                return $placeholder;
535
            }
536
        }
537
        
538
        throw new Mailcode_Exception(
539
            'No such safeguard placeholder.',
540
            sprintf(
541
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
542
                $string
543
            ),
544
            self::ERROR_PLACEHOLDER_NOT_FOUND
545
        );
546
    }
547
    
548
    public function hasPlaceholders() : bool
549
    {
550
        $placeholders = $this->getPlaceholders();
551
        
552
        return !empty($placeholders);
553
    }
554
}
555