Mailcode_Collection::getFirstCommand()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
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
                $result->getSubject()
292
            );
293
        }
294
295
        return $errors;
296
    }
297
298
    public function getFirstError() : Mailcode_Collection_Error
299
    {
300
        $errors = $this->getErrors();
301
302
        if(!empty($errors))
303
        {
304
            return array_shift($errors);
305
        }
306
307
        throw new Mailcode_Exception(
308
            'Cannot retrieve first error: no errors detected',
309
            null,
310
            self::ERROR_CANNOT_RETRIEVE_FIRST_ERROR
311
        );
312
    }
313
314
    public function isValid() : bool
315
    {
316
        $errors = $this->getErrors();
317
318
        return empty($errors);
319
    }
320
321
    private function validateNesting() : void
322
    {
323
        foreach($this->commands as $command)
324
        {
325
            $command->validateNesting();
326
327
            if(!$command->isValid()) {
328
                $this->addInvalidCommand($command);
329
            }
330
        }
331
    }
332
333
    // endregion
334
335
    // region: Getting filtered commands
336
337
    /**
338
     * Retrieves only ShowXXX commands in the collection, if any.
339
     * Includes ShowVariable, ShowDate, ShowNumber, ShowSnippet.
340
     *
341
     * @return Mailcode_Commands_ShowBase[]
342
     */
343
    public function getShowCommands(): array
344
    {
345
        return Mailcode_Collection_TypeFilter::getShowCommands($this->commands);
346
    }
347
348
    /**
349
     * Retrieves all commands that implement the ListVariables interface,
350
     * meaning that they use list variables.
351
     *
352
     * @return Mailcode_Interfaces_Commands_ListVariables[]
353
     * @see Mailcode_Interfaces_Commands_ListVariables
354
     */
355
    public function getListVariableCommands() : array
356
    {
357
        return Mailcode_Collection_TypeFilter::getListVariableCommands($this->commands);
358
    }
359
360
    /**
361
    * Retrieves only show variable commands in the collection, if any.
362
    * 
363
    * @return Mailcode_Commands_Command_ShowVariable[]
364
    */
365
    public function getShowVariableCommands(): array
366
    {
367
        return Mailcode_Collection_TypeFilter::getShowVariableCommands($this->commands);
368
    }
369
370
    /**
371
     * @return Mailcode_Commands_Command_For[]
372
     */
373
    public function getForCommands() : array
374
    {
375
        return Mailcode_Collection_TypeFilter::getForCommands($this->commands);
376
    }
377
378
   /**
379
    * Retrieves only show date commands in the collection, if any.
380
    *
381
    * @return Mailcode_Commands_Command_ShowDate[]
382
    */
383
    public function getShowDateCommands() : array
384
    {
385
        return Mailcode_Collection_TypeFilter::getShowDateCommands($this->commands);
386
    }
387
388
    /**
389
     * Retrieves only if commands in the collection, if any.
390
     *
391
     * @return Mailcode_Commands_Command_If[]
392
     */
393
    public function getIfCommands() : array
394
    {
395
        return Mailcode_Collection_TypeFilter::getIfCommands($this->commands);
396
    }
397
398
    // endregion
399
400
    public function getFirstCommand() : ?Mailcode_Commands_Command
401
    {
402
        $commands = $this->getCommands();
403
        
404
        if(!empty($commands))
405
        {
406
            return array_shift($commands);
407
        }
408
        
409
        return null;
410
    }
411
412
    public function finalize() : void
413
    {
414
        $this->finalized = true;
415
416
        $this->validateNesting();
417
        $this->pruneProtectedContentCommands();
418
    }
419
420
    public function isFinalized() : bool
421
    {
422
        return $this->finalized;
423
    }
424
425
    /**
426
     * Any commands that are nested in protected content
427
     * commands must be removed from the collection.
428
     * These commands are not interpreted (or interpreted
429
     * independently, depending on the command), and thus
430
     * must not be treated as members of the collection.
431
     *
432
     * @return void
433
     */
434
    private function pruneProtectedContentCommands() : void
435
    {
436
        if(!$this->isValid())
437
        {
438
            return;
439
        }
440
441
        $keep = array();
442
443
        foreach($this->commands as $command)
444
        {
445
            if(!$command->hasContentParent())
446
            {
447
                $keep[] = $command;
448
            }
449
        }
450
451
        $this->commands = $keep;
452
    }
453
454
    /**
455
     * @return int[]
456
     */
457
    public function getErrorCodes() : array
458
    {
459
        $errors = $this->getErrors();
460
        $result = array();
461
462
        foreach($errors as $error)
463
        {
464
            $result[] = $error->getCode();
465
        }
466
467
        return $result;
468
    }
469
}
470