Dotclear

source: inc/core/class.dc.selectstatement.php @ 3747:3d9aee789637

Revision 3747:3d9aee789637, 22.2 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Add some helpers, delete, update and insert SQL builder

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 * SQL Statement : small utility to build SQL queries
16 */
17class dcSqlStatement
18{
19    protected $core;
20    protected $con;
21
22    protected $columns;
23    protected $from;
24    protected $where;
25    protected $cond;
26    protected $sql;
27
28    /**
29     * Class constructor
30     *
31     * @param dcCore    $core   dcCore instance
32     * @param mixed     $from   optional from clause(s)
33     */
34    public function __construct(&$core, $from = null)
35    {
36        $this->core = &$core;
37        $this->con  = &$core->con;
38
39        $this->columns =
40        $this->from    =
41        $this->where   =
42        $this->cond    =
43        $this->sql     =
44        array();
45
46        if ($from !== null) {
47            if (is_array($from)) {
48                $this->froms($from);
49            } else {
50                $this->from($from);
51            }
52        }
53    }
54
55    /**
56     * Magic getter method
57     *
58     * @param      string  $property  The property
59     *
60     * @return     mixed   property value if property exists
61     */
62    public function __get($property)
63    {
64        if (property_exists($this, $property)) {
65            return $this->$property;
66        }
67        trigger_error('Unknown property ' . $property, E_USER_ERROR);
68        return;
69    }
70
71    /**
72     * Magic setter method
73     *
74     * @param      string  $property  The property
75     * @param      mixed   $value     The value
76     *
77     * @return     self
78     */
79    public function __set($property, $value)
80    {
81        if (property_exists($this, $property)) {
82            $this->$property = $value;
83        } else {
84            trigger_error('Unknown property ' . $property, E_USER_ERROR);
85        }
86        return $this;
87    }
88
89    /**
90     * Adds column(s)
91     *
92     * @param mixed     $c      the column(s)
93     * @param boolean   $reset  reset previous column(s) first
94     *
95     * @return dcSelectStatement self instance, enabling to chain calls
96     */
97    public function columns($c, $reset = false)
98    {
99        if ($reset) {
100            $this->columns = array();
101        }
102        if (is_array($c)) {
103            $this->columns = array_merge($this->columns, $c);
104        } else {
105            array_push($this->columns, $c);
106        }
107        return $this;
108    }
109
110    /**
111     * columns() alias
112     *
113     * @param      mixed    $c      the column(s)
114     * @param      boolean  $reset  reset previous column(s) first
115     *
116     * @return dcSelectStatement self instance, enabling to chain calls
117     */
118    public function column($c, $reset = false)
119    {
120        return $this->columns($c, $reset);
121    }
122
123    /**
124     * Adds FROM clause(s)
125     *
126     * @param mixed     $c      the from clause(s)
127     * @param boolean   $reset  reset previous from(s) first
128     *
129     * @return dcSelectStatement self instance, enabling to chain calls
130     */
131    public function from($c, $reset = false)
132    {
133        if ($reset) {
134            $this->from = array();
135        }
136        if (is_array($c)) {
137            $filter = function($v) {
138                return trim(ltrim($v, ','));
139            };
140            $c          = array_map($filter, $c); // Cope with legacy code
141            $this->from = array_merge($this->from, $c);
142        } else {
143            $c = trim(ltrim($c, ',')); // Cope with legacy code
144            array_push($this->from, $c);
145        }
146        return $this;
147    }
148
149    /**
150     * Adds WHERE clause(s) condition (each will be AND combined in statement)
151     *
152     * @param mixed     $c      the clause(s)
153     * @param boolean   $reset  reset previous where(s) first
154     *
155     * @return dcSelectStatement self instance, enabling to chain calls
156     */
157    public function where($c, $reset = false)
158    {
159        if ($reset) {
160            $this->where = array();
161        }
162        if (is_array($c)) {
163            $this->where = array_merge($this->where, $c);
164        } else {
165            array_push($this->where, $c);
166        }
167        return $this;
168    }
169
170    /**
171     * Adds additional WHERE clause condition(s) (including an operator at beginning)
172     *
173     * @param mixed     $c      the clause(s)
174     * @param boolean   $reset  reset previous condition(s) first
175     *
176     * @return dcSelectStatement self instance, enabling to chain calls
177     */
178    public function cond($c, $reset = false)
179    {
180        if ($reset) {
181            $this->cond = array();
182        }
183        if (is_array($c)) {
184            $this->cond = array_merge($this->cond, $c);
185        } else {
186            array_push($this->cond, $c);
187        }
188        return $this;
189    }
190
191    /**
192     * Adds generic clause(s)
193     *
194     * @param mixed     $c      the clause(s)
195     * @param boolean   $reset  reset previous generic clause(s) first
196     *
197     * @return dcSelectStatement self instance, enabling to chain calls
198     */
199    public function sql($c, $reset = false)
200    {
201        if ($reset) {
202            $this->sql = array();
203        }
204        if (is_array($c)) {
205            $this->sql = array_merge($this->sql, $c);
206        } else {
207            array_push($this->sql, $c);
208        }
209        return $this;
210    }
211
212    // Helpers
213
214    /**
215     * Escape a value
216     *
217     * @param      string  $value  The value
218     *
219     * @return     string
220     */
221    public function escape($value)
222    {
223        return $this->con->escape($value);
224    }
225
226    /**
227     * Quote and escape a value if necessary (type string)
228     *
229     * @param      mixed    $value   The value
230     * @param      boolean  $escape  The escape
231     *
232     * @return     string
233     */
234    public function quote($value, $escape = true)
235    {
236        return
237            (is_string($value) ? "'" : '') .
238            ($escape ? $this->con->escape($value) : $value) .
239            (is_string($value) ? "'" : '');
240    }
241
242    /**
243     * Return an SQL IN (…) fragment
244     *
245     * @param      mixed  $list   The list
246     *
247     * @return     string
248     */
249    public function in($list)
250    {
251        return $this->con->in($list);
252    }
253
254    /**
255     * Return an SQL formatted date
256     *
257     * @param   string    $field     Field name
258     * @param   string    $pattern   Date format
259     *
260     * @return     string
261     */
262    public function dateFormat($field, $pattern)
263    {
264        return $this->con->dateFormat($field, $pattern);
265    }
266
267    /**
268     * Return an SQL formatted REGEXP clause
269     *
270     * @param      string  $value  The value
271     *
272     * @return     string
273     */
274    public function regexp($value)
275    {
276        if ($this->con->driver() == 'mysql' || $this->con->driver() == 'mysqli' || $this->con->driver() == 'mysqlimb4') {
277            $clause = "REGEXP '^" . $this->escape(preg_quote($value)) . "[0-9]+$'";
278        } elseif ($this->con->driver() == 'pgsql') {
279            $clause = "~ '^" . $this->escape(preg_quote($value)) . "[0-9]+$'";
280        } else {
281            $clause = "LIKE '" .
282                $sql->escape(preg_replace(array('%', '_', '!'), array('!%', '!_', '!!'), $value)) .
283                "%' ESCAPE '!'";
284        }
285        return $clause;
286    }
287
288    /**
289     * Compare two SQL queries
290     *
291     * May be used for debugging purpose as:
292     *
293     * if (!$sql->isSame($sql->statement(), $oldRequest)) {
294     *    trigger_error('SQL statement error: ' . $sql->statement() . ' / ' .$oldRequest, E_USER_ERROR);
295     * }
296     *
297     * @param      string   $local     The local
298     * @param      string   $external  The external
299     *
300     * @return     boolean  True if same, False otherwise.
301     */
302    public function isSame($local, $external)
303    {
304        $filter = function ($s) {
305            $s = strtoupper($s);
306            $patterns = array(
307                '\s+' => ' ', // Multiple spaces/tabs -> one space
308                ' \)' => ')', // <space>) -> )
309                ' ,'  => ',', // <space>, -> ,
310                '\( ' => '(' // (<space> -> (
311            );
312            foreach ($patterns as $pattern => $replace) {
313                $s = preg_replace('!' . $pattern . '!', $replace, $s);
314            }
315            return trim($s);
316        };
317        return ($filter($local) === $filter($external));
318    }
319}
320
321/**
322 * Select Statement : small utility to build select queries
323 */
324class dcSelectStatement extends dcSqlStatement
325{
326    protected $join;
327    protected $having;
328    protected $order;
329    protected $group;
330    protected $limit;
331    protected $offset;
332    protected $distinct;
333
334    /**
335     * Class constructor
336     *
337     * @param dcCore    $core   dcCore instance
338     * @param mixed     $from   optional from clause(s)
339     */
340    public function __construct(&$core, $from = null)
341    {
342        $this->join    =
343        $this->having  =
344        $this->order   =
345        $this->group   =
346        array();
347
348        $this->limit    = null;
349        $this->offset   = null;
350        $this->distinct = false;
351
352        parent::__construct($core, $from);
353    }
354
355    /**
356     * Adds JOIN clause(s) (applied on first from item only)
357     *
358     * @param mixed     $c      the join clause(s)
359     * @param boolean   $reset  reset previous join(s) first
360     *
361     * @return dcSelectStatement self instance, enabling to chain calls
362     */
363    public function join($c, $reset = false)
364    {
365        if ($reset) {
366            $this->join = array();
367        }
368        if (is_array($c)) {
369            $this->join = array_merge($this->join, $c);
370        } else {
371            array_push($this->join, $c);
372        }
373        return $this;
374    }
375
376    /**
377     * Adds HAVING clause(s)
378     *
379     * @param mixed     $c      the clause(s)
380     * @param boolean   $reset  reset previous having(s) first
381     *
382     * @return dcSelectStatement self instance, enabling to chain calls
383     */
384    public function having($c, $reset = false)
385    {
386        if ($reset) {
387            $this->having = array();
388        }
389        if (is_array($c)) {
390            $this->having = array_merge($this->having, $c);
391        } else {
392            array_push($this->having, $c);
393        }
394        return $this;
395    }
396
397    /**
398     * Adds ORDER BY clause(s)
399     *
400     * @param mixed     $c      the clause(s)
401     * @param boolean   $reset  reset previous order(s) first
402     *
403     * @return dcSelectStatement self instance, enabling to chain calls
404     */
405    public function order($c, $reset = false)
406    {
407        if ($reset) {
408            $this->order = array();
409        }
410        if (is_array($c)) {
411            $this->order = array_merge($this->order, $c);
412        } else {
413            array_push($this->order, $c);
414        }
415        return $this;
416    }
417
418    /**
419     * Adds GROUP BY clause(s)
420     *
421     * @param mixed     $c      the clause(s)
422     * @param boolean   $reset  reset previous group(s) first
423     *
424     * @return dcSelectStatement self instance, enabling to chain calls
425     */
426    public function group($c, $reset = false)
427    {
428        if ($reset) {
429            $this->group = array();
430        }
431        if (is_array($c)) {
432            $this->group = array_merge($this->group, $c);
433        } else {
434            array_push($this->group, $c);
435        }
436        return $this;
437    }
438
439    /**
440     * Defines the LIMIT for select
441     *
442     * @param mixed $limit
443     * @return dcSelectStatement self instance, enabling to chain calls
444     */
445    public function limit($limit)
446    {
447        $offset = null;
448        if (is_array($limit)) {
449            // Keep only values
450            $limit = array_values($limit);
451            // If 2 values, [0] -> offset, [1] -> limit
452            // If 1 value, [0] -> limit
453            if (isset($limit[1])) {
454                $offset = $limit[0];
455                $limit  = $limit[1];
456            } else {
457                $limit = limit[0];
458            }
459        }
460        $this->limit = $limit;
461        if ($offset !== null) {
462            $this->offset = $offset;
463        }
464        return $this;
465    }
466
467    /**
468     * Defines the OFFSET for select
469     *
470     * @param integer $offset
471     * @return dcSelectStatement self instance, enabling to chain calls
472     */
473    public function offset($offset)
474    {
475        $this->offset = $offset;
476        return $this;
477    }
478
479    /**
480     * Defines the DISTINCT flag for select
481     *
482     * @param boolean $distinct
483     * @return dcSelectStatement self instance, enabling to chain calls
484     */
485    public function distinct($distinct = true)
486    {
487        $this->distinct = $distinct;
488        return $this;
489    }
490
491    /**
492     * Returns the select statement
493     *
494     * @return string the statement
495     */
496    public function statement()
497    {
498        // Check if source given
499        if (!count($this->from)) {
500            trigger_error(__('SQL SELECT requires a FROM source'), E_USER_ERROR);
501            return '';
502        }
503
504        // Query
505        $query = 'SELECT ' . ($this->distinct ? 'DISTINCT ' : '');
506
507        // Specific column(s) or all (*)
508        if (count($this->columns)) {
509            $query .= join(', ', $this->columns) . ' ';
510        } else {
511            $query .= '* ';
512        }
513
514        // Table(s) and Join(s)
515        $query .= 'FROM ' . $this->from[0] . ' ';
516        $query .= join(' ', $this->join) . ' ';
517        if (count($this->from) > 1) {
518            array_shift($this->from);
519            $query .= ', ' . join(', ', $this->from) . ' '; // All other from(s)
520        }
521
522        // Where clause(s)
523        if (count($this->where)) {
524            $query .= 'WHERE ' . join(' AND ', $this->where) . ' ';
525        }
526
527        // Direct where clause(s)
528        if (count($this->cond)) {
529            if (!count($this->where)) {
530                $query .= 'WHERE 1 '; // Hack to cope with the operator included in top of each condition
531            }
532            $query .= join(' ', $this->cond) . ' ';
533        }
534
535        // Generic clause(s)
536        if (count($this->sql)) {
537            $query .= join(' ', $this->sql) . ' ';
538        }
539
540        // Group by clause (columns or aliases)
541        if (count($this->group)) {
542            $query .= 'GROUP BY ' . join(', ', $this->group) . ' ';
543        }
544
545        // Having clause(s)
546        if (count($this->having)) {
547            $query .= 'HAVING ' . join(' AND ', $this->having) . ' ';
548        }
549
550        // Order by clause (columns or aliases and optionnaly order ASC/DESC)
551        if (count($this->order)) {
552            $query .= 'ORDER BY ' . join(', ', $this->order) . ' ';
553        }
554
555        // Limit clause
556        if ($this->limit !== null) {
557            $query .= 'LIMIT ' . $this->limit . ' ';
558        }
559
560        // Offset clause
561        if ($this->offset !== null) {
562            $query .= 'OFFSET ' . $this->offset . ' ';
563        }
564
565        return trim($query);
566    }
567}
568
569/**
570 * Delete Statement : small utility to build delete queries
571 */
572class dcDeleteStatement extends dcSqlStatement
573{
574    /**
575     * Returns the delete statement
576     *
577     * @return string the statement
578     */
579    public function statement()
580    {
581        // Check if source given
582        if (!count($this->from)) {
583            trigger_error(__('SQL DELETE requires a FROM source'), E_USER_ERROR);
584            return '';
585        }
586
587        // Query
588        $query = 'DELETE ';
589
590        // Table
591        $query .= 'FROM ' . $this->from[0] . ' ';
592
593        // Where clause(s)
594        if (count($this->where)) {
595            $query .= 'WHERE ' . join(' AND ', $this->where) . ' ';
596        }
597
598        // Direct where clause(s)
599        if (count($this->cond)) {
600            if (!count($this->where)) {
601                $query .= 'WHERE 1 '; // Hack to cope with the operator included in top of each condition
602            }
603            $query .= join(' ', $this->cond) . ' ';
604        }
605
606        // Generic clause(s)
607        if (count($this->sql)) {
608            $query .= join(' ', $this->sql) . ' ';
609        }
610
611        return trim($query);
612    }
613}
614
615/**
616 * Update Statement : small utility to build update queries
617 */
618class dcUpdateStatement extends dcSqlStatement
619{
620    protected $set;
621
622    /**
623     * Class constructor
624     *
625     * @param dcCore    $core   dcCore instance
626     * @param mixed     $from   optional from clause(s)
627     */
628    public function __construct(&$core, $from = null)
629    {
630        $this->set = array();
631
632        parent::__construct($core, $from);
633    }
634
635    /**
636     * from() alias
637     *
638     * @param mixed     $c      the reference clause(s)
639     * @param boolean   $reset  reset previous reference first
640     *
641     * @return dcUpdateStatement self instance, enabling to chain calls
642     */
643    public function reference($c, $reset = false)
644    {
645        return $this->from($c, $reset);
646    }
647
648    /**
649     * from() alias
650     *
651     * @param mixed     $c      the reference clause(s)
652     * @param boolean   $reset  reset previous reference first
653     *
654     * @return dcUpdateStatement self instance, enabling to chain calls
655     */
656    public function ref($c, $reset = false)
657    {
658        return $this->reference($c, $reset);
659    }
660
661    /**
662     * Adds update value(s)
663     *
664     * @param mixed     $c      the udpate values(s)
665     * @param boolean   $reset  reset previous update value(s) first
666     *
667     * @return dcUpdateStatement self instance, enabling to chain calls
668     */
669    public function set($c, $reset = false)
670    {
671        if ($reset) {
672            $this->set = array();
673        }
674        if (is_array($c)) {
675            $this->set = array_merge($this->set, $c);
676        } else {
677            array_push($this->set, $c);
678        }
679        return $this;
680    }
681
682    /**
683     * set() alias
684     *
685     * @param      mixed    $c      the update value(s)
686     * @param      boolean  $reset  reset previous update value(s) first
687     *
688     * @return dcUpdateStatement self instance, enabling to chain calls
689     */
690    public function sets($c, $reset = false)
691    {
692        return $this->set($c, $reset);
693    }
694
695    /**
696     * Returns the WHERE part of update statement
697     *
698     * Useful to construct the where clause used with cursor->update() method
699     */
700    public function whereStatement()
701    {
702        $query = '';
703
704        // Where clause(s)
705        if (count($this->where)) {
706            $query .= 'WHERE ' . join(' AND ', $this->where) . ' ';
707        }
708
709        // Direct where clause(s)
710        if (count($this->cond)) {
711            if (!count($this->where)) {
712                $query .= 'WHERE 1 '; // Hack to cope with the operator included in top of each condition
713            }
714            $query .= join(' ', $this->cond) . ' ';
715        }
716
717        // Generic clause(s)
718        if (count($this->sql)) {
719            $query .= join(' ', $this->sql) . ' ';
720        }
721
722        return trim($query);
723    }
724
725    /**
726     * Returns the update statement
727     *
728     * @return string the statement
729     */
730    public function statement()
731    {
732        // Check if source given
733        if (!count($this->from)) {
734            trigger_error(__('SQL UPDATE requires an INTO source'), E_USER_ERROR);
735            return '';
736        }
737
738        // Query
739        $query = 'UPDATE ';
740
741        // Reference
742        $query .= $this->from[0] . ' ';
743
744        // Value(s)
745        if (count($this->set)) {
746            $query .= 'SET ' . join(', ', $this->set) . ' ';
747        }
748
749        // Where clause(s)
750        if (count($this->where)) {
751            $query .= 'WHERE ' . join(' AND ', $this->where) . ' ';
752        }
753
754        // Direct where clause(s)
755        if (count($this->cond)) {
756            if (!count($this->where)) {
757                $query .= 'WHERE 1 '; // Hack to cope with the operator included in top of each condition
758            }
759            $query .= join(' ', $this->cond) . ' ';
760        }
761
762        // Generic clause(s)
763        if (count($this->sql)) {
764            $query .= join(' ', $this->sql) . ' ';
765        }
766
767        return trim($query);
768    }
769}
770
771/**
772 * Insert Statement : small utility to build insert queries
773 */
774class dcInsertStatement extends dcSqlStatement
775{
776    protected $lines;
777
778    /**
779     * Class constructor
780     *
781     * @param dcCore    $core   dcCore instance
782     * @param mixed     $into   optional into clause(s)
783     */
784    public function __construct(&$core, $into = null)
785    {
786        $this->lines = array();
787
788        parent::__construct($core, $into);
789    }
790
791    /**
792     * from() alias
793     *
794     * @param mixed     $c      the into clause(s)
795     * @param boolean   $reset  reset previous into first
796     *
797     * @return dcSelectStatement self instance, enabling to chain calls
798     */
799    public function into($c, $reset = false)
800    {
801        return $this->into($c, $reset);
802    }
803
804    /**
805     * Adds update value(s)
806     *
807     * @param mixed     $c      the insert values(s)
808     * @param boolean   $reset  reset previous insert value(s) first
809     *
810     * @return dcSelectStatement self instance, enabling to chain calls
811     */
812    public function lines($c, $reset = false)
813    {
814        if ($reset) {
815            $this->lines = array();
816        }
817        if (is_array($c)) {
818            $this->lines = array_merge($this->lines, $c);
819        } else {
820            array_push($this->lines, $c);
821        }
822        return $this;
823    }
824
825    /**
826     * line() alias
827     *
828     * @param      mixed    $c      the insert value(s)
829     * @param      boolean  $reset  reset previous insert value(s) first
830     *
831     * @return dcInsertStatement self instance, enabling to chain calls
832     */
833    public function line($c, $reset = false)
834    {
835        return $this->lines($c, $reset);
836    }
837
838    /**
839     * Returns the insert statement
840     *
841     * @return string the statement
842     */
843    public function statement()
844    {
845        // Check if source given
846        if (!count($this->from)) {
847            trigger_error(__('SQL INSERT requires an INTO source'), E_USER_ERROR);
848            return '';
849        }
850
851        // Query
852        $query = 'INSERT ';
853
854        // Reference
855        $query .= 'INTO ' . $this->from[0] . ' ';
856
857        // Column(s)
858        if (count($this->columns)) {
859            $query .= '(' . join(', ', $this->columns) . ') ';
860        }
861
862        // Value(s)
863        $query .= 'VALUES ';
864        if (count($this->lines)) {
865            $raws = array();
866            foreach ($this->lines as $line) {
867                $raws[] = '(' . join(', ', $line) . ')';
868            }
869            $query .= join(', ', $raws);
870        } else {
871            // Use SQL default values (useful only if SQL strict mode is off or if every columns has a defined default value)
872            $query .= '()';
873        }
874
875        return trim($query);
876    }
877}
Note: See TracBrowser for help on using the repository browser.

Sites map