Passed
Push — master ( 6c661c...e8275e )
by Sebastian
04:14
created

Mailcode_Collection::validate()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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