1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19:
20: final class DibiTranslator extends DibiObject
21: {
22:
23: private $driver;
24:
25:
26: private $cursor;
27:
28:
29: private $args;
30:
31:
32: private $hasError;
33:
34:
35: private $comment;
36:
37:
38: private $ifLevel;
39:
40:
41: private $ifLevelStart;
42:
43:
44: private $limit;
45:
46:
47: private $offset;
48:
49:
50: private $identifiers;
51:
52:
53:
54: public function __construct(IDibiDriver $driver)
55: {
56: $this->driver = $driver;
57: }
58:
59:
60:
61: 62: 63: 64: 65: 66:
67: public function translate(array $args)
68: {
69: $this->identifiers = new DibiLazyStorage(array($this, 'delimite'));
70: $args = array_values($args);
71: while (count($args) === 1 && is_array($args[0])) { 72: $args = array_values($args[0]);
73: }
74: $this->args = $args;
75:
76: $this->limit = -1;
77: $this->offset = 0;
78: $this->hasError = FALSE;
79: $commandIns = NULL;
80: $lastArr = NULL;
81: 82: $cursor = & $this->cursor;
83: $cursor = 0;
84:
85: 86: $this->ifLevel = $this->ifLevelStart = 0;
87: $comment = & $this->comment;
88: $comment = FALSE;
89:
90: 91: $sql = array();
92: while ($cursor < count($this->args))
93: {
94: $arg = $this->args[$cursor];
95: $cursor++;
96:
97: 98: if (is_string($arg)) {
99: 100: $toSkip = strcspn($arg, '`[\'":%');
101:
102: if (strlen($arg) === $toSkip) { 103: $sql[] = $arg;
104: } else {
105: $sql[] = substr($arg, 0, $toSkip)
106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: . preg_replace_callback('/(?=[`[\'":%?])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?)|%([a-zA-Z~][a-zA-Z0-9~]{0,5})|(\?))/s',
121: array($this, 'cb'),
122: substr($arg, $toSkip)
123: );
124: if (preg_last_error()) throw new DibiPcreException;
125: }
126: continue;
127: }
128:
129: if ($comment) {
130: $sql[] = '...';
131: continue;
132: }
133:
134: if ($arg instanceof Traversable) {
135: $arg = iterator_to_array($arg);
136: }
137:
138: if (is_array($arg)) {
139: if (is_string(key($arg))) {
140: 141: if ($commandIns === NULL) {
142: $commandIns = strtoupper(substr(ltrim($this->args[0]), 0, 6));
143: $commandIns = $commandIns === 'INSERT' || $commandIns === 'REPLAC';
144: $sql[] = $this->formatValue($arg, $commandIns ? 'v' : 'a');
145: } else {
146: if ($lastArr === $cursor - 1) $sql[] = ',';
147: $sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
148: }
149: $lastArr = $cursor;
150: continue;
151: }
152: }
153:
154: 155: $sql[] = $this->formatValue($arg, FALSE);
156: } 157:
158:
159: if ($comment) $sql[] = "*/";
160:
161: $sql = implode(' ', $sql);
162:
163: if ($this->hasError) {
164: throw new DibiException('SQL translate error', 0, $sql);
165: }
166:
167: 168: if ($this->limit > -1 || $this->offset > 0) {
169: $this->driver->applyLimit($sql, $this->limit, $this->offset);
170: }
171:
172: return $sql;
173: }
174:
175:
176:
177: 178: 179: 180: 181: 182:
183: public function formatValue($value, $modifier)
184: {
185: 186: if ($value instanceof Traversable) {
187: $value = iterator_to_array($value);
188: }
189:
190: if (is_array($value)) {
191: $vx = $kx = array();
192: switch ($modifier) {
193: case 'and':
194: case 'or': 195: if (empty($value)) {
196: return '1=1';
197: }
198:
199: foreach ($value as $k => $v) {
200: if (is_string($k)) {
201: $pair = explode('%', $k, 2); 202: $k = $this->identifiers->{$pair[0]} . ' ';
203: if (!isset($pair[1])) {
204: $v = $this->formatValue($v, FALSE);
205: $vx[] = $k . ($v === 'NULL' ? 'IS ' : '= ') . $v;
206:
207: } elseif ($pair[1] === 'ex') { 208: $vx[] = $k . $this->formatValue($v, 'ex');
209:
210: } else {
211: $v = $this->formatValue($v, $pair[1]);
212: $vx[] = $k . ($pair[1] === 'l' || $pair[1] === 'in' ? 'IN ' : ($v === 'NULL' ? 'IS ' : '= ')) . $v;
213: }
214:
215: } else {
216: $vx[] = $this->formatValue($v, 'ex');
217: }
218: }
219: return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
220:
221: case 'n': 222: foreach ($value as $k => $v) {
223: if (is_string($k)) {
224: $vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $v);
225: } else {
226: $pair = explode('%', $v, 2); 227: $vx[] = $this->identifiers->{$pair[0]};
228: }
229: }
230: return implode(', ', $vx);
231:
232:
233: case 'a': 234: foreach ($value as $k => $v) {
235: $pair = explode('%', $k, 2); 236: $vx[] = $this->identifiers->{$pair[0]} . '='
237: . $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
238: }
239: return implode(', ', $vx);
240:
241:
242: case 'in':243: case 'l': 244: foreach ($value as $k => $v) {
245: $pair = explode('%', $k, 2); 246: $vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
247: }
248: return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
249:
250:
251: case 'v': 252: foreach ($value as $k => $v) {
253: $pair = explode('%', $k, 2); 254: $kx[] = $this->identifiers->{$pair[0]};
255: $vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
256: }
257: return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
258:
259: case 'm': 260: foreach ($value as $k => $v) {
261: if (is_array($v)) {
262: if (isset($proto)) {
263: if ($proto !== array_keys($v)) {
264: $this->hasError = TRUE;
265: return '**Multi-insert array "' . $k . '" is different.**';
266: }
267: } else {
268: $proto = array_keys($v);
269: }
270: } else {
271: $this->hasError = TRUE;
272: return '**Unexpected type ' . gettype($v) . '**';
273: }
274:
275: $pair = explode('%', $k, 2); 276: $kx[] = $this->identifiers->{$pair[0]};
277: foreach ($v as $k2 => $v2) {
278: $vx[$k2][] = $this->formatValue($v2, isset($pair[1]) ? $pair[1] : (is_array($v2) ? 'ex' : FALSE));
279: }
280: }
281: foreach ($vx as $k => $v) {
282: $vx[$k] = '(' . implode(', ', $v) . ')';
283: }
284: return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
285:
286: case 'by': 287: foreach ($value as $k => $v) {
288: if (is_array($v)) {
289: $vx[] = $this->formatValue($v, 'ex');
290: } elseif (is_string($k)) {
291: $v = (is_string($v) && strncasecmp($v, 'd', 1)) || $v > 0 ? 'ASC' : 'DESC';
292: $vx[] = $this->identifiers->$k . ' ' . $v;
293: } else {
294: $vx[] = $this->identifiers->$v;
295: }
296: }
297: return implode(', ', $vx);
298:
299: case 'ex':
300: case 'sql':
301: $translator = new self($this->driver);
302: return $translator->translate($value);
303:
304: default: 305: foreach ($value as $v) {
306: $vx[] = $this->formatValue($v, $modifier);
307: }
308: return implode(', ', $vx);
309: }
310: }
311:
312:
313: 314: if ($modifier) {
315: if ($value !== NULL && !is_scalar($value) && !($value instanceof DateTime)) { 316: $this->hasError = TRUE;
317: return '**Unexpected type ' . gettype($value) . '**';
318: }
319:
320: switch ($modifier) {
321: case 's': 322: case 'bin':323: case 'b': 324: return $value === NULL ? 'NULL' : $this->driver->escape($value, $modifier);
325:
326: case 'sN': 327: case 'sn':
328: return $value == '' ? 'NULL' : $this->driver->escape($value, dibi::TEXT); 329:
330: case 'iN': 331: case 'in': 332: if ($value == '') $value = NULL;
333: 334:
335: case 'i': 336: case 'u': 337: 338: if (is_string($value) && preg_match('#[+-]?\d++(e\d+)?$#A', $value)) {
339: return $value;
340: } else {
341: return $value === NULL ? 'NULL' : (string) (int) ($value + 0);
342: }
343:
344: case 'f': 345: 346: if (is_string($value) && is_numeric($value) && strpos($value, 'x') === FALSE) {
347: return $value; 348: } else {
349: return $value === NULL ? 'NULL' : rtrim(rtrim(number_format($value + 0, 5, '.', ''), '0'), '.');
350: }
351:
352: case 'd': 353: case 't': 354: if ($value === NULL) {
355: return 'NULL';
356: } else {
357: if (is_numeric($value)) {
358: $value = (int) $value; 359:
360: } elseif (is_string($value)) {
361: $value = new DateTime($value);
362: }
363: return $this->driver->escape($value, $modifier);
364: }
365:
366: case 'by':
367: case 'n': 368: return $this->identifiers->$value;
369:
370: case 'ex':
371: case 'sql': 372: $value = (string) $value;
373: 374: $toSkip = strcspn($value, '`[\'":');
375: if (strlen($value) !== $toSkip) {
376: $value = substr($value, 0, $toSkip)
377: . preg_replace_callback(
378: '/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
379: array($this, 'cb'),
380: substr($value, $toSkip)
381: );
382: if (preg_last_error()) throw new PcreException;
383: }
384: return $value;
385:
386: case 'SQL': 387: return (string) $value;
388:
389: case 'like~': 390: return $this->driver->escapeLike($value, 1);
391:
392: case '~like': 393: return $this->driver->escapeLike($value, -1);
394:
395: case '~like~': 396: return $this->driver->escapeLike($value, 0);
397:
398: case 'and':
399: case 'or':
400: case 'a':
401: case 'l':
402: case 'v':
403: $this->hasError = TRUE;
404: return '**Unexpected type ' . gettype($value) . '**';
405:
406: default:
407: $this->hasError = TRUE;
408: return "**Unknown or invalid modifier %$modifier**";
409: }
410: }
411:
412:
413: 414: if (is_string($value)) {
415: return $this->driver->escape($value, dibi::TEXT);
416:
417: } elseif (is_int($value)) {
418: return (string) $value;
419:
420: } elseif (is_float($value)) {
421: return rtrim(rtrim(number_format($value, 5, '.', ''), '0'), '.');
422:
423: } elseif (is_bool($value)) {
424: return $this->driver->escape($value, dibi::BOOL);
425:
426: } elseif ($value === NULL) {
427: return 'NULL';
428:
429: } elseif ($value instanceof DateTime) {
430: return $this->driver->escape($value, dibi::DATETIME);
431:
432: } else {
433: $this->hasError = TRUE;
434: return '**Unexpected ' . gettype($value) . '**';
435: }
436: }
437:
438:
439:
440: 441: 442: 443: 444:
445: private function cb($matches)
446: {
447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458:
459:
460: if (!empty($matches[11])) { 461: $cursor = & $this->cursor;
462:
463: if ($cursor >= count($this->args)) {
464: $this->hasError = TRUE;
465: return "**Extra placeholder**";
466: }
467:
468: $cursor++;
469: return $this->formatValue($this->args[$cursor - 1], FALSE);
470: }
471:
472: if (!empty($matches[10])) { 473: $mod = $matches[10];
474: $cursor = & $this->cursor;
475:
476: if ($cursor >= count($this->args) && $mod !== 'else' && $mod !== 'end') {
477: $this->hasError = TRUE;
478: return "**Extra modifier %$mod**";
479: }
480:
481: if ($mod === 'if') {
482: $this->ifLevel++;
483: $cursor++;
484: if (!$this->comment && !$this->args[$cursor - 1]) {
485: 486: $this->ifLevelStart = $this->ifLevel;
487: $this->comment = TRUE;
488: return "/*";
489: }
490: return '';
491:
492: } elseif ($mod === 'else') {
493: if ($this->ifLevelStart === $this->ifLevel) {
494: $this->ifLevelStart = 0;
495: $this->comment = FALSE;
496: return "*/";
497: } elseif (!$this->comment) {
498: $this->ifLevelStart = $this->ifLevel;
499: $this->comment = TRUE;
500: return "/*";
501: }
502:
503: } elseif ($mod === 'end') {
504: $this->ifLevel--;
505: if ($this->ifLevelStart === $this->ifLevel + 1) {
506: 507: $this->ifLevelStart = 0;
508: $this->comment = FALSE;
509: return "*/";
510: }
511: return '';
512:
513: } elseif ($mod === 'ex') { 514: array_splice($this->args, $cursor, 1, $this->args[$cursor]);
515: return '';
516:
517: } elseif ($mod === 'lmt') { 518: if ($this->args[$cursor] !== NULL) $this->limit = (int) $this->args[$cursor];
519: $cursor++;
520: return '';
521:
522: } elseif ($mod === 'ofs') { 523: if ($this->args[$cursor] !== NULL) $this->offset = (int) $this->args[$cursor];
524: $cursor++;
525: return '';
526:
527: } else { 528: $cursor++;
529: return $this->formatValue($this->args[$cursor - 1], $mod);
530: }
531: }
532:
533: if ($this->comment) return '...';
534:
535: if ($matches[1]) 536: return $this->identifiers->{$matches[1]};
537:
538: if ($matches[2]) 539: return $this->identifiers->{$matches[2]};
540:
541: if ($matches[3]) 542: return $this->driver->escape( str_replace("''", "'", $matches[4]), dibi::TEXT);
543:
544: if ($matches[5]) 545: return $this->driver->escape( str_replace('""', '"', $matches[6]), dibi::TEXT);
546:
547: if ($matches[7]) { 548: $this->hasError = TRUE;
549: return '**Alone quote**';
550: }
551:
552: if ($matches[8]) { 553: $m = substr($matches[8], 0, -1);
554: return $matches[9] == '' ? $this->formatValue(dibi::$substs->$m, FALSE) : dibi::$substs->$m . $matches[9]; 555: }
556:
557: die('this should be never executed');
558: }
559:
560:
561:
562: 563: 564: 565: 566: 567:
568: public function delimite($value)
569: {
570: $value = self::substitute($value);
571: $parts = explode('.', $value);
572: foreach ($parts as & $v) {
573: if ($v !== '*') $v = $this->driver->escape($v, dibi::IDENTIFIER);
574: }
575: return implode('.', $parts);
576: }
577:
578:
579:
580: 581: 582: 583:
584: public static function substitute($value)
585: {
586: if (strpos($value, ':') !== FALSE) { 587: return preg_replace_callback('#:([^:\s]*):#', array(__CLASS__, 'subCb'), $value);
588: }
589: return $value;
590: }
591:
592:
593:
594: 595: 596: 597: 598:
599: private static function subCb($m)
600: {
601: return dibi::$substs->{$m[1]};
602: }
603:
604: }
605: