1: <?php
2: 3: 4: 5: 6: 7: 8:
9:
10: 11: 12: 13:
14: class Horde_Rdo_Query
15: {
16: 17: 18:
19: public $mapper;
20:
21: 22: 23:
24: public $conjunction = 'AND';
25:
26: 27: 28:
29: public $fields = array('*');
30:
31: 32: 33:
34: public $distinct = false;
35:
36: 37: 38:
39: public $tests = array();
40:
41: 42: 43:
44: public $relationships = array();
45:
46: 47: 48:
49: public $limit;
50:
51: 52: 53:
54: public $limitOffset = null;
55:
56: 57: 58:
59: protected $_sortby = array();
60:
61: 62: 63:
64: protected $_aliasCount = 0;
65:
66: 67: 68:
69: protected $_aliases = array();
70:
71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81:
82: public static function create($query, $mapper = null)
83: {
84: if ($query instanceof Horde_Rdo_Query ||
85: $query instanceof Horde_Rdo_Query_Literal) {
86: $query = clone $query;
87: if (!is_null($mapper)) {
88: $query->setMapper($mapper);
89: }
90: return $query;
91: }
92:
93: $q = new Horde_Rdo_Query($mapper);
94:
95: if (is_scalar($query)) {
96: $q->addTest($mapper->tableDefinition->getPrimaryKey(), '=', $query);
97: } elseif ($query) {
98: $q->combineWith('AND');
99: foreach ($query as $key => $value) {
100: $q->addTest($key, '=', $value);
101: }
102: }
103:
104: return $q;
105: }
106:
107: 108: 109:
110: public function __construct($mapper = null)
111: {
112: $this->setMapper($mapper);
113: }
114:
115: 116: 117: 118: 119:
120: public function setMapper($mapper)
121: {
122: if ($mapper === $this->mapper) {
123: return $this;
124: }
125:
126: $this->mapper = $mapper;
127:
128:
129: $this->setFields($mapper->fields, $mapper->table . '.');
130:
131:
132: foreach ($mapper->relationships as $relationship => $rel) {
133: if (isset($rel['mapper'])) {
134:
135:
136:
137: $m = new $rel['mapper']($this->mapper->adapter);
138: } else {
139: $m = $this->mapper->tableToMapper($relationship);
140: if (is_null($m)) {
141: throw new Horde_Rdo_Exception('Unable to find a Mapper class for eager-loading relationship ' . $relationship);
142: }
143: }
144:
145:
146: $m->tableAlias = $this->_alias($m->table);
147: $this->addFields($m->fields, $m->tableAlias . '.@');
148:
149: switch ($rel['type']) {
150: case Horde_Rdo::ONE_TO_ONE:
151: case Horde_Rdo::MANY_TO_ONE:
152: if (isset($rel['query'])) {
153: $query = $this->_fillJoinPlaceholders($m, $mapper, $rel['query']);
154: } else {
155: $query = array($mapper->table . '.' . $rel['foreignKey'] => new Horde_Rdo_Query_Literal($m->table . '.' . $m->tableDefinition->getPrimaryKey()));
156: }
157: $this->addRelationship($relationship, array('mapper' => $m,
158: 'type' => $rel['type'],
159: 'query' => $query));
160: break;
161:
162: case Horde_Rdo::ONE_TO_MANY:
163: case Horde_Rdo::MANY_TO_MANY:
164:
165: }
166: }
167:
168: return $this;
169: }
170:
171: 172: 173: 174: 175: 176: 177: 178: 179:
180: public function distinct($distinct)
181: {
182: $this->distinct = $distinct;
183: return $this;
184: }
185:
186: 187: 188: 189: 190:
191: public function setFields($fields, $fieldPrefix = null)
192: {
193: if (!is_array($fields)) {
194: $fields = array($fields);
195: }
196: if (!is_null($fieldPrefix)) {
197: array_walk($fields, array($this, '_prefix'), $fieldPrefix);
198: }
199: $this->fields = $fields;
200: return $this;
201: }
202:
203: 204: 205: 206: 207:
208: public function addFields($fields, $fieldPrefix = null)
209: {
210: if (!is_null($fieldPrefix)) {
211: array_walk($fields, array($this, '_prefix'), $fieldPrefix);
212: }
213: $this->fields = array_merge($this->fields, $fields);
214: }
215:
216: 217: 218:
219: public function combineWith($conjunction)
220: {
221: $this->conjunction = $conjunction;
222: return $this;
223: }
224:
225: 226:
227: public function addTest($field, $test, $value)
228: {
229: $this->tests[] = array('field' => $field,
230: 'test' => $test,
231: 'value' => $value);
232: return $this;
233: }
234:
235: 236:
237: public function addRelationship($relationship, $args)
238: {
239: if (!isset($args['mapper'])) {
240: throw new InvalidArgumentException('Relationships must contain a Horde_Rdo_Mapper object.');
241: }
242: if (!isset($args['table'])) {
243: $args['table'] = $args['mapper']->table;
244: }
245: if (!isset($args['tableAlias'])) {
246: if (isset($args['mapper']->tableAlias)) {
247: $args['tableAlias'] = $args['mapper']->tableAlias;
248: } else {
249: $args['tableAlias'] = $this->_alias($args['table']);
250: }
251: }
252: if (!isset($args['type'])) {
253: $args['type'] = Horde_Rdo::MANY_TO_MANY;
254: }
255: if (!isset($args['join_type'])) {
256: switch ($args['type']) {
257: case Horde_Rdo::ONE_TO_ONE:
258: case Horde_Rdo::MANY_TO_ONE:
259: $args['join_type'] = 'INNER JOIN';
260: break;
261:
262: default:
263: $args['join_type'] = 'LEFT JOIN';
264: }
265: }
266:
267: $this->relationships[$relationship] = $args;
268: return $this;
269: }
270:
271: 272: 273: 274: 275:
276: public function sortBy($sort)
277: {
278: $this->_sortby[] = $sort;
279: return $this;
280: }
281:
282: 283:
284: public function clearSort()
285: {
286: $this->_sortby = array();
287: return $this;
288: }
289:
290: 291: 292: 293: 294: 295:
296: public function limit($limit, $offset = null)
297: {
298: $this->limit = $limit;
299: $this->limitOffset = $offset;
300: return $this;
301: }
302:
303: 304: 305: 306: 307:
308: public function __get($key)
309: {
310: switch ($key) {
311: case 'sortby':
312: if (!$this->_sortby && $this->mapper->defaultSort) {
313:
314:
315: $this->sortBy($this->mapper->defaultSort);
316: }
317: return $this->_sortby;
318: }
319:
320: throw new InvalidArgumentException('Undefined property ' . $key);
321: }
322:
323: 324: 325: 326: 327: 328:
329: public function getQuery()
330: {
331: $bindParams = array();
332: $sql = '';
333:
334: $this->_select($sql, $bindParams);
335: $this->_from($sql, $bindParams);
336: $this->_join($sql, $bindParams);
337: $this->_where($sql, $bindParams);
338: $this->_orderBy($sql, $bindParams);
339: $this->_limit($sql, $bindParams);
340:
341: return array($sql, $bindParams);
342: }
343:
344: 345:
346: protected function _select(&$sql, &$bindParams)
347: {
348: $fields = array();
349: foreach ($this->fields as $field) {
350: $parts = explode('.@', $field, 2);
351: if (count($parts) == 1) {
352: $fields[] = $field;
353: } else {
354: list($tableName, $columnName) = $parts;
355: if (isset($this->_aliases[$tableName])) {
356: $tableName = $this->_aliases[$tableName];
357: }
358: $fields[] = str_replace('.@', '.', $field) . ' AS ' . $this->mapper->adapter->quoteColumnName($tableName . '@' . $columnName);
359: }
360: }
361:
362: if ($this->distinct) {
363: $sql = 'SELECT ' . $this->mapper->adapter->distinct(implode(', ', $fields), implode(', ', $this->sortby));
364: } else {
365: $sql = 'SELECT ' . implode(', ', $fields);
366: }
367: }
368:
369: 370:
371: protected function _from(&$sql, &$bindParams)
372: {
373: $sql .= ' FROM ' . $this->mapper->table;
374: }
375:
376: 377:
378: protected function _join(&$sql, &$bindParams)
379: {
380: foreach ($this->relationships as $relationship) {
381: $relsql = array();
382: $table = $relationship['table'];
383: $tableAlias = $relationship['tableAlias'];
384: foreach ($relationship['query'] as $key => $value) {
385: if ($value instanceof Horde_Rdo_Query_Literal) {
386: $relsql[] = $key . ' = ' . str_replace("{$table}.", "{$tableAlias}.", (string)$value);
387: } else {
388: $relsql[] = $key . ' = ?';
389: $bindParams[] = $value;
390: }
391: }
392:
393: $sql .= ' ' . $relationship['join_type'] . ' ' . $relationship['table'] . ' ' . $tableAlias . ' ON ' . implode(' AND ', $relsql);
394: }
395: }
396:
397: 398:
399: protected function _where(&$sql, &$bindParams)
400: {
401: $clauses = array();
402: foreach ($this->tests as $test) {
403: if (strpos($test['field'], '@') !== false) {
404: list($rel, $field) = explode('@', $test['field']);
405: if (!isset($this->relationships[$rel])) {
406: continue;
407: }
408: $clause = $this->relationships[$rel]['tableAlias'] . '.' . $field . ' ' . $test['test'];
409: } else {
410: $clause = $this->mapper->table . '.' . $this->mapper->adapter->quoteColumnName($test['field']) . ' ' . $test['test'];
411: }
412:
413: if ($test['value'] instanceof Horde_Rdo_Query_Literal) {
414: $clauses[] = $clause . ' ' . (string)$test['value'];
415: } else {
416: if (($test['test'] == 'IN' || $test['test'] == 'NOT IN') && is_array($test['value'])) {
417: $clauses[] = $clause . '(?' . str_repeat(',?', count($test['value']) - 1) . ')';
418: $bindParams = array_merge($bindParams, array_values($test['value']));
419: } else {
420: $clauses[] = $clause . ' ?';
421: $bindParams[] = $test['value'];
422: }
423: }
424: }
425:
426: if ($clauses) {
427: $sql .= ' WHERE ' . implode(' ' . $this->conjunction . ' ', $clauses);
428: }
429: }
430:
431: 432:
433: protected function _orderBy(&$sql, &$bindParams)
434: {
435: if ($this->sortby) {
436: $sql .= ' ORDER BY';
437: foreach ($this->sortby as $sort) {
438: if (strpos($sort, '@') !== false) {
439: list($rel, $field) = explode('@', $sort);
440: if (!isset($this->relationships[$rel])) {
441: continue;
442: }
443: $sql .= ' ' . $this->relationships[$rel]['tableAlias'] . '.' . $field . ',';
444: } else {
445: $sql .= " $sort,";
446: }
447: }
448:
449: $sql = substr($sql, 0, -1);
450: }
451: }
452:
453: 454:
455: protected function _limit(&$sql, &$bindParams)
456: {
457: if ($this->limit) {
458: $opts = array('limit' => $this->limit, 'offset' => $this->limitOffset);
459: $sql = $this->mapper->adapter->addLimitOffset($sql, $opts);
460: }
461: }
462:
463: 464: 465: 466:
467: protected function _prefix(&$fieldName, $key, $prefix)
468: {
469: $fieldName = $prefix . $fieldName;
470: }
471:
472: 473: 474:
475: protected function _alias($tableName)
476: {
477: $alias = 't' . ++$this->_aliasCount;
478: $this->_aliases[$alias] = $tableName;
479: return $alias;
480: }
481:
482: 483: 484: 485: 486: 487: 488: 489: 490: 491:
492: protected function _fillJoinPlaceholders($m1, $m2, $query)
493: {
494: $q = array();
495: foreach (array_keys($query) as $field) {
496: $value = $query[$field];
497: if (preg_match('/^@(.*)@$/', $value, $matches)) {
498: $q[$m1->tableAlias . '.' . $field] = new Horde_Rdo_Query_Literal($m2->table . '.' . $matches[1]);
499: } else {
500: $q[$m1->tableAlias . '.' . $field] = $value;
501: }
502: }
503:
504: return $q;
505: }
506:
507: }
508: