Completed
Push — master ( 2b9193...abd76f )
by James Ekow Abaka
03:07
created

FilterCompiler::parseIn()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
ccs 15
cts 15
cp 1
cc 3
eloc 17
nc 2
nop 0
crap 3
1
<?php
2
3
namespace ntentan\nibii;
4
5
/**
6
 * Safely compiles SQL conditions to ensure that a portable interface is provided
7
 * through which conditions can be specified accross database platforms. Also
8
 * the FilterCompiler ensures that raw data is never passed through queries.
9
 * This is done in order to minimize injection errors.
10
 */
11
class FilterCompiler {
12
13
    private $lookahead;
14
    private $token;
15
    private $filter;
16
    private $tokens = array(
17
        'equals' => '\=',
18
        'number' => '[0-9]+',
19
        'cast' => 'cast\b',
20
        'as' => 'as\b',
21
        'between' => 'between\b',
22
        'in' => 'in\b',
23
        'like' => 'like\b',
24
        'is' => 'is\b',
25
        'and' => 'and\b',
26
        'not' => 'not\b',
27
        'or' => 'or\b',
28
        'greater_or_equal' => '\>\=',
29
        'less_or_equal' => '\<\=',
30
        'not_equal' => '\<\>',
31
        'greater' => '\>',
32
        'less' => '\<',
33
        'add' => '\+',
34
        'subtract' => '\-',
35
        'multiply' => '\*',
36
        'function' => '[a-zA-Z][a-zA-Z0-9\_]*\s*\(',
37
        'identifier' => '[a-zA-Z][a-zA-Z0-9\.\_\:]*\b',
38
        'named_bind_param' => '\:[a-z_][a-z0-9\_]+',
39
        'position_bind_param' => '\\?',
40
        'obracket' => '\(',
41
        'cbracket' => '\)',
42
        'comma' => ','
43
    );
44
    private $operators = array(
45
        array('between', 'or' /* , 'like' */),
46
        array('and'),
47
        array('not'),
48
        array('equals', 'greater', 'less', 'greater_or_equal', 'less_or_equal', 'not_equal', 'is'),
49
        array('add', 'subtract'),
50
        array('in'),
51
        array('multiply')
52
    );
53
    private $numPositions = 0;
54
55 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...
56 14
        $this->filter = $filter;
57 14
        $this->getToken();
58 14
        $expression = $this->parseExpression();
59 12
        if ($this->token !== false) {
60 4
            throw new FilterCompilerException("Unexpected '" . $this->token . "' in filter [$filter]");
61
        }
62 8
        $parsed = $this->renderExpression($expression);
63 8
        return $parsed;
64
    }
65
66 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...
67 8
        if (is_array($expression)) {
68 8
            $expression = $this->renderExpression($expression['left']) . " {$expression['opr']} " . $this->renderExpression($expression['right']);
69
        }
70 8
        return $expression;
71
    }
72
73 4
    private function match($tokens) {
74 4
        if (is_string($tokens)) {
75 4
            $tokens = [$tokens];
76
        }
77 4
        if (array_search($this->lookahead, $tokens) === false) {
78
            throw new FilterCompilerException("Expected " . implode(' or ', $tokens) . " but found " . $this->lookahead);
79
        }
80 4
    }
81
82 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...
83 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
84 2
        $left = $this->token;
85 2
        $this->getToken();
86 2
        $this->match('and');
87 2
        $this->getToken();
88 2
        $this->match(['named_bind_param', 'number', 'position_bind_param']);
89 2
        $right = $this->token;
90 2
        $this->getToken();
91 2
        return "$left AND $right";
92
    }
93
94 2
    private function parseIn() {
95 2
        $expression = "(";
96 2
        $this->match('obracket');
97 2
        $this->getToken();
98
99
        do {
100 2
            $expression .= $this->parseExpression();
101 2
            if ($this->lookahead === 'comma') {
102 2
                $expression .= ',';
103 2
                $this->getToken();
104 2
                continue;
105
            } else {
106 2
                break;
107
            }
108 2
        } while (true);
109
110 2
        $this->match('cbracket');
111
112 2
        $this->getToken();
113
114 2
        $expression .= ')';
115 2
        return $expression;
116
    }
117
118 4
    private function parseFunctionParams() {
119 4
        $parameters = '';
120 4
        $size = 0;
121
        do {
122 4
            $size++;
123 4
            $parameters .= $this->renderExpression($this->parseExpression());
124 4
            if ($this->lookahead == 'comma') {
125 2
                $this->getToken();
126 2
                $parameters .= ", ";
127 4
            } else if ($this->lookahead == 'cbracket') {
128 4
                break;
129
            }
130 2
        } while ($size < 100);
131 4
        return $parameters;
132
    }
133
134 2
    private function parseCast() {
135 2
        $return = 'cast(';
136 2
        $this->getToken();
137 2
        $this->match('obracket');
138 2
        $this->getToken();
139 2
        $return .= $this->renderExpression($this->parseExpression());
140 2
        $this->match('as');
141 2
        $return .= ' as ';
142 2
        $this->getToken();
143 2
        $this->match('identifier');
144 2
        $return .= $this->token;
145 2
        $this->getToken();
146 2
        $this->match('cbracket');
147 2
        $return .= ')';
148 2
        return $return;
149
    }
150
151 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...
152 4
        $name = $this->token;
153 4
        $this->getToken();
154 4
        $parameters = $this->parseFunctionParams();
155 4
        return "$name$parameters)";
156
    }
157
158 12
    private function returnToken() {
159 12
        return $this->token;
160
    }
161
162 10
    private function returnPositionTag() {
163 10
        return ":filter_bind_" . ( ++$this->numPositions);
164
    }
165
166 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...
167 2
        $this->getToken();
168 2
        $expression = $this->parseExpression();
169 2
        return $this->renderExpression($expression);
170
    }
171
172 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...
173 14
        $return = null;
174
        $methods = [
175 14
            'cast' => 'parseCast',
176
            'function' => 'parseFunction',
177
            'identifier' => 'returnToken',
178
            'named_bind_param' => 'returnToken',
179
            'number' => 'returnToken',
180
            'position_bind_param' => 'returnPositionTag',
181
            'obracket' => 'parseObracket'
182
        ];
183
184 14
        if (isset($methods[$this->lookahead])) {
185 12
            $method = $methods[$this->lookahead];
186 12
            $return = $this->$method();
187
        }
188
189 14
        $this->getToken();
190 14
        return $return;
191
    }
192
193 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...
194
        switch ($opr) {
195 10
            case 'between': return $this->parseBetween();
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
196 10
            case 'in': return $this->parseIn();
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
197 10
            default: return $this->parseExpression($level);
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
198
        }
199
    }
200
201 14
    private function parseExpression($level = 0) {
1 ignored issue
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...
202 14
        if ($level === count($this->operators)) {
203 14
            return $this->parseFactor();
204
        } else {
205 14
            $expression = $this->parseExpression($level + 1);
206
        }
207
208 14
        while ($this->token != false) {
209 14
            if (array_search($this->lookahead, $this->operators[$level]) !== false) {
210 12
                $left = $expression;
211 12
                $opr = $this->token;
212 12
                $this->getToken();
213 10
                $right = $this->parseRightExpression($level + 1, strtolower($opr));
214
                $expression = array(
215 10
                    'left' => $left,
216 10
                    'opr' => $opr,
217 10
                    'right' => $right
218
                );
219
            } else {
220 14
                break;
221
            }
222
        }
223
224 14
        return $expression;
225
    }
226
227 14
    private function getToken() {
228 14
        $this->eatWhite();
229 14
        $this->token = false;
230 14
        foreach ($this->tokens as $token => $regex) {
231 14
            if (preg_match("/^$regex/i", $this->filter, $matches)) {
232 14
                $this->filter = substr($this->filter, strlen($matches[0]));
233 14
                $this->lookahead = $token;
234 14
                $this->token = $matches[0];
235 14
                break;
236
            }
237
        }
238
239 14
        if ($this->token === false && strlen($this->filter) > 0) {
240 2
            throw new FilterCompilerException("Unexpected character [" . $this->filter[0] . "] begining " . $this->filter . ".");
241
        }
242 14
    }
243
244 14
    private function eatWhite() {
245 14
        if (preg_match("/^\s*/", $this->filter, $matches)) {
246 14
            $this->filter = substr($this->filter, strlen($matches[0]));
247
        }
248 14
    }
249
250 6
    public function rewriteBoundData($data) {
251 6
        $rewritten = [];
252 6
        foreach ($data as $key => $value) {
253 6
            if (is_numeric($key)) {
254 6
                $rewritten["filter_bind_" . ($key + 1)] = $value;
255
            } else {
256 6
                $rewritten[$key] = $value;
257
            }
258
        }
259 6
        return $rewritten;
260
    }
261
262
}
263