Dotclear

source: inc/core/class.dc.sql.statement.php @ 3754:c16c1cadbae6

Revision 3754:c16c1cadbae6, 23.7 KB checked in by franck <carnet.franck.paul@…>, 7 years ago (diff)

Use con->syntax rather than con->driver where is useful

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

Sites map