1 <?php
2 3 4 5 6 7 8 9 10
11
12 namespace Neevo;
13
14 use DateTime;
15 use Traversable;
16
17
18 19 20 21
22 class Parser {
23
24
25
26 protected $stmt;
27
28
29 protected $clauses = array();
30
31
32 33 34 35
36 public function __construct(BaseStatement $statement){
37 $this->stmt = $statement;
38 }
39
40
41 42 43 44
45 public function parse(){
46 $where = $order = $group = $limit = $q = '';
47 $source = $this->parseSource();
48
49 if($this->stmt->getConditions())
50 $where = $this->parseWhere();
51 if($this->stmt instanceof Result && $this->stmt->getGrouping())
52 $group = $this->parseGrouping();
53 if($this->stmt->getSorting())
54 $order = $this->parseSorting();
55
56 $this->clauses = array($source, $where, $group, $order);
57
58 if($this->stmt->getType() === Manager::STMT_SELECT)
59 $q = $this->parseSelectStmt();
60 elseif($this->stmt->getType() === Manager::STMT_INSERT)
61 $q = $this->parseInsertStmt();
62 elseif($this->stmt->getType() === Manager::STMT_UPDATE)
63 $q = $this->parseUpdateStmt();
64 elseif($this->stmt->getType() === Manager::STMT_DELETE)
65 $q = $this->parseDeleteStmt();
66
67 return trim($q);
68 }
69
70
71 72 73 74
75 protected function parseSelectStmt(){
76 $cols = $this->stmt->getColumns();
77 list($source, $where, $group, $order) = $this->clauses;
78 foreach($cols as $key => $col){
79 $col = preg_match('~^[\w.]+$~', $col) ? ":$col" : $col;
80 $cols[$key] = $this->tryDelimite($col);
81 }
82 $cols = implode(', ', $cols);
83
84 return $this->applyLimit("SELECT $cols\nFROM " . $source . $where . $group . $order);
85 }
86
87
88 89 90 91
92 protected function parseInsertStmt(){
93 $cols = array();
94 foreach($this->escapeValue($this->stmt->getValues()) as $col => $value){
95 $cols[] = $this->parseFieldName($col);
96 $values[] = $value;
97 }
98 $data = ' (' . implode(', ', $cols) . ")\nVALUES (" . implode(', ', $values). ')';
99
100 return 'INSERT INTO ' . $this->clauses[0] . $data;
101 }
102
103
104 105 106 107
108 protected function parseUpdateStmt(){
109 $values = array();
110 list($table, $where) = $this->clauses;
111 foreach($this->escapeValue($this->stmt->getValues()) as $col => $value){
112 $values[] = $this->parseFieldName($col) . ' = ' . $value;
113 }
114 $data = "\nSET " . implode(', ', $values);
115 return 'UPDATE ' . $table . $data . $where;
116 }
117
118
119 120 121 122
123 protected function parseDeleteStmt(){
124 list($table, $where) = $this->clauses;
125 return 'DELETE FROM ' . $table . $where;
126 }
127
128
129 130 131 132
133 protected function parseSource(){
134 if(!$this->stmt instanceof Result)
135 return $this->escapeValue($this->stmt->getTable(), Manager::IDENTIFIER);
136
137
138 if($this->stmt->getTable() !== null){
139 $source = $this->escapeValue($this->stmt->getTable(), Manager::IDENTIFIER);
140 }
141
142 else{
143 $subq = $this->stmt->getSource();
144 $alias = $this->escapeValue($subq->getAlias()
145 ? $subq->getAlias() : '_table_', Manager::IDENTIFIER);
146 $source = "(\n\t" . implode("\n\t", explode("\n", $subq)) . "\n) $alias";
147 }
148 $source = $this->tryDelimite($source);
149
150
151 foreach($this->stmt->getJoins() as $key => $join){
152 list($join_source, $cond, $type) = $join;
153
154
155 if($join_source instanceof Result){
156 $join_alias = $this->escapeValue($join_source->getAlias()
157 ? $join_source->getAlias() : '_join_' . ($key+1), Manager::IDENTIFIER);
158
159 $join_source = "(\n\t" . implode("\n\t", explode("\n", $join_source)) . "\n) $join_alias";
160 }
161
162 elseif($join_source instanceof Literal){
163 $join_source = $join_source->value;
164 }
165
166 elseif(is_scalar($join_source)){
167 $join_source = $this->parseFieldName($join_source, true);
168 }
169 $type = strtoupper(substr($type, 5));
170 $type .= ($type === '') ? '' : ' ';
171 $source .= $cond instanceof Literal
172 ? "\n{$type}JOIN $join_source ON $cond->value"
173 : $this->tryDelimite("\n{$type}JOIN $join_source ON $cond");
174 }
175 return $source;
176 }
177
178
179 180 181 182
183 protected function parseWhere(){
184 $conds = $this->stmt->getConditions();
185
186
187 unset($conds[count($conds)-1]['glue']);
188
189 $conditions = array();
190 foreach($conds as $cond){
191
192
193 if($cond['simple'] === false){
194 $values = $this->escapeValue($cond['values'], $cond['types']);
195 $s = '(' . $this->applyModifiers($cond['expr'], $cond['modifiers'], $values) . ')';
196 if(isset($cond['glue']))
197 $s .= ' ' . $cond['glue'];
198
199 $conditions[] = $s;
200 continue;
201 }
202
203
204 $field = $this->parseFieldName($cond['field']);
205 $operator = '';
206 $value = $cond['value'];
207 if($value === null){
208 $value = ' IS NULL';
209 } elseif($value === true){
210 $value = '';
211 } elseif($value === false){
212 $value = $field;
213 $field = 'NOT ';
214 } elseif($value instanceof Result){
215 $operator = ' IN ';
216 $value = $this->escapeValue($value, Manager::SUBQUERY);
217 } elseif(is_array($value) || $value instanceof Traversable){
218 $value = ' IN ' . $this->escapeValue($value, Manager::ARR);
219 } elseif($value instanceof Literal){
220 $operator = ' = ';
221 $value = $this->escapeValue($value, Manager::LITERAL);
222 } elseif($value instanceof DateTime){
223 $operator = ' = ';
224 $value = $this->escapeValue($value, Manager::DATETIME);
225 } else{
226 $operator = ' = ';
227 $value = $this->escapeValue($value);
228 }
229 $s = '(' . $field . $operator . $value . ')';
230 if(isset($cond['glue']))
231 $s .= ' '.$cond['glue'];
232
233 $conditions[] = $s;
234
235 }
236
237 return "\nWHERE " . implode(' ', $conditions);
238 }
239
240
241 242 243 244
245 protected function parseSorting(){
246 $order = array();
247 foreach($this->stmt->getSorting() as $rule){
248 list($field, $type) = $rule;
249 $order[] = $this->tryDelimite($field) . ($type !== null ? ' ' . $type : '');
250 }
251 return "\nORDER BY " . implode(', ', $order);
252 }
253
254
255 256 257 258
259 protected function parseGrouping(){
260 list($group, $having) = $this->stmt->getGrouping();
261 return $this->tryDelimite("\nGROUP BY $group" . ($having !== null ? " HAVING $having" : ''));
262 }
263
264
265 266 267 268 269 270
271 protected function parseFieldName($field, $table = false){
272
273 if(is_array($field))
274 $field = $field[0];
275 if($field instanceof Literal)
276 return $field->value;
277
278 $field = trim($field);
279
280 if($field === '*')
281 return $field;
282
283 if(strpos($field, ' '))
284 return $field;
285
286 $field = str_replace(':', '', $field);
287
288 if(strpos($field, '.') !== false || $table === true){
289 $prefix = $this->stmt->getConnection()->getPrefix();
290 $field = $prefix . $field;
291 }
292
293 return $this->stmt->getConnection()->getDriver()->escape($field, Manager::IDENTIFIER);
294 }
295
296
297 298 299 300 301
302 protected function applyLimit($sql){
303 list($limit, $offset) = $this->stmt->getLimit();
304
305 if((int) $limit > 0){
306 $sql .= "\nLIMIT " . (int) $limit;
307 if((int) $offset > 0)
308 $sql .= ' OFFSET ' . (int) $offset;
309 }
310 return $sql;
311 }
312
313
314 315 316 317 318 319
320 protected function escapeValue($value, $type = null){
321 if(!$type){
322
323 if($value === null)
324 return 'NULL';
325
326
327 elseif(is_array($value)){
328 foreach($value as $k => $v)
329 $value[$k] = $this->escapeValue($v);
330 return $value;
331 }
332
333
334 else{
335 if($value instanceof DateTime){
336 return $this->escapeValue($value, Manager::DATETIME);
337 } elseif($value instanceof Literal){
338 return $value->value;
339 } else{
340 return is_numeric($value)
341 ? +$value
342 : $this->stmt->getConnection()->getDriver()->escape($value, Manager::TEXT);
343 }
344 }
345 }
346
347
348 elseif(is_array($type)){
349 foreach($value as $k => $v)
350 $value[$k] = $this->escapeValue($v, $type[$k]);
351 return $value;
352 }
353
354
355 elseif($type !== null){
356 if($type === Manager::INT){
357 return (int) $value;
358 } elseif($type === Manager::FLOAT){
359 return (float) $value;
360 } elseif($type === Manager::SUBQUERY && $value instanceof Result){
361 return "(\n\t" . implode("\n\t", explode("\n", $value)) . "\n)";
362 } elseif($type === Manager::ARR){
363 $arr = $value instanceof Traversable ? iterator_to_array($value) : (array) $value;
364 return '(' . implode(', ', $this->escapeValue($arr)) . ')';
365 } elseif($type === Manager::LITERAL){
366 return $value instanceof Literal ? $value->value : $value;
367 } else{
368 return $this->stmt->getConnection()->getDriver()->escape($value, $type);
369 }
370 }
371 }
372
373
374 375 376 377 378 379 380
381 protected function applyModifiers($expr, array $modifiers, array $values){
382 foreach($modifiers as &$mod){
383 $mod = preg_quote("/$mod/");
384 }
385 $expr = $this->tryDelimite($expr);
386 return preg_replace($modifiers, $values, $expr, 1);
387 }
388
389
390 391 392 393 394
395 protected function tryDelimite($expr){
396 if($expr instanceof Literal)
397 return $expr->value;
398 return preg_replace_callback('~:([a-z_\*][a-z0-9._\*]*)~i', array($this, 'parseFieldName'), $expr);
399 }
400
401
402 }
403