Completed
Push — master ( 50d5a1...5a832a )
by Jared
02:13
created

WhereStatement::addOrCondition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace JAQB\Statement;
12
13
use JAQB\Query\SelectQuery;
14
15
class WhereStatement extends Statement
16
{
17
    /**
18
     * @var bool
19
     */
20
    protected $having;
21
22
    /**
23
     * @var array
24
     */
25
    protected $conditions = [];
26
27
    /**
28
     * @param bool $having when true, statement becomes a having statement
29
     */
30
    public function __construct($having = false)
31
    {
32
        $this->having = $having;
33
    }
34
35
    /**
36
     * Tells whether this statement is a HAVING statement.
37
     *
38
     * @return bool true: is HAVING, false: is WHERE
39
     */
40
    public function isHaving()
41
    {
42
        return $this->having;
43
    }
44
45
    /**
46
     * Adds a condition to the statement.
47
     *
48
     * Accepts the following forms:
49
     * 1. Equality comparison:
50
     *      addCondition('username', 'john')
51
     * 2. Comparison with custom operator:
52
     *      addCondition('balance', 100, '>')
53
     * 3. IN statement:
54
     *      addCondition('group', ['admin', 'owner'])
55
     * 4. SQL fragment:
56
     *      addCondition('name LIKE "%john%"')
57
     * 5. Subquery:
58
     *      addCondition(function(SelectQuery $query) {})
59
     * 6. List of conditions to add:
60
     *      addCondition([['balance', 100, '>'],
61
     *                    ['user_id', 5]])
62
     * 7. Map of equality comparisons:
63
     *      addCondition(['username' => 'john',
64
     *                    'user_id' => 5])
65
     * 8. List of SQL fragments:
66
     *      addCondition(['first_name LIKE "%john%"',
67
     *                    'last_name LIKE "%doe%"'])
68
     *
69
     * @param array|string $field
70
     * @param string|bool  $value    condition value (optional)
71
     * @param string       $operator operator (optional)
72
     *
73
     * @return self
74
     */
75
    public function addCondition($field, $value = false, $operator = '=')
76
    {
77
        if (is_array($field) && !$value) {
78
            foreach ($field as $key => $value) {
79
                // handles #6
80
                if (is_array($value)) {
81
                    call_user_func_array([$this, 'addCondition'], $value);
82
                // handles #7
83
                } elseif (!is_numeric($key)) {
84
                    $this->addCondition($key, $value);
85
                // handles #8
86
                } else {
87
                    $this->addCondition($value);
88
                }
89
            }
90
91
            return $this;
92
        }
93
94
        // handles #4 and #5
95
        $condition = [$field];
96
97
        if (func_num_args() >= 2) {
98
            // handles #3
99
            if (is_array($value) && $operator === '=') {
100
                $operator = 'IN';
101
            } elseif (is_array($value) && $operator === '<>') {
102
                $operator = 'NOT IN';
103
            }
104
105
            // handles #1 and #2
106
            $condition[] = $operator;
107
            $condition[] = $value;
108
        }
109
110
        $this->conditions[] = $condition;
111
112
        return $this;
113
    }
114
115
    /**
116
     * Adds an OR condition. Uses same arguments as
117
     * adding AND conditions.
118
     *
119
     * @return self
120
     */
121
    public function addOrCondition()
122
    {
123
        $this->conditions[] = ['OR'];
124
125
        return call_user_func_array([$this, 'addCondition'], func_get_args());
126
    }
127
128
    /**
129
     * Adds a between condition to the query.
130
     *
131
     * @param string $field
132
     * @param mixed  $a     first between value
133
     * @param mixed  $b     second between value
134
     *
135
     * @return self
136
     */
137
    public function addBetweenCondition($field, $a, $b)
138
    {
139
        $this->conditions[] = ['BETWEEN', $field, $a, $b, true];
140
141
        return $this;
142
    }
143
144
    /**
145
     * Adds a not between condition to the query.
146
     *
147
     * @param string $field
148
     * @param mixed  $a     first between value
149
     * @param mixed  $b     second between value
150
     *
151
     * @return self
152
     */
153
    public function addNotBetweenCondition($field, $a, $b)
154
    {
155
        $this->conditions[] = ['BETWEEN', $field, $a, $b, false];
156
157
        return $this;
158
    }
159
160
    /**
161
     * Adds an exists condition to the query.
162
     *
163
     * @param callable $f
164
     *
165
     * @return self
166
     */
167
    public function addExistsCondition(callable $f)
168
    {
169
        $this->conditions[] = ['EXISTS', $f, true];
170
171
        return $this;
172
    }
173
174
    /**
175
     * Adds a not exists condition to the query.
176
     *
177
     * @param callable $f
178
     *
179
     * @return self
180
     */
181
    public function addNotExistsCondition(callable $f)
182
    {
183
        $this->conditions[] = ['EXISTS', $f, false];
184
185
        return $this;
186
    }
187
188
    /**
189
     * Gets the conditions for this statement.
190
     *
191
     * @return array
192
     */
193
    public function getConditions()
194
    {
195
        return $this->conditions;
196
    }
197
198
    public function build()
199
    {
200
        // reset the parameterized values
201
        $this->values = [];
202
203
        // build clause from conditions
204
        $clauses = [];
205
        foreach ($this->conditions as $condition) {
206
            $clauses[] = $this->buildClause($condition);
207
        }
208
209
        // remove empty values
210
        $clauses = array_filter($clauses);
211
212
        if (count($clauses) == 0) {
213
            return '';
214
        }
215
216
        return ((!$this->having) ? 'WHERE ' : 'HAVING ').$this->implodeClauses($clauses);
217
    }
218
219
    /**
220
     * Builds a parameterized and escaped SQL fragment
221
     * for a condition that uses our own internal
222
     * representation.
223
     *
224
     * A condition is represented by an array, and can be
225
     * have one of the following forms:
226
     * 1. ['SQL fragment']
227
     * 2. ['identifier', '=', 'value']
228
     * 3. ['BETWEEN', 'identifier', 'value', 'value', true]
229
     * 4. ['EXISTS', function(SelectQuery $query) {}, true]
230
     * 5. [function(SelectQuery $query) {}]
231
     * 6. [function(SelectQuery $query) {}, '=', 'value']
232
     *
233
     * @param array $cond
234
     *
235
     * @return string generated SQL fragment
236
     */
237
    protected function buildClause(array $cond)
238
    {
239
        // handle SQL fragments
240
        if (count($cond) == 1 && (is_string($cond[0]) || !is_callable($cond[0]))) {
241
            return $cond[0];
242
        }
243
244
        // handle EXISTS conditions
245
        if ($cond[0] === 'EXISTS') {
246
            return $this->buildExists($cond[1], $cond[2]);
247
        }
248
249
        // handle BETWEEN conditions
250
        if ($cond[0] === 'BETWEEN') {
251
            return $this->buildBetween($cond[1], $cond[2], $cond[3], $cond[4]);
252
        }
253
254
        // escape an identifier
255
        if (is_string($cond[0]) || !is_callable($cond[0])) {
256
            $cond[0] = $this->escapeIdentifier($cond[0]);
257
258
        // handle a subquery
259
        // NOTE string callables are not supported
260
        // as subquery functions
261
        } elseif (is_callable($cond[0])) {
262
            $cond[0] = $this->buildSubquery($cond[0]);
263
        }
264
265
        if (count($cond) === 1 || empty($cond[0])) {
266
            return $cond[0];
267
        }
268
269
        // handle NULL values
270
        if ($cond[2] === null && in_array($cond[1], ['=', '<>'])) {
271
            return $this->buildNull($cond[0], $cond[1] == '=');
272
        }
273
274
        // handle array values, i.e. for IN conditions
275
        if (is_array($cond[2])) {
276
            $cond[2] = $this->parameterizeValues($cond[2]);
277
        // otherwise parameterize the value
278
        } else {
279
            $cond[2] = $this->parameterize($cond[2]);
0 ignored issues
show
Documentation introduced by
$cond[2] is of type callable, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
280
        }
281
282
        return implode(' ', $cond);
283
    }
284
285
    /**
286
     * Builds a subquery.
287
     *
288
     * @param callable $f
289
     *
290
     * @return string
291
     */
292
    protected function buildSubquery(callable $f)
293
    {
294
        $query = new SelectQuery();
295
        $query->getSelect()->clearFields();
296
        $f($query);
297
        $sql = $query->build();
298
        $this->values = array_merge($this->values, $query->getValues());
299
300
        return '('.$sql.')';
301
    }
302
303
    /**
304
     * Builds an EXISTS clause.
305
     *
306
     * @param callable $f
307
     * @param bool     $isExists
308
     *
309
     * @return string
310
     */
311
    protected function buildExists(callable $f, $isExists)
312
    {
313
        $operator = $isExists ? 'EXISTS' : 'NOT EXISTS';
314
315
        return $operator.' '.$this->buildSubquery($f);
316
    }
317
318
    /**
319
     * Builds a BETWEEN clause.
320
     *
321
     * @param string $field
322
     * @param mixed  $value1
323
     * @param mixed  $value2
324
     * @param bool   $isBetween
325
     *
326
     * @return string
327
     */
328
    protected function buildBetween($field, $value1, $value2, $isBetween)
329
    {
330
        $operator = $isBetween ? 'BETWEEN' : 'NOT BETWEEN';
331
332
        return $this->escapeIdentifier($field).' '.$operator.' '.$this->parameterize($value1).' AND '.$this->parameterize($value2);
333
    }
334
335
    /**
336
     * Builds a NULL clause.
337
     *
338
     * @param string $field
339
     * @param bool   $isEqual
340
     *
341
     * @return string
342
     */
343
    protected function buildNull($field, $isEqual)
344
    {
345
        $operator = $isEqual ? ' IS NULL' : ' IS NOT NULL';
346
347
        return $field.$operator;
348
    }
349
350
    /**
351
     * Implodes a list of WHERE clauses.
352
     *
353
     * @param array $clauses
354
     *
355
     * @return string
356
     */
357
    protected function implodeClauses(array $clauses)
358
    {
359
        $str = false;
360
        $or = false;
361
        foreach ($clauses as $clause) {
362
            // an 'OR' token will change the operator used
363
            // when concatenating the next clause
364
            if ($clause == 'OR') {
365
                $or = true;
366
                continue;
367
            }
368
369
            if (!$str) { // first clause needs no operator
370
                $str = $clause;
371
            } elseif ($or) {
372
                $str .= " OR $clause";
373
            } else {
374
                $str .= " AND $clause";
375
            }
376
377
            $or = false;
378
        }
379
380
        return $str;
381
    }
382
}
383