Completed
Push — master ( fd9cc4...882132 )
by James Ekow Abaka
02:03
created

FilterCompiler::parseIn()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 15
cts 15
cp 1
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 17
nc 2
nop 0
crap 3
1
<?php
2
3
namespace ntentan\nibii;
4
5
use ntentan\nibii\exceptions\FilterCompilerException;
6
7
/**
8
 * Safely compiles SQL conditions to ensure that a portable interface is provided
9
 * through which conditions can be specified accross database platforms. Also
10
 * the FilterCompiler ensures that raw data is never passed through queries.
11
 * This is done in order to minimize injection errors.
12
 */
13
class FilterCompiler
14
{
15
16
    private $lookahead;
17
    private $token;
18
    private $filter;
19
    private $tokens = array(
20
        'equals' => '\=',
21
        'number' => '[0-9]+',
22
        'cast' => 'cast\b',
23
        'as' => 'as\b',
24
        'between' => 'between\b',
25
        'in' => 'in\b',
26
        'like' => 'like\b',
27
        'is' => 'is\b',
28
        'and' => 'and\b',
29
        'not' => 'not\b',
30
        'or' => 'or\b',
31
        'greater_or_equal' => '\>\=',
32
        'less_or_equal' => '\<\=',
33
        'not_equal' => '\<\>',
34
        'greater' => '\>',
35
        'less' => '\<',
36
        'add' => '\+',
37
        'subtract' => '\-',
38
        'multiply' => '\*',
39
        'function' => '[a-zA-Z][a-zA-Z0-9\_]*\s*\(',
40
        'identifier' => '[a-zA-Z][a-zA-Z0-9\.\_\:]*\b',
41
        'named_bind_param' => '\:[a-z_][a-z0-9\_]+',
42
        'position_bind_param' => '\\?',
43
        'obracket' => '\(',
44
        'cbracket' => '\)',
45
        'comma' => ','
46
    );
47
    private $operators = array(
48
        array('between', 'or', 'like'),
49
        array('and'),
50
        array('not'),
51
        array('equals', 'greater', 'less', 'greater_or_equal', 'less_or_equal', 'not_equal', 'is'),
52
        array('add', 'subtract'),
53
        array('in'),
54
        array('multiply')
55
    );
56
    private $numPositions = 0;
57
58 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...
59
    {
60 14
        $this->filter = $filter;
61 14
        $this->getToken();
62 14
        $expression = $this->parseExpression();
63 12
        if ($this->token !== false) {
64 4
            throw new FilterCompilerException("Unexpected '" . $this->token . "' in filter [$filter]");
65
        }
66 8
        $parsed = $this->renderExpression($expression);
67 8
        return $parsed;
68
    }
69
70 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...
71
    {
72 8
        if (is_array($expression)) {
73 8
            $expression = $this->renderExpression($expression['left']) . " {$expression['opr']} " . $this->renderExpression($expression['right']);
74
        }
75 8
        return $expression;
76
    }
77
78 4
    private function match($tokens)
79
    {
80 4
        if (is_string($tokens)) {
81 4
            $tokens = [$tokens];
82
        }
83 4
        if (array_search($this->lookahead, $tokens) === false) {
84
            throw new FilterCompilerException("Expected " . implode(' or ', $tokens) . " but found " . $this->lookahead);
85
        }
86 4
    }
87
88 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...
89
    {
90 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
91 2
        $left = $this->token;
92 2
        $this->getToken();
93 2
        $this->match('and');
94 2
        $this->getToken();
95 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
96 2
        $right = $this->token;
97 2
        $this->getToken();
98 2
        return "$left AND $right";
99
    }
100
101 2
    private function parseIn()
102
    {
103 2
        $expression = "(";
104 2
        $this->match('obracket');
105 2
        $this->getToken();
106
107
        do {
108 2
            $expression .= $this->parseExpression();
109 2
            if ($this->lookahead === 'comma') {
110 2
                $expression .= ',';
111 2
                $this->getToken();
112 2
                continue;
113
            } else {
114 2
                break;
115
            }
116 2
        } while (true);
117
118 2
        $this->match('cbracket');
119
120 2
        $this->getToken();
121
122 2
        $expression .= ')';
123 2
        return $expression;
124
    }
125
126 4
    private function parseFunctionParams()
127
    {
128 4
        $parameters = '';
129 4
        $size = 0;
130
        do {
131 4
            $size++;
132 4
            $parameters .= $this->renderExpression($this->parseExpression());
133 4
            if ($this->lookahead == 'comma') {
134 2
                $this->getToken();
135 2
                $parameters .= ", ";
136 4
            } else if ($this->lookahead == 'cbracket') {
137 4
                break;
138
            }
139 2
        } while ($size < 100);
140 4
        return $parameters;
141
    }
142
143 2
    private function parseCast()
144
    {
145 2
        $return = 'cast(';
146 2
        $this->getToken();
147 2
        $this->match('obracket');
148 2
        $this->getToken();
149 2
        $return .= $this->renderExpression($this->parseExpression());
150 2
        $this->match('as');
151 2
        $return .= ' as ';
152 2
        $this->getToken();
153 2
        $this->match('identifier');
154 2
        $return .= $this->token;
155 2
        $this->getToken();
156 2
        $this->match('cbracket');
157 2
        $return .= ')';
158 2
        return $return;
159
    }
160
161 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...
162
    {
163 4
        $name = $this->token;
164 4
        $this->getToken();
165 4
        $parameters = $this->parseFunctionParams();
166 4
        return "$name$parameters)";
167
    }
168
169 12
    private function returnToken()
170
    {
171 12
        return $this->token;
172
    }
173
174 10
    private function returnPositionTag()
175
    {
176 10
        return ":filter_bind_" . (++$this->numPositions);
177
    }
178
179 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...
180
    {
181 2
        $this->getToken();
182 2
        $expression = $this->parseExpression();
183 2
        return $this->renderExpression($expression);
184
    }
185
186 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...
187
    {
188 14
        $return = null;
189
        $methods = [
190 14
            'cast' => 'parseCast',
191
            'function' => 'parseFunction',
192
            'identifier' => 'returnToken',
193
            'named_bind_param' => 'returnToken',
194
            'number' => 'returnToken',
195
            'position_bind_param' => 'returnPositionTag',
196
            'obracket' => 'parseObracket'
197
        ];
198
199 14
        if (isset($methods[$this->lookahead])) {
200 12
            $method = $methods[$this->lookahead];
201 12
            $return = $this->$method();
202
        }
203
204 14
        $this->getToken();
205 14
        return $return;
206
    }
207
208 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...
209
    {
210
        switch ($opr) {
211 10
            case 'between':
212 2
                return $this->parseBetween();
213 10
            case 'in':
214 2
                return $this->parseIn();
215
            default:
216 10
                return $this->parseExpression($level);
217
        }
218
    }
219
220 14
    private function parseExpression($level = 0)
221
    {
222 14
        if ($level === count($this->operators)) {
223 14
            return $this->parseFactor();
224
        } else {
225 14
            $expression = $this->parseExpression($level + 1);
226
        }
227
228 14
        while ($this->token != false) {
229 14
            if (array_search($this->lookahead, $this->operators[$level]) !== false) {
230 12
                $left = $expression;
231 12
                $opr = $this->token;
232 12
                $this->getToken();
233 10
                $right = $this->parseRightExpression($level + 1, strtolower($opr));
234
                $expression = array(
235 10
                    'left' => $left,
236 10
                    'opr' => $opr,
237 10
                    'right' => $right
238
                );
239
            } else {
240 14
                break;
241
            }
242
        }
243
244 14
        return $expression;
245
    }
246
247 14
    private function getToken()
248
    {
249 14
        $this->eatWhite();
250 14
        $this->token = false;
251 14
        foreach ($this->tokens as $token => $regex) {
252 14
            if (preg_match("/^$regex/i", $this->filter, $matches)) {
253 14
                $this->filter = substr($this->filter, strlen($matches[0]));
254 14
                $this->lookahead = $token;
255 14
                $this->token = $matches[0];
256 14
                break;
257
            }
258
        }
259
260 14
        if ($this->token === false && strlen($this->filter) > 0) {
261 2
            throw new FilterCompilerException("Unexpected character [" . $this->filter[0] . "] begining " . $this->filter . ".");
262
        }
263 14
    }
264
265 14
    private function eatWhite()
266
    {
267 14
        if (preg_match("/^\s*/", $this->filter, $matches)) {
268 14
            $this->filter = substr($this->filter, strlen($matches[0]));
269
        }
270 14
    }
271
272 6
    public function rewriteBoundData($data)
273
    {
274 6
        $rewritten = [];
275 6
        foreach ($data as $key => $value) {
276 6
            if (is_numeric($key)) {
277 6
                $rewritten["filter_bind_" . ($key + 1)] = $value;
278
            } else {
279 6
                $rewritten[$key] = $value;
280
            }
281
        }
282 6
        return $rewritten;
283
    }
284
285
}
286