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

pruneProtectedContentCommands()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 4
nop 0
dl 0
loc 18
rs 10
1
<?php
2
/**
3
 * File containing the {@see Mailcode_Collection} class.
4
 *
5
 * @package Mailcode
6
 * @subpackage Collection
7
 * @see Mailcode_Collection
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
use AppUtils\OperationResult;
15
16
/**
17
 * Commands collection: container for commands.
18
 *
19
 * @package Mailcode
20
 * @subpackage Collection
21
 * @author Sebastian Mordziol <[email protected]>
22
 */
23
class Mailcode_Collection
24
{
25
    public const ERROR_CANNOT_RETRIEVE_FIRST_ERROR = 52301;
26
    public const ERROR_CANNOT_MODIFY_FINALIZED = 52302;
27
    public const ERROR_NO_VALIDATION_RESULT_AVAILABLE = 52303;
28
    
29
   /**
30
    * @var Mailcode_Commands_Command[]
31
    */
32
    protected array $commands = array();
33
    
34
    /**
35
     * @var bool
36
     */
37
    private bool $finalized = false;
38
39
    /**
40
     * Adds a command to the collection.
41
     *
42
     * @param Mailcode_Commands_Command $command
43
     * @return Mailcode_Collection
44
     * @throws Mailcode_Exception
45
     */
46
    public function addCommand(Mailcode_Commands_Command $command) : Mailcode_Collection
47
    {
48
        if($this->finalized)
49
        {
50
            throw new Mailcode_Exception(
51
                'Cannot add commands to a finalized collection',
52
                'When a collection has been finalized, it may not be modified anymore.',
53
                self::ERROR_CANNOT_MODIFY_FINALIZED
54
            );
55
        }
56
57
        $this->commands[] = $command;
58
59
        // reset the collection's validation result, since it
60
        // depends on the commands.
61
        $this->validationResult = null;
62
        
63
        return $this;
64
    }
65
    
66
   /**
67
    * Whether there are any commands in the collection.
68
    * 
69
    * @return bool
70
    */
71
    public function hasCommands() : bool
72
    {
73
        return !empty($this->commands);
74
    }
75
    
76
   /**
77
    * Counts the amount of commands in the collection.
78
    * 
79
    * @return int
80
    */
81
    public function countCommands() : int
82
    {
83
        return count($this->commands);
84
    }
85
86
    public function removeCommand(Mailcode_Commands_Command $command) : void
87
    {
88
        $keep = array();
89
90
        foreach($this->commands as $existing)
91
        {
92
            if($existing !== $command)
93
            {
94
                $keep[] = $existing;
95
            }
96
        }
97
98
        $this->commands = $keep;
99
    }
100
    
101
   /**
102
    * Retrieves all commands that were detected, in the exact order
103
    * they were found.
104
    * 
105
    * @return Mailcode_Commands_Command[]
106
    */
107
    public function getCommands() : array
108
    {
109
        $this->validate();
110
111
        return $this->commands;
112
    }
113
114
    /**
115
     * Retrieves all unique commands by their matched
116
     * string hash: this ensures only commands that were
117
     * written the exact same way (including spacing)
118
     * are returned.
119
     *
120
     * @return Mailcode_Commands_Command[]
121
     * @throws Mailcode_Exception
122
     */
123
    public function getGroupedByHash() : array
124
    {
125
        $this->validate();
126
127
        $hashes = array();
128
        
129
        foreach($this->commands as $command)
130
        {
131
            $hash = $command->getHash();
132
            
133
            if(!isset($hashes[$hash]))
134
            {
135
                $hashes[$hash] = $command;
136
            }
137
        }
138
            
139
        return array_values($hashes);
140
    }
141
142
    /**
143
     * Adds several commands at once.
144
     *
145
     * @param Mailcode_Commands_Command[] $commands
146
     * @return Mailcode_Collection
147
     * @throws Mailcode_Exception
148
     */
149
    public function addCommands(array $commands) : Mailcode_Collection
150
    {
151
        foreach($commands as $command)
152
        {
153
            $this->addCommand($command);
154
        }
155
        
156
        return $this;
157
    }
158
159
    public function mergeWith(Mailcode_Collection $collection) : Mailcode_Collection
160
    {
161
        $merged = new Mailcode_Collection();
162
        $merged->addCommands($this->getCommands());
163
        $merged->addCommands($collection->getCommands());
164
165
        return $merged;
166
    }
167
    
168
    public function getVariables() : Mailcode_Variables_Collection
169
    {
170
        $this->validate();
171
172
        $collection = new Mailcode_Variables_Collection_Regular();
173
        
174
        foreach($this->commands as $command)
175
        {
176
            $collection->mergeWith($command->getVariables());
177
        }
178
        
179
        return $collection;
180
    }
181
182
    // region: Validation
183
184
    /**
185
     * @var Mailcode_Collection_Error[]
186
     */
187
    protected array $errors = array();
188
189
    /**
190
     * @var OperationResult|NULL
191
     */
192
    protected ?OperationResult $validationResult = null;
193
194
    /**
195
     * @var bool
196
     */
197
    private bool $validating = false;
198
199
    /**
200
     * Whether the collection has been validated yet. This is used
201
     * primarily in the test suites.
202
     *
203
     * @return bool
204
     */
205
    public function hasBeenValidated() : bool
206
    {
207
        return isset($this->validationResult);
208
    }
209
210
    public function getValidationResult() : OperationResult
211
    {
212
        $this->validate();
213
214
        if(isset($this->validationResult))
215
        {
216
            return $this->validationResult;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->validationResult could return the type null which is incompatible with the type-hinted return AppUtils\OperationResult. Consider adding an additional type-check to rule them out.
Loading history...
217
        }
218
219
        throw new Mailcode_Exception(
220
            'No validation result stored.',
221
            '',
222
            self::ERROR_NO_VALIDATION_RESULT_AVAILABLE
223
        );
224
    }
225
226
    private function validate() : void
227
    {
228
        if(isset($this->validationResult) || $this->validating)
229
        {
230
            return;
231
        }
232
233
        // The nesting validator calls the getCommands() method, which
234
        // creates a circular call, since that calls validate(). To
235
        // avoid this issue, we use the validating flag.
236
        $this->validating = true;
237
        
238
        $nesting = new Mailcode_Collection_NestingValidator($this);
239
        $this->validationResult = $nesting->validate();
240
241
        $this->validating = false;
242
    }
243
    
244
    public function hasErrorCode(int $code) : bool
245
    {
246
        $errors = $this->getErrors();
247
        
248
        foreach($errors as $error)
249
        {
250
            if($error->getCode() === $code)
251
            {
252
                return true;
253
            }
254
        }
255
        
256
        return false;
257
    }
258
259
    public function addErrorMessage(string $matchedText, string $message, int $code) : void
260
    {
261
        $this->errors[] = new Mailcode_Collection_Error_Message(
262
            $matchedText,
263
            $code,
264
            $message
265
        );
266
    }
267
268
    public function addInvalidCommand(Mailcode_Commands_Command $command) : void
269
    {
270
        // Remove the command in case it was already added
271
        $this->removeCommand($command);
272
273
        $this->errors[] = new Mailcode_Collection_Error_Command($command);
274
    }
275
276
    /**
277
     * @return Mailcode_Collection_Error[]
278
     */
279
    public function getErrors() : array
280
    {
281
        $result = $this->getValidationResult();
282
283
        $errors = $this->errors;
284
285
        if(!$result->isValid())
286
        {
287
            $errors[] = new Mailcode_Collection_Error_Message(
288
                '',
289
                $result->getCode(),
290
                $result->getErrorMessage()
291
            );
292
        }
293
294
        return $errors;
295
    }
296
297
    public function getFirstError() : Mailcode_Collection_Error
298
    {
299
        $errors = $this->getErrors();
300
301
        if(!empty($errors))
302
        {
303
            return array_shift($errors);
304
        }
305
306
        throw new Mailcode_Exception(
307
            'Cannot retrieve first error: no errors detected',
308
            null,
309
            self::ERROR_CANNOT_RETRIEVE_FIRST_ERROR
310
        );
311
    }
312
313
    public function isValid() : bool
314
    {
315
        $errors = $this->getErrors();
316
317
        return empty($errors);
318
    }
319
320
    private function validateNesting() : void
321
    {
322
        foreach($this->commands as $command)
323
        {
324
            $command->validateNesting();
325
326
            if(!$command->isValid()) {
327
                $this->addInvalidCommand($command);
328
            }
329
        }
330
    }
331
332
    // endregion
333
334
    // region: Getting filtered commands
335
336
    /**
337
     * Retrieves only ShowXXX commands in the collection, if any.
338
     * Includes ShowVariable, ShowDate, ShowNumber, ShowSnippet.
339
     *
340
     * @return Mailcode_Commands_ShowBase[]
341
     */
342
    public function getShowCommands(): array
343
    {
344
        return Mailcode_Collection_TypeFilter::getShowCommands($this->commands);
345
    }
346
347
    /**
348
     * Retrieves all commands that implement the ListVariables interface,
349
     * meaning that they use list variables.
350
     *
351
     * @return Mailcode_Interfaces_Commands_ListVariables[]
352
     * @see Mailcode_Interfaces_Commands_ListVariables
353
     */
354
    public function getListVariableCommands() : array
355
    {
356
        return Mailcode_Collection_TypeFilter::getListVariableCommands($this->commands);
357
    }
358
359
    /**
360
    * Retrieves only show variable commands in the collection, if any.
361
    * 
362
    * @return Mailcode_Commands_Command_ShowVariable[]
363
    */
364
    public function getShowVariableCommands(): array
365
    {
366
        return Mailcode_Collection_TypeFilter::getShowVariableCommands($this->commands);
367
    }
368
369
    /**
370
     * @return Mailcode_Commands_Command_For[]
371
     */
372
    public function getForCommands() : array
373
    {
374
        return Mailcode_Collection_TypeFilter::getForCommands($this->commands);
375
    }
376
377
   /**
378
    * Retrieves only show date commands in the collection, if any.
379
    *
380
    * @return Mailcode_Commands_Command_ShowDate[]
381
    */
382
    public function getShowDateCommands() : array
383
    {
384
        return Mailcode_Collection_TypeFilter::getShowDateCommands($this->commands);
385
    }
386
387
    /**
388
     * Retrieves only if commands in the collection, if any.
389
     *
390
     * @return Mailcode_Commands_Command_If[]
391
     */
392
    public function getIfCommands() : array
393
    {
394
        return Mailcode_Collection_TypeFilter::getIfCommands($this->commands);
395
    }
396
397
    // endregion
398
399
    public function getFirstCommand() : ?Mailcode_Commands_Command
400
    {
401
        $commands = $this->getCommands();
402
        
403
        if(!empty($commands))
404
        {
405
            return array_shift($commands);
406
        }
407
        
408
        return null;
409
    }
410
411
    public function finalize() : void
412
    {
413
        $this->finalized = true;
414
415
        $this->validateNesting();
416
        $this->pruneProtectedContentCommands();
417
    }
418
419
    public function isFinalized() : bool
420
    {
421
        return $this->finalized;
422
    }
423
424
    /**
425
     * Any commands that are nested in protected content
426
     * commands must be removed from the collection.
427
     * These commands are not interpreted (or interpreted
428
     * independently, depending on the command), and thus
429
     * must not be treated as members of the collection.
430
     *
431
     * @return void
432
     */
433
    private function pruneProtectedContentCommands() : void
434
    {
435
        if(!$this->isValid())
436
        {
437
            return;
438
        }
439
440
        $keep = array();
441
442
        foreach($this->commands as $command)
443
        {
444
            if(!$command->hasContentParent())
445
            {
446
                $keep[] = $command;
447
            }
448
        }
449
450
        $this->commands = $keep;
451
    }
452
}
453