Passed
Push — master ( c355fd...36c5b6 )
by Sebastian
04:30
created

Mailcode_Parser::replaceBrackets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * File containing the {@see Mailcode_Parser} class.
4
 * 
5
 * @package Mailcode
6
 * @subpackage Parser
7
 * @see Mailcode_Parser
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
/**
15
 * Mailcode parser, capable of detecting commands in strings.
16
 * 
17
 * @package Mailcode
18
 * @subpackage Parser
19
 * @author Sebastian Mordziol <[email protected]>
20
 */
21
class Mailcode_Parser
22
{
23
    public const ERROR_NOT_A_COMMAND = 73301;
24
25
    public const COMMAND_REGEX_PARTS = array( 
26
        '{\s*([a-z]+)\s*}',
27
        '{\s*([a-z]+)\s*:([^}]*)}',
28
        '{\s*([a-z]+)\s+([a-z-]+)\s*:([^}]*)}'
29
    );
30
    
31
   /**
32
    * @var Mailcode
33
    */
34
    protected $mailcode;
35
    
36
   /**
37
    * @var Mailcode_Commands
38
    */
39
    protected $commands;
40
41
    /**
42
     * @var Mailcode_Commands_Command_Type_Opening[]
43
     */
44
    protected $stack = array();
45
46
    public function __construct(Mailcode $mailcode)
47
    {
48
        $this->mailcode = $mailcode;
49
        $this->commands = $this->mailcode->getCommands();
50
    }
51
    
52
   /**
53
    * Gets the regex format string used to detect commands.
54
    * 
55
    * @return string
56
    */
57
    protected static function getRegex() : string
58
    {
59
        return '/'.implode('|', self::COMMAND_REGEX_PARTS).'/sixU';
60
    }
61
62
    /**
63
     * Parses a string to detect all commands within. Returns a
64
     * collection instance that contains information on all the
65
     * commands.
66
     *
67
     * @param string $string
68
     * @return Mailcode_Collection A collection with all unique commands found.
69
     * @throws Mailcode_Exception
70
     */
71
    public function parseString(string $string) : Mailcode_Collection
72
    {
73
        $string = $this->prepareString($string);
74
75
        $matches = array();
76
        preg_match_all(self::getRegex(), $string, $matches, PREG_PATTERN_ORDER);
77
        
78
        $total = count($matches[0]);
79
        $collection = new Mailcode_Collection();
80
81
        for($i=0; $i < $total; $i++)
82
        {
83
            $match = $this->parseMatch($matches, $i);
84
            
85
            $this->processMatch($match, $collection);
86
        }
87
88
        $collection->finalize();
89
90
        return $collection;
91
    }
92
93
    protected function prepareString(string $subject) : string
94
    {
95
        return (new Mailcode_Parser_StringPreProcessor($subject))->process();
96
    }
97
98
    /**
99
     * Processes a single match found in the string: creates the command,
100
     * and adds it to the collection if it's a valid command, or to the list
101
     * of invalid commands otherwise.
102
     *
103
     * @param Mailcode_Parser_Match $match
104
     * @param Mailcode_Collection $collection
105
     * @throws Mailcode_Exception
106
     */
107
    protected function processMatch(Mailcode_Parser_Match $match, Mailcode_Collection $collection) : void
108
    {
109
        $name = $match->getName();
110
111
        if (!$this->commands->nameExists($name)) {
112
            $collection->addErrorMessage(
113
                $match->getMatchedString(),
114
                t('No command found with the name %1$s.', $name),
115
                Mailcode_Commands_Command::VALIDATION_UNKNOWN_COMMAND_NAME
116
            );
117
            return;
118
        }
119
120
        $cmd = $this->commands->createCommand(
121
            $this->commands->getIDByName($name),
122
            $match->getType(),
123
            $match->getParams(),
124
            $match->getMatchedString()
125
        );
126
127
        $this->handleNesting($cmd);
128
129
        if (!$cmd->isValid())
130
        {
131
            $collection->addInvalidCommand($cmd);
132
            return;
133
        }
134
135
        $collection->addCommand($cmd);
136
    }
137
138
    /**
139
     * @param Mailcode_Commands_Command $cmd
140
     * @return void
141
     * @throws Mailcode_Exception
142
     */
143
    private function handleNesting(Mailcode_Commands_Command $cmd) : void
144
    {
145
        // Set the command's parent from the stack, if any is present.
146
        if(!empty($this->stack))
147
        {
148
            $cmd->setParent($this->getStackLast());
149
        }
150
151
        // Handle opening and closing commands, adding and removing from the stack.
152
        if($cmd instanceof Mailcode_Commands_Command_Type_Opening)
153
        {
154
            $this->stack[] = $cmd;
155
        }
156
        else if($cmd instanceof Mailcode_Commands_Command_Type_Closing)
157
        {
158
            array_pop($this->stack);
159
        }
160
    }
161
162
    /**
163
     * @return Mailcode_Commands_Command
164
     * @throws Mailcode_Exception
165
     */
166
    private function getStackLast() : Mailcode_Commands_Command
167
    {
168
        $cmd = $this->stack[array_key_last($this->stack)];
169
170
        if($cmd instanceof Mailcode_Commands_Command)
171
        {
172
            return $cmd;
173
        }
174
175
        throw new Mailcode_Exception('Not a command', '', self::ERROR_NOT_A_COMMAND);
176
    }
177
    
178
   /**
179
    * Parses a single regex match: determines which named group
180
    * matches, and retrieves the according information.
181
    * 
182
    * @param array[] $matches The regex results array.
183
    * @param int $index The matched index.
184
    * @return Mailcode_Parser_Match
185
    */
186
    protected function parseMatch(array $matches, int $index) : Mailcode_Parser_Match
187
    {
188
        $name = ''; // the command name, e.g. "showvar"
189
        $type = '';
190
        $params = '';
191
        
192
        // 1 = single command
193
        // 2 = parameter command, name
194
        // 3 = parameter command, params
195
        // 4 = parameter type command, name
196
        // 5 = parameter type command, type
197
        // 6 = parameter type command, params
198
        
199
        if(!empty($matches[1][$index]))
200
        {
201
            $name = $matches[1][$index];
202
        }
203
        else if(!empty($matches[2][$index]))
204
        {
205
            $name = $matches[2][$index];
206
            $params = $matches[3][$index];
207
        }
208
        else if(!empty($matches[4][$index]))
209
        {
210
            $name = $matches[4][$index];
211
            $type = $matches[5][$index];
212
            $params = $matches[6][$index];
213
        }
214
        
215
        return new Mailcode_Parser_Match(
216
            $name, 
217
            $type, 
218
            $params, 
219
            $matches[0][$index]
220
        );
221
    }
222
    
223
   /**
224
    * Creates an instance of the safeguard tool, which
225
    * is used to safeguard commands in a string with placeholders.
226
    * 
227
    * @param string $subject The string to use to safeguard commands in.
228
    * @return Mailcode_Parser_Safeguard
229
    * @see Mailcode_Parser_Safeguard
230
    */
231
    public function createSafeguard(string $subject) : Mailcode_Parser_Safeguard
232
    {
233
        return new Mailcode_Parser_Safeguard($this, $subject);
234
    }
235
236
    /**
237
     * Creates a statement parser, which is used to validate arbitrary
238
     * command statements.
239
     *
240
     * @param string $statement
241
     * @param bool $freeform
242
     * @param Mailcode_Commands_Command|null $sourceCommand
243
     * @return Mailcode_Parser_Statement
244
     */
245
    public function createStatement(string $statement, bool $freeform=false, ?Mailcode_Commands_Command $sourceCommand=null) : Mailcode_Parser_Statement
246
    {
247
        return new Mailcode_Parser_Statement($statement, $freeform, $sourceCommand);
248
    }
249
}
250