FilterCompiler::match()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
rs 9.9666
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3.0416
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2014-2018 James Ekow Abaka Ainooson
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace ntentan\nibii;
28
29
use ntentan\nibii\exceptions\FilterCompilerException;
30
31
/**
32
 * Safely compiles SQL conditions to ensure that a portable interface is provided
33
 * through which conditions can be specified accross database platforms. Also
34
 * the FilterCompiler ensures that raw data is never passed through queries.
35
 * This is done in order to minimize injection errors.
36
 */
37
class FilterCompiler
38
{
39
    private $lookahead;
40
    private $token;
41
    private $filter;
42
    private $tokens = [
43
        'equals'              => '\=',
44
        'number'              => '[0-9]+',
45
        'cast'                => 'cast\b',
46
        'as'                  => 'as\b',
47
        'between'             => 'between\b',
48
        'in'                  => 'in\b',
49
        'like'                => 'like\b',
50
        'is'                  => 'is\b',
51
        'and'                 => 'and\b',
52
        'not'                 => 'not\b',
53
        'or'                  => 'or\b',
54
        'greater_or_equal'    => '\>\=',
55
        'less_or_equal'       => '\<\=',
56
        'not_equal'           => '\<\>',
57
        'greater'             => '\>',
58
        'less'                => '\<',
59
        'add'                 => '\+',
60
        'subtract'            => '\-',
61
        'multiply'            => '\*',
62
        'function'            => '[a-zA-Z][a-zA-Z0-9\_]*\s*\(',
63
        'identifier'          => '[a-zA-Z][a-zA-Z0-9\.\_\:]*\b',
64
        'named_bind_param'    => '\:[a-z_][a-z0-9\_]+',
65
        'position_bind_param' => '\\?',
66
        'obracket'            => '\(',
67
        'cbracket'            => '\)',
68
        'comma'               => ',',
69
    ];
70
    private $operators = [
71
        ['between', 'or', 'like'],
72
        ['and'],
73
        ['not'],
74
        ['equals', 'greater', 'less', 'greater_or_equal', 'less_or_equal', 'not_equal', 'is'],
75
        ['add', 'subtract'],
76
        ['in'],
77
        ['multiply'],
78
    ];
79
    private $numPositions = 0;
80
81 14
    public function compile($filter)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
82
    {
83 14
        $this->filter = $filter;
84 14
        $this->getToken();
85 14
        $expression = $this->parseExpression();
86 12
        if ($this->token !== false) {
87 4
            throw new FilterCompilerException("Unexpected '".$this->token."' in filter [$filter]");
88
        }
89 8
        $parsed = $this->renderExpression($expression);
90
91 8
        return $parsed;
92
    }
93
94 8
    private function renderExpression($expression)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
95
    {
96 8
        if (is_array($expression)) {
97 8
            $expression = $this->renderExpression($expression['left'])." {$expression['opr']} ".$this->renderExpression($expression['right']);
98
        }
99
100 8
        return $expression;
101
    }
102
103 4
    private function match($tokens)
104
    {
105 4
        if (is_string($tokens)) {
106 4
            $tokens = [$tokens];
107
        }
108 4
        if (array_search($this->lookahead, $tokens) === false) {
109
            throw new FilterCompilerException('Expected '.implode(' or ', $tokens).' but found '.$this->lookahead);
110
        }
111 4
    }
112
113 2
    private function parseBetween()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
114
    {
115 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
116 2
        $left = $this->token;
117 2
        $this->getToken();
118 2
        $this->match('and');
119 2
        $this->getToken();
120 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
121 2
        $right = $this->token;
122 2
        $this->getToken();
123
124 2
        return "$left AND $right";
125
    }
126
127 2
    private function parseIn()
128
    {
129 2
        $expression = '(';
130 2
        $this->match('obracket');
131 2
        $this->getToken();
132
133
        do {
134 2
            $expression .= $this->parseExpression();
135 2
            if ($this->lookahead === 'comma') {
136 2
                $expression .= ',';
137 2
                $this->getToken();
138 2
                continue;
139
            } else {
140 2
                break;
141
            }
142 2
        } while (true);
143
144 2
        $this->match('cbracket');
145
146 2
        $this->getToken();
147
148 2
        $expression .= ')';
149
150 2
        return $expression;
151
    }
152
153 4
    private function parseFunctionParams()
154
    {
155 4
        $parameters = '';
156 4
        $size = 0;
157
        do {
158 4
            $size++;
159 4
            $parameters .= $this->renderExpression($this->parseExpression());
160 4
            if ($this->lookahead == 'comma') {
161 2
                $this->getToken();
162 2
                $parameters .= ', ';
163 4
            } elseif ($this->lookahead == 'cbracket') {
164 4
                break;
165
            }
166 2
        } while ($size < 100);
167
168 4
        return $parameters;
169
    }
170
171 2
    private function parseCast()
172
    {
173 2
        $return = 'cast(';
174 2
        $this->getToken();
175 2
        $this->match('obracket');
176 2
        $this->getToken();
177 2
        $return .= $this->renderExpression($this->parseExpression());
178 2
        $this->match('as');
179 2
        $return .= ' as ';
180 2
        $this->getToken();
181 2
        $this->match('identifier');
182 2
        $return .= $this->token;
183 2
        $this->getToken();
184 2
        $this->match('cbracket');
185 2
        $return .= ')';
186
187 2
        return $return;
188
    }
189
190 4
    private function parseFunction()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
191
    {
192 4
        $name = $this->token;
193 4
        $this->getToken();
194 4
        $parameters = $this->parseFunctionParams();
195
196 4
        return "$name$parameters)";
197
    }
198
199 12
    private function returnToken()
200
    {
201 12
        return $this->token;
202
    }
203
204 10
    private function returnPositionTag()
205
    {
206 10
        return ':filter_bind_'.(++$this->numPositions);
207
    }
208
209 2
    private function parseObracket()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
210
    {
211 2
        $this->getToken();
212 2
        $expression = $this->parseExpression();
213
214 2
        return $this->renderExpression($expression);
215
    }
216
217 14
    private function parseFactor()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
218
    {
219 14
        $return = null;
220
        $methods = [
221 14
            'cast'                => 'parseCast',
222
            'function'            => 'parseFunction',
223
            'identifier'          => 'returnToken',
224
            'named_bind_param'    => 'returnToken',
225
            'number'              => 'returnToken',
226
            'position_bind_param' => 'returnPositionTag',
227
            'obracket'            => 'parseObracket',
228
        ];
229
230 14
        if (isset($methods[$this->lookahead])) {
231 12
            $method = $methods[$this->lookahead];
232 12
            $return = $this->$method();
233
        }
234
235 14
        $this->getToken();
236
237 14
        return $return;
238
    }
239
240 10
    private function parseRightExpression($level, $opr)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
241
    {
242 10
        switch ($opr) {
243 10
            case 'between':
244 2
                return $this->parseBetween();
245 10
            case 'in':
246 2
                return $this->parseIn();
247
            default:
248 10
                return $this->parseExpression($level);
249
        }
250
    }
251
252 14
    private function parseExpression($level = 0)
253
    {
254 14
        if ($level === count($this->operators)) {
255 14
            return $this->parseFactor();
256
        } else {
257 14
            $expression = $this->parseExpression($level + 1);
258
        }
259
260 14
        while ($this->token != false) {
261 14
            if (array_search($this->lookahead, $this->operators[$level]) !== false) {
262 12
                $left = $expression;
263 12
                $opr = $this->token;
264 12
                $this->getToken();
265 10
                $right = $this->parseRightExpression($level + 1, strtolower($opr));
266
                $expression = [
267 10
                    'left'  => $left,
268 10
                    'opr'   => $opr,
269 10
                    'right' => $right,
270
                ];
271
            } else {
272 14
                break;
273
            }
274
        }
275
276 14
        return $expression;
277
    }
278
279 14
    private function getToken()
280
    {
281 14
        $this->eatWhite();
282 14
        $this->token = false;
283 14
        foreach ($this->tokens as $token => $regex) {
284 14
            if (preg_match("/^$regex/i", $this->filter, $matches)) {
285 14
                $this->filter = substr($this->filter, strlen($matches[0]));
286 14
                $this->lookahead = $token;
287 14
                $this->token = $matches[0];
288 14
                break;
289
            }
290
        }
291
292 14
        if ($this->token === false && strlen($this->filter) > 0) {
293 2
            throw new FilterCompilerException('Unexpected character ['.$this->filter[0].'] begining '.$this->filter.'.');
294
        }
295 14
    }
296
297 14
    private function eatWhite()
298
    {
299 14
        if (preg_match("/^\s*/", $this->filter, $matches)) {
300 14
            $this->filter = substr($this->filter, strlen($matches[0]));
301
        }
302 14
    }
303
304 6
    public function rewriteBoundData($data)
305
    {
306 6
        $rewritten = [];
307 6
        foreach ($data as $key => $value) {
308 6
            if (is_numeric($key)) {
309 6
                $rewritten['filter_bind_'.($key + 1)] = $value;
310
            } else {
311 2
                $rewritten[$key] = $value;
312
            }
313
        }
314
315 6
        return $rewritten;
316
    }
317
}
318