Dotclear

source: inc/core/class.dc.selectstatement.php @ 3743:07ef48e3d0e5

Revision 3743:07ef48e3d0e5, 14.1 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Work in progress, not yet finished

Line 
1<?php
2/**
3 * @brief Select query statement builder
4 *
5 * dcSelectStatement is a class used to build select queries
6 *
7 * @package Dotclear
8 * @subpackage Core
9 *
10 * @copyright Bruno Hondelatte & Association Dotclear
11 * @copyright GPL-2.0-only
12 */
13
14/**
15 * Select Statement : small utility to build select queries
16 */
17class dcSelectStatement
18{
19    protected $core;
20    protected $con;
21
22    protected $columns;
23    protected $from;
24    protected $join;
25    protected $where;
26    protected $cond;
27    protected $sql;
28    protected $having;
29    protected $order;
30    protected $group;
31    protected $limit;
32    protected $offset;
33    protected $distinct;
34
35    /**
36     * Class constructor
37     *
38     * @param dcCore    $core   dcCore instance
39     * @param mixed     $from   optional from clause(s)
40     */
41    public function __construct(&$core, $from = null)
42    {
43        $this->core = &$core;
44        $this->con  = &$core->con;
45
46        $this->columns =
47        $this->from    =
48        $this->join    =
49        $this->where   =
50        $this->cond    =
51        $this->sql     =
52        $this->having  =
53        $this->order   =
54        $this->group   =
55        array();
56        $this->limit    = null;
57        $this->offset   = null;
58        $this->distinct = false;
59
60        if ($from !== null) {
61            if (is_array($from)) {
62                $this->froms($from);
63            } else {
64                $this->from($from);
65            }
66        }
67    }
68
69    /**
70     * Magic getter method
71     *
72     * @param      string  $property  The property
73     *
74     * @return     mixed   property value if property exists
75     */
76    public function __get($property)
77    {
78        if (property_exists($this, $property)) {
79            return $this->$property;
80        }
81        trigger_error('Unknown property ' . $property, E_USER_ERROR);
82        return;
83    }
84
85    /**
86     * Magic setter method
87     *
88     * @param      string  $property  The property
89     * @param      mixed   $value     The value
90     *
91     * @return     self
92     */
93    public function __set($property, $value)
94    {
95        if (property_exists($this, $property)) {
96            $this->$property = $value;
97        } else {
98            trigger_error('Unknown property ' . $property, E_USER_ERROR);
99        }
100        return $this;
101    }
102
103    /**
104     * Adds column(s)
105     *
106     * @param mixed     $c      the column(s)
107     * @param boolean   $reset  reset previous column(s) first
108     *
109     * @return dcSelectStatement self instance, enabling to chain calls
110     */
111    public function columns($c, $reset = false)
112    {
113        if ($reset) {
114            $this->columns = array();
115        }
116        if (is_array($c)) {
117            $this->columns = array_merge($this->columns, $c);
118        } else {
119            array_push($this->columns, $c);
120        }
121        return $this;
122    }
123
124    /**
125     * columns() alias
126     *
127     * @param      mixed    $c      the column(s)
128     * @param      boolean  $reset  reset previous column(s) first
129     *
130     * @return dcSelectStatement self instance, enabling to chain calls
131     */
132    public function column($c, $reset = false)
133    {
134        return $this->columns($c, $reset);
135    }
136
137    /**
138     * Adds FROM clause(s)
139     *
140     * @param mixed     $c      the from clause(s)
141     * @param boolean   $reset  reset previous from(s) first
142     *
143     * @return dcSelectStatement self instance, enabling to chain calls
144     */
145    public function from($c, $reset = false)
146    {
147        if ($reset) {
148            $this->from = array();
149        }
150        if (is_array($c)) {
151            $filter = function($v) {
152                return trim(ltrim($v, ','));
153            };
154            $c          = array_map($filter, $c); // Cope with legacy code
155            $this->from = array_merge($this->from, $c);
156        } else {
157            $c = trim(ltrim($c, ',')); // Cope with legacy code
158            array_push($this->from, $c);
159        }
160        return $this;
161    }
162
163    /**
164     * Adds JOIN clause(s) (applied on first from item only)
165     *
166     * @param mixed     $c      the join clause(s)
167     * @param boolean   $reset  reset previous join(s) first
168     *
169     * @return dcSelectStatement self instance, enabling to chain calls
170     */
171    public function join($c, $reset = false)
172    {
173        if ($reset) {
174            $this->join = array();
175        }
176        if (is_array($c)) {
177            $this->join = array_merge($this->join, $c);
178        } else {
179            array_push($this->join, $c);
180        }
181        return $this;
182    }
183
184    /**
185     * Adds WHERE clause(s) condition (each will be AND combined in statement)
186     *
187     * @param mixed     $c      the clause(s)
188     * @param boolean   $reset  reset previous where(s) first
189     *
190     * @return dcSelectStatement self instance, enabling to chain calls
191     */
192    public function where($c, $reset = false)
193    {
194        if ($reset) {
195            $this->where = array();
196        }
197        if (is_array($c)) {
198            $this->where = array_merge($this->where, $c);
199        } else {
200            array_push($this->where, $c);
201        }
202        return $this;
203    }
204
205    /**
206     * Adds additional WHERE clause condition(s) (including an operator at beginning)
207     *
208     * @param mixed     $c      the clause(s)
209     * @param boolean   $reset  reset previous condition(s) first
210     *
211     * @return dcSelectStatement self instance, enabling to chain calls
212     */
213    public function cond($c, $reset = false)
214    {
215        if ($reset) {
216            $this->cond = array();
217        }
218        if (is_array($c)) {
219            $this->cond = array_merge($this->cond, $c);
220        } else {
221            array_push($this->cond, $c);
222        }
223        return $this;
224    }
225
226    /**
227     * Adds generic clause(s)
228     *
229     * @param mixed     $c      the clause(s)
230     * @param boolean   $reset  reset previous generic clause(s) first
231     *
232     * @return dcSelectStatement self instance, enabling to chain calls
233     */
234    public function sql($c, $reset = false)
235    {
236        if ($reset) {
237            $this->sql = array();
238        }
239        if (is_array($c)) {
240            $this->sql = array_merge($this->sql, $c);
241        } else {
242            array_push($this->sql, $c);
243        }
244        return $this;
245    }
246
247    /**
248     * Adds HAVING clause(s)
249     *
250     * @param mixed     $c      the clause(s)
251     * @param boolean   $reset  reset previous having(s) first
252     *
253     * @return dcSelectStatement self instance, enabling to chain calls
254     */
255    public function having($c, $reset = false)
256    {
257        if ($reset) {
258            $this->having = array();
259        }
260        if (is_array($c)) {
261            $this->having = array_merge($this->having, $c);
262        } else {
263            array_push($this->having, $c);
264        }
265        return $this;
266    }
267
268    /**
269     * Adds ORDER BY clause(s)
270     *
271     * @param mixed     $c      the clause(s)
272     * @param boolean   $reset  reset previous order(s) first
273     *
274     * @return dcSelectStatement self instance, enabling to chain calls
275     */
276    public function order($c, $reset = false)
277    {
278        if ($reset) {
279            $this->order = array();
280        }
281        if (is_array($c)) {
282            $this->order = array_merge($this->order, $c);
283        } else {
284            array_push($this->order, $c);
285        }
286        return $this;
287    }
288
289    /**
290     * Adds GROUP BY clause(s)
291     *
292     * @param mixed     $c      the clause(s)
293     * @param boolean   $reset  reset previous group(s) first
294     *
295     * @return dcSelectStatement self instance, enabling to chain calls
296     */
297    public function group($c, $reset = false)
298    {
299        if ($reset) {
300            $this->group = array();
301        }
302        if (is_array($c)) {
303            $this->group = array_merge($this->group, $c);
304        } else {
305            array_push($this->group, $c);
306        }
307        return $this;
308    }
309
310    /**
311     * Defines the LIMIT for select
312     *
313     * @param mixed $limit
314     * @return dcSelectStatement self instance, enabling to chain calls
315     */
316    public function limit($limit)
317    {
318        $offset = null;
319        if (is_array($limit)) {
320            // Keep only values
321            $limit = array_values($limit);
322            // If 2 values, [0] -> offset, [1] -> limit
323            // If 1 value, [0] -> limit
324            if (isset($limit[1])) {
325                $offset = $limit[0];
326                $limit  = $limit[1];
327            } else {
328                $limit = limit[0];
329            }
330        }
331        $this->limit = $limit;
332        if ($offset !== null) {
333            $this->offset = $offset;
334        }
335        return $this;
336    }
337
338    /**
339     * Defines the OFFSET for select
340     *
341     * @param integer $offset
342     * @return dcSelectStatement self instance, enabling to chain calls
343     */
344    public function offset($offset)
345    {
346        $this->offset = $offset;
347        return $this;
348    }
349
350    /**
351     * Defines the DISTINCT flag for select
352     *
353     * @param boolean $distinct
354     * @return dcSelectStatement self instance, enabling to chain calls
355     */
356    public function distinct($distinct = true)
357    {
358        $this->distinct = $distinct;
359        return $this;
360    }
361
362    // Helpers
363
364    /**
365     * Escape a value
366     *
367     * @param      string  $value  The value
368     *
369     * @return     string
370     */
371    public function escape($value)
372    {
373        return $this->con->escape($value);
374    }
375
376    /**
377     * Return an SQL IN (…) fragment
378     *
379     * @param      mixed  $list   The list
380     *
381     * @return     string
382     */
383    public function in($list)
384    {
385        return $this->con->in($list);
386    }
387
388    /**
389     * Return an SQL formatted date
390     *
391     * @param   string    $field     Field name
392     * @param   string    $pattern   Date format
393     *
394     * @return     string
395     */
396    public function dateFormat($field, $pattern)
397    {
398        return $this->con->dateFormat($field, $pattern);
399    }
400
401    /**
402     * Return an SQL formatted REGEXP clause
403     *
404     * @param      string  $value  The value
405     *
406     * @return     string
407     */
408    public function regexp($value)
409    {
410        if ($this->con->driver() == 'mysql' || $this->con->driver() == 'mysqli' || $this->con->driver() == 'mysqlimb4') {
411            $clause = "REGEXP '^" . $this->escape(preg_quote($value)) . "[0-9]+$'";
412        } elseif ($this->con->driver() == 'pgsql') {
413            $clause = "~ '^" . $this->escape(preg_quote($value)) . "[0-9]+$'";
414        } else {
415            $clause = "LIKE '" .
416                $sql->escape(preg_replace(array('%', '_', '!'), array('!%', '!_', '!!'), $value)) .
417                "%' ESCAPE '!'";
418        }
419        return $clause;
420    }
421
422    /**
423     * Quote and escape a value if necessary (type string)
424     *
425     * @param      mixed    $value   The value
426     * @param      boolean  $escape  The escape
427     *
428     * @return     string
429     */
430    public function quote($value, $escape = true)
431    {
432        return
433            (is_string($value) ? "'" : '') .
434            ($escape ? $this->con->escape($value) : $value) .
435            (is_string($value) ? "'" : '');
436    }
437
438    /**
439     * Returns the select statement
440     *
441     * @return string the statement
442     */
443    public function statement()
444    {
445        // Check if source given
446        if (!count($this->from)) {
447            trigger_error(__('SQL SELECT requires a FROM source'), E_USER_ERROR);
448            return '';
449        }
450
451        // Query
452        $query = 'SELECT ' . ($this->distinct ? 'DISTINCT ' : '');
453
454        // Specific column(s) or all (*)
455        if (count($this->columns)) {
456            $query .= join(', ', $this->columns) . ' ';
457        } else {
458            $query .= '* ';
459        }
460
461        // Table(s) and Join(s)
462        $query .= 'FROM ' . $this->from[0] . ' ';
463        $query .= join(' ', $this->join) . ' ';
464        if (count($this->from) > 1) {
465            array_shift($this->from);
466            $query .= ', ' . join(', ', $this->from) . ' '; // All other from(s)
467        }
468
469        // Where clause(s)
470        if (count($this->where)) {
471            $query .= 'WHERE ' . join(' AND ', $this->where) . ' ';
472        }
473
474        // Direct where clause(s)
475        if (count($this->cond)) {
476            if (!count($this->where)) {
477                $query .= 'WHERE 1 '; // Hack to cope with the operator included in top of each condition
478            }
479            $query .= join(' ', $this->cond) . ' ';
480        }
481
482        // Generic clause(s)
483        if (count($this->sql)) {
484            $query .= join(' ', $this->sql) . ' ';
485        }
486
487        // Group by clause (columns or aliases)
488        if (count($this->group)) {
489            $query .= 'GROUP BY ' . join(', ', $this->group) . ' ';
490        }
491
492        // Having clause(s)
493        if (count($this->having)) {
494            $query .= 'HAVING ' . join(' AND ', $this->having) . ' ';
495        }
496
497        // Order by clause (columns or aliases and optionnaly order ASC/DESC)
498        if (count($this->order)) {
499            $query .= 'ORDER BY ' . join(', ', $this->order) . ' ';
500        }
501
502        // Limit clause
503        if ($this->limit !== null) {
504            $query .= 'LIMIT ' . $this->limit . ' ';
505        }
506
507        // Offset clause
508        if ($this->offset !== null) {
509            $query .= 'OFFSET ' . $this->offset . ' ';
510        }
511
512        return trim($query);
513    }
514
515    /**
516     * Compare two SQL queries
517     *
518     * May be used for debugging purpose as:
519     * if (!$sql->isSame($sql->statement(), $oldRequest)) {
520     *    trigger_error('SQL statement error', E_USER_ERROR);
521     * }
522     *
523     * @param      string   $local     The local
524     * @param      string   $external  The external
525     *
526     * @return     boolean  True if same, False otherwise.
527     */
528    public function isSame($local, $external)
529    {
530        $filter = function ($s) {
531            $s = strtoupper($s);
532            $patterns = array(
533                '\s+' => ' ', // Multiple spaces/tabs -> one space
534                ' \)' => ')', // <space>) -> )
535                ' ,'  => ',', // <space>, -> ,
536                '\( ' => '(' // (<space> -> (
537            );
538            foreach ($patterns as $pattern => $replace) {
539                $s = preg_replace('!' . $pattern . '!', $replace, $s);
540            }
541            return $s;
542        };
543        return ($filter($local) === $filter($external));
544    }
545}
Note: See TracBrowser for help on using the repository browser.

Sites map