Mailcode_Commands_Command   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 60
eloc 159
c 5
b 0
f 0
dl 0
loc 517
rs 3.6

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __toString() 0 3 1
A getInstanceID() 0 3 1
A isDummy() 0 3 1
A setComment() 0 5 1
A getMatchedText() 0 3 1
A getType() 0 8 2
A getHash() 0 9 2
A getParams() 0 3 1
A getValidator() 0 11 2
A getVariables() 0 3 1
A getCommandType() 0 29 5
A getHighlighted() 0 8 2
A __construct() 0 12 1
A getParamsString() 0 8 2
A hasParameters() 0 3 2
A hasFreeformParameters() 0 3 1
A getNormalized() 0 3 1
A hasContentParent() 0 3 2
A getComment() 0 3 1
A hasParent() 0 3 1
A setParent() 0 3 1
A validateNesting() 0 12 2
A getValidationResult() 0 3 1
A getID() 0 6 1
A getLogicKeywords() 0 11 3
A _validateNesting() 0 2 1
A getParent() 0 3 1
A validateSyntax() 0 10 3
A hasType() 0 3 2
A requireParams() 0 11 2
A resolveValidations() 0 3 1
A getSupportedTypes() 0 3 1
A init() 0 2 1
A validateSyntaxMethod() 0 20 2
A requireNonDummy() 0 11 2
A validate() 0 11 2
A isValid() 0 3 1
A getTranslationParam() 0 3 1
A setTranslationParam() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Mailcode_Commands_Command 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_Commands_Command, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * File containing the {@see Mailcode_Commands_Command} class.
4
 *
5
 * @package Mailcode
6
 * @subpackage Commands
7
 * @see Mailcode_Commands_Command
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
use AppUtils\OperationResult;
15
use Mailcode\Commands\CommandException;
16
use Mailcode\Commands\ParamsException;
17
18
abstract class Mailcode_Commands_Command
19
    implements
20
    Mailcode_Interfaces_Commands_Command,
21
    Mailcode_Interfaces_Commands_Validation_EmptyParams,
22
    Mailcode_Interfaces_Commands_Validation_ParamKeywords,
23
    Mailcode_Interfaces_Commands_Validation_ParseParams,
24
    Mailcode_Interfaces_Commands_Validation_TypeSupported,
25
    Mailcode_Interfaces_Commands_Validation_TypeUnsupported
26
{
27
    use Mailcode_Traits_Commands_Validation_EmptyParams;
28
    use Mailcode_Traits_Commands_Validation_ParamKeywords;
29
    use Mailcode_Traits_Commands_Validation_ParseParams;
30
    use Mailcode_Traits_Commands_Validation_TypeSupported;
31
    use Mailcode_Traits_Commands_Validation_TypeUnsupported;
32
33
    public const ERROR_NON_DUMMY_OPERATION = 46001;
34
    public const ERROR_MISSING_VALIDATION_METHOD = 46003;
35
    public const ERROR_MISSING_TYPE_INTERFACE = 46004;
36
    public const ERROR_LOGIC_COMMANDS_NOT_SUPPORTED = 46005;
37
    public const ERROR_URL_ENCODING_NOT_SUPPORTED = 46006;
38
    public const ERROR_NO_PARAMETERS_AVAILABLE = 46007;
39
    public const ERROR_VALIDATOR_INSTANCE_NOT_SET = 46008;
40
    
41
    public const VALIDATION_MISSING_PARAMETERS = 48301;
42
    public const VALIDATION_ADDONS_NOT_SUPPORTED = 48302;
43
    public const VALIDATION_ADDON_NOT_SUPPORTED = 48303;
44
    public const VALIDATION_UNKNOWN_COMMAND_NAME = 48304;
45
    public const VALIDATION_INVALID_PARAMS_STATEMENT = 48305;
46
47
    public const META_URL_ENCODING = 'url_encoding';
48
49
   /**
50
    * @var string
51
    */
52
    protected string $type = '';
53
54
   /**
55
    * @var string
56
    */
57
    protected string $paramsString = '';
58
    
59
   /**
60
    * @var string
61
    */
62
    protected string $matchedText = '';
63
64
   /**
65
    * @var string
66
    */
67
    protected string $hash = '';
68
    
69
   /**
70
    * @var OperationResult
71
    */
72
    protected OperationResult $validationResult;
73
    
74
   /**
75
    * @var Mailcode
76
    */
77
    protected Mailcode $mailcode;
78
    
79
   /**
80
    * @var Mailcode_Parser_Statement|NULL
81
    */
82
    protected ?Mailcode_Parser_Statement $params = null;
83
84
   /**
85
    * @var string[] 
86
    */
87
    protected array $validations = array(
88
        Mailcode_Interfaces_Commands_Validation_EmptyParams::VALIDATION_NAME_EMPTY_PARAMS,
89
        Mailcode_Interfaces_Commands_Validation_ParamKeywords::VALIDATION_NAME_KEYWORDS,
90
        Mailcode_Interfaces_Commands_Validation_ParseParams::VALIDATION_NAME_PARSE_PARAMS,
91
        Mailcode_Interfaces_Commands_Validation_TypeSupported::VALIDATION_NAME_TYPE_SUPPORTED,
92
        Mailcode_Interfaces_Commands_Validation_TypeUnsupported::VALIDATION_NAME_TYPE_UNSUPPORTED
93
    );
94
    
95
   /**
96
    * @var string
97
    */
98
    protected string $comment = '';
99
    
100
   /**
101
    * @var Mailcode_Commands_LogicKeywords|NULL
102
    */
103
    protected ?Mailcode_Commands_LogicKeywords $logicKeywords = null;
104
    
105
   /**
106
    * @var Mailcode_Parser_Statement_Validator|NULL
107
    */
108
    protected ?Mailcode_Parser_Statement_Validator $validator = null;
109
    
110
   /**
111
    * @var boolean
112
    */
113
    private bool $validated = false;
114
115
    /**
116
     * Collection of parameters for the translation backend.
117
     * @var array<string,mixed>
118
     */
119
    protected array $translationParams = array();
120
121
    /**
122
     * @var Mailcode_Commands_Command|NULL
123
     */
124
    protected ?Mailcode_Commands_Command $parent = null;
125
126
    /**
127
     * @var bool
128
     */
129
    private bool $nestingValidated = false;
130
131
    private static int $instanceCounter = 0;
132
133
    private int $instanceID;
134
135
    public function __construct(string $type='', string $paramsString='', string $matchedText='')
136
    {
137
        self::$instanceCounter++;
138
139
        $this->instanceID = self::$instanceCounter;
140
        $this->type = $type;
141
        $this->paramsString = html_entity_decode($paramsString);
142
        $this->matchedText = $matchedText;
143
        $this->mailcode = Mailcode::create();
144
        $this->validationResult = new OperationResult($this);
145
        
146
        $this->init();
147
    }
148
149
    /**
150
     * @return int
151
     */
152
    public function getInstanceID() : int
153
    {
154
        return $this->instanceID;
155
    }
156
    
157
    protected function init() : void
158
    {
159
        
160
    }
161
162
   /**
163
    * Sets the command's parent opening command, if any.
164
    * NOTE: This is set automatically by the parser, and
165
    * should not be called manually.
166
    *
167
    * @param Mailcode_Commands_Command $command
168
    */
169
    public function setParent(Mailcode_Commands_Command $command) : void
170
    {
171
        $this->parent = $command;
172
    }
173
174
    public function hasParent() : bool
175
    {
176
        return isset($this->parent);
177
    }
178
179
    public function getParent() : ?Mailcode_Commands_Command
180
    {
181
        return $this->parent;
182
    }
183
184
    /**
185
     * Whether this command has a parent command that is
186
     * a protected content command, which means it is nested
187
     * in a content block of a command.
188
     *
189
     * @return bool
190
     * @see Mailcode_Interfaces_Commands_ProtectedContent
191
     */
192
    public function hasContentParent() : bool
193
    {
194
        return isset($this->parent) && $this->parent instanceof Mailcode_Interfaces_Commands_ProtectedContent;
195
    }
196
197
    public function getID() : string
198
    {
199
        // account for commands with types: If_Variable should still return If.
200
        $base = str_replace(Mailcode_Commands_Command::class.'_', '', get_class($this));
201
        $tokens = explode('_', $base);
202
        return array_shift($tokens);
203
    }
204
205
    public function setComment(string $comment) : Mailcode_Commands_Command
206
    {
207
        $this->comment = $comment;
208
        
209
        return $this;
210
    }
211
212
    public function getComment() : string
213
    {
214
        return $this->comment;
215
    }
216
217
    public function isDummy() : bool
218
    {
219
        return $this->type === '__dummy';
220
    }
221
222
    public function getHash() : string
223
    {
224
        $this->requireNonDummy();
225
        
226
        if($this->hash === '') {
227
            $this->hash = md5($this->matchedText);
228
        }
229
        
230
        return $this->hash;
231
    }
232
233
    /**
234
     * @return void
235
     * @throws CommandException
236
     */
237
    protected function requireNonDummy() : void
238
    {
239
        if(!$this->isDummy())
240
        {
241
            return;
242
        }
243
        
244
        throw new CommandException(
245
            'Operation not allowed with dummy commands',
246
            null,
247
            self::ERROR_NON_DUMMY_OPERATION
248
        );
249
    }
250
    
251
    public function isValid() : bool
252
    {
253
        return $this->validate()->isValid();
254
    }
255
    
256
    protected function validate() : OperationResult
257
    {
258
        if(!$this->validated)
259
        {
260
            $this->requireNonDummy();
261
            $this->validateSyntax();
262
263
            $this->validated = true;
264
        }
265
        
266
        return $this->validationResult;
267
    }
268
    
269
    public function getValidationResult() :  OperationResult
270
    {
271
        return $this->validationResult;
272
    }
273
    
274
    protected function validateSyntax() : void
275
    {
276
        $validations = $this->resolveValidations();
277
278
        foreach($validations as $validation)
279
        {
280
            // break off at the first validation issue
281
            if(!$this->validateSyntaxMethod($validation))
282
            {
283
                return;
284
            }
285
        }
286
    }
287
288
    /**
289
     * @return string[]
290
     */
291
    protected function resolveValidations() : array
292
    {
293
        return array_merge($this->validations, $this->getValidations());
294
    }
295
296
    /**
297
     * @param string $validation
298
     * @return bool
299
     * @throws CommandException
300
     */
301
    protected function validateSyntaxMethod(string $validation) : bool
302
    {
303
        $method = 'validateSyntax_'.$validation;
304
        
305
        if(!method_exists($this, $method))
306
        {
307
            throw new CommandException(
308
                'Missing validation method ['.$validation.']',
309
                sprintf(
310
                    'The method [%s] is missing from class [%s].',
311
                    $method,
312
                    get_class($this)
313
                ),
314
                self::ERROR_MISSING_VALIDATION_METHOD
315
            );
316
        }
317
        
318
        $this->$method();
319
        
320
        return $this->validationResult->isValid();
321
    }
322
    
323
   /**
324
    * @return string[]
325
    */
326
    abstract protected function getValidations() : array;
327
328
    protected function _validateNesting() : void
329
    {
330
331
    }
332
333
    public function validateNesting() : OperationResult
334
    {
335
        if($this->nestingValidated)
336
        {
337
            return $this->validationResult;
338
        }
339
340
        $this->nestingValidated = true;
341
342
        $this->_validateNesting();
343
344
        return $this->validationResult;
345
    }
346
    
347
    public function hasFreeformParameters() : bool
348
    {
349
        return false;
350
    }
351
352
    public function hasType() : bool
353
    {
354
        return $this->supportsType() && !empty($this->type);
355
    }
356
    
357
    public function getType() : string
358
    {
359
        if($this->supportsType())
360
        {
361
            return $this->type;
362
        }
363
        
364
        return '';
365
    }
366
    
367
    public function hasParameters() : bool
368
    {
369
        return $this->requiresParameters() && !empty($this->paramsString);
370
    }
371
    
372
    public function getMatchedText() : string
373
    {
374
        return $this->matchedText;
375
    }
376
    
377
    public function getHighlighted() : string
378
    {
379
        if(!$this->isValid())
380
        {
381
            return '';
382
        }
383
384
        return (new Mailcode_Commands_Highlighter($this))->highlight();
385
    }
386
    
387
    public function getParamsString() : string
388
    {
389
        if($this->requiresParameters())
390
        {
391
            return $this->paramsString;
392
        }
393
        
394
        return '';
395
    }
396
    
397
    public function getParams() : ?Mailcode_Parser_Statement
398
    {
399
        return $this->params;
400
    }
401
402
    /**
403
     * @return Mailcode_Parser_Statement
404
     * @throws ParamsException
405
     */
406
    public function requireParams() : Mailcode_Parser_Statement
407
    {
408
        if(isset($this->params))
409
        {
410
            return $this->params;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->params could return the type null which is incompatible with the type-hinted return Mailcode\Mailcode_Parser_Statement. Consider adding an additional type-check to rule them out.
Loading history...
411
        }
412
413
        throw new ParamsException(
414
            'No parameters available.',
415
            'The command has no parameters.',
416
            self::ERROR_NO_PARAMETERS_AVAILABLE
417
        );
418
    }
419
    
420
    abstract public function getName() : string;
421
    
422
    abstract public function getLabel() : string;
423
    
424
    abstract public function requiresParameters() : bool;
425
426
    abstract public function supportsType() : bool;
427
428
    abstract public function supportsLogicKeywords() : bool;
429
    
430
    abstract public function generatesContent() : bool;
431
432
    abstract public function getDefaultType() : string;
433
434
    /**
435
     * @return string
436
     * @throws ParamsException
437
     */
438
    final public function getCommandType() : string
439
    {
440
        if($this instanceof Mailcode_Commands_Command_Type_Closing)
441
        {
442
            return 'Closing';
443
        }
444
        
445
        if($this instanceof Mailcode_Commands_Command_Type_Opening)
446
        {
447
            return 'Opening';
448
        }
449
        
450
        if($this instanceof Mailcode_Commands_Command_Type_Sibling)
451
        {
452
            return 'Sibling';
453
        }
454
        
455
        if($this instanceof Mailcode_Commands_Command_Type_Standalone)
456
        {
457
            return 'Standalone';
458
        }
459
        
460
        throw new ParamsException(
461
            'Invalid command type',
462
            sprintf(
463
                'The command [%s] does not implement any of the type interfaces.',
464
                get_class($this)
465
            ),
466
            self::ERROR_MISSING_TYPE_INTERFACE
467
        );
468
    }
469
    
470
    public function getNormalized() : string
471
    {
472
        return (new Mailcode_Commands_Normalizer($this))->normalize();
473
    }
474
475
    public function getSupportedTypes() : array
476
    {
477
        return array();
478
    }
479
480
    public function getVariables() : Mailcode_Variables_Collection_Regular
481
    {
482
        return Mailcode::create()->findVariables($this->paramsString, $this);
483
    }
484
    
485
    public function __toString()
486
    {
487
        return $this->getNormalized();
488
    }
489
490
    /**
491
     * @return Mailcode_Commands_LogicKeywords
492
     * @throws ParamsException
493
     */
494
    public function getLogicKeywords() : Mailcode_Commands_LogicKeywords
495
    {
496
        if(isset($this->logicKeywords) && $this->supportsLogicKeywords())
497
        {
498
            return $this->logicKeywords;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->logicKeywords could return the type null which is incompatible with the type-hinted return Mailcode\Mailcode_Commands_LogicKeywords. Consider adding an additional type-check to rule them out.
Loading history...
499
        }
500
        
501
        throw new ParamsException(
502
            'Logic keywords are not supported',
503
            'Cannot retrieve the logic keywords instance: it is only available for commands supporting logic commands.',
504
            self::ERROR_LOGIC_COMMANDS_NOT_SUPPORTED
505
        );
506
    }
507
508
    /**
509
     * @param string $name
510
     * @param mixed $value
511
     * @return $this
512
     */
513
    public function setTranslationParam(string $name, $value) : self
514
    {
515
        $this->translationParams[$name] = $value;
516
        return $this;
517
    }
518
519
    public function getTranslationParam(string $name)
520
    {
521
        return $this->translationParams[$name] ?? null;
522
    }
523
524
    protected function getValidator() : Mailcode_Parser_Statement_Validator
525
    {
526
        if(isset($this->validator))
527
        {
528
            return $this->validator;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->validator could return the type null which is incompatible with the type-hinted return Mailcode\Mailcode_Parser_Statement_Validator. Consider adding an additional type-check to rule them out.
Loading history...
529
        }
530
531
        throw new ParamsException(
532
            'No validator available',
533
            'The validator instance has not been set.',
534
            self::ERROR_VALIDATOR_INSTANCE_NOT_SET
535
        );
536
    }
537
}
538