Completed
Push — master ( 315f2a...ca41cd )
by Jared
02:18
created

WhereStatement::buildIn()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
nop 3
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
            // todo
100
101
            // handles #1 and #2
102
            $condition[] = $operator;
103
            $condition[] = $value;
104
        }
105
106
        $this->conditions[] = $condition;
107
108
        return $this;
109
    }
110
111
    /**
112
     * Adds an OR condition. Uses same arguments as
113
     * adding AND conditions.
114
     *
115
     * @return self
116
     */
117
    public function addOrCondition()
118
    {
119
        $this->conditions[] = ['OR'];
120
121
        return call_user_func_array([$this, 'addCondition'], func_get_args());
122
    }
123
124
    /**
125
     * Adds a between condition to the query.
126
     *
127
     * @param string $field
128
     * @param mixed  $a     first between value
129
     * @param mixed  $b     second between value
130
     *
131
     * @return self
132
     */
133
    public function addBetweenCondition($field, $a, $b)
134
    {
135
        $this->conditions[] = ['BETWEEN', $field, $a, $b, true];
136
137
        return $this;
138
    }
139
140
    /**
141
     * Adds a not between condition to the query.
142
     *
143
     * @param string $field
144
     * @param mixed  $a     first between value
145
     * @param mixed  $b     second between value
146
     *
147
     * @return self
148
     */
149
    public function addNotBetweenCondition($field, $a, $b)
150
    {
151
        $this->conditions[] = ['BETWEEN', $field, $a, $b, false];
152
153
        return $this;
154
    }
155
156
    /**
157
     * Adds an exists condition to the query.
158
     *
159
     * @param callable $f
160
     *
161
     * @return self
162
     */
163
    public function addExistsCondition(callable $f)
164
    {
165
        $this->conditions[] = ['EXISTS', $f, true];
166
167
        return $this;
168
    }
169
170
    /**
171
     * Adds a not exists condition to the query.
172
     *
173
     * @param callable $f
174
     *
175
     * @return self
176
     */
177
    public function addNotExistsCondition(callable $f)
178
    {
179
        $this->conditions[] = ['EXISTS', $f, false];
180
181
        return $this;
182
    }
183
184
    /**
185
     * Gets the conditions for this statement.
186
     *
187
     * @return array
188
     */
189
    public function getConditions()
190
    {
191
        return $this->conditions;
192
    }
193
194
    public function build()
195
    {
196
        // reset the parameterized values
197
        $this->values = [];
198
199
        // build clause from conditions
200
        $clauses = [];
201
        foreach ($this->conditions as $condition) {
202
            $clauses[] = $this->buildClause($condition);
203
        }
204
205
        // remove empty values
206
        $clauses = array_filter($clauses);
207
208
        if (count($clauses) == 0) {
209
            return '';
210
        }
211
212
        return ((!$this->having) ? 'WHERE ' : 'HAVING ').$this->implodeClauses($clauses);
213
    }
214
215
    /**
216
     * Builds a parameterized and escaped SQL fragment
217
     * for a condition that uses our own internal
218
     * representation.
219
     *
220
     * A condition is represented by an array, and can be
221
     * have one of the following forms:
222
     * 1. ['SQL fragment']
223
     * 2. ['identifier', '=', 'value']
224
     * 3. ['BETWEEN', 'identifier', 'value', 'value', true]
225
     * 4. ['EXISTS', function(SelectQuery $query) {}, true]
226
     * 5. [function(SelectQuery $query) {}]
227
     * 6. [function(SelectQuery $query) {}, '=', 'value']
228
     *
229
     * @param array $cond
230
     *
231
     * @return string generated SQL fragment
232
     */
233
    protected function buildClause(array $cond)
234
    {
235
        // handle SQL fragments
236
        if (count($cond) == 1 && (is_string($cond[0]) || !is_callable($cond[0]))) {
237
            return $cond[0];
238
        }
239
240
        // handle EXISTS conditions
241
        if ($cond[0] === 'EXISTS') {
242
            return $this->buildExists($cond[1], $cond[2]);
243
        }
244
245
        // handle BETWEEN conditions
246
        if ($cond[0] === 'BETWEEN') {
247
            return $this->buildBetween($cond[1], $cond[2], $cond[3], $cond[4]);
248
        }
249
250
        // escape an identifier
251
        if (is_string($cond[0]) || !is_callable($cond[0])) {
252
            $cond[0] = $this->escapeIdentifier($cond[0]);
253
254
        // handle a subquery
255
        // NOTE string callables are not supported
256
        // as subquery functions
257
        } elseif (is_callable($cond[0])) {
258
            $cond[0] = $this->buildSubquery($cond[0]);
259
        }
260
261
        if (count($cond) === 1 || empty($cond[0])) {
262
            return $cond[0];
263
        }
264
265
        // handle NULL values
266
        if ($cond[2] === null && in_array($cond[1], ['=', '<>'])) {
267
            return $this->buildNull($cond[0], $cond[1] == '=');
268
        }
269
270
        // handle IN values
271
        if (is_array($cond[2]) && in_array($cond[1], ['=', '<>'])) {
272
            return $this->buildIn($cond[0], $cond[2], $cond[1] == '=');
273
        }
274
275
        // otherwise parameterize the value
276
        $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...
277
278
        return implode(' ', $cond);
279
    }
280
281
    /**
282
     * Builds a subquery.
283
     *
284
     * @param callable $f
285
     *
286
     * @return string
287
     */
288
    protected function buildSubquery(callable $f)
289
    {
290
        $query = new SelectQuery();
291
        $query->getSelect()->clearFields();
292
        $f($query);
293
        $sql = $query->build();
294
        $this->values = array_merge($this->values, $query->getValues());
295
296
        return '('.$sql.')';
297
    }
298
299
    /**
300
     * Builds an EXISTS clause.
301
     *
302
     * @param callable $f
303
     * @param bool     $isExists
304
     *
305
     * @return string
306
     */
307
    protected function buildExists(callable $f, $isExists)
308
    {
309
        $operator = $isExists ? 'EXISTS' : 'NOT EXISTS';
310
311
        return $operator.' '.$this->buildSubquery($f);
312
    }
313
314
    /**
315
     * Builds a BETWEEN clause.
316
     *
317
     * @param string $field
318
     * @param mixed  $value1
319
     * @param mixed  $value2
320
     * @param bool   $isBetween
321
     *
322
     * @return string
323
     */
324
    protected function buildBetween($field, $value1, $value2, $isBetween)
325
    {
326
        $operator = $isBetween ? 'BETWEEN' : 'NOT BETWEEN';
327
328
        return $this->escapeIdentifier($field).' '.$operator.' '.$this->parameterize($value1).' AND '.$this->parameterize($value2);
329
    }
330
331
    /**
332
     * Builds a NULL clause.
333
     *
334
     * @param string $field
335
     * @param bool   $isEqual
336
     *
337
     * @return string
338
     */
339
    protected function buildNull($field, $isEqual)
340
    {
341
        $operator = $isEqual ? ' IS NULL' : ' IS NOT NULL';
342
343
        return $field.$operator;
344
    }
345
346
    protected function buildIn($field, $values, $isIn)
347
    {
348
        $operator = $isIn ? ' IN ' : ' NOT IN ';
349
350
        return $field.$operator.$this->parameterizeValues($values);
351
    }
352
353
    /**
354
     * Implodes a list of WHERE clauses.
355
     *
356
     * @param array $clauses
357
     *
358
     * @return string
359
     */
360
    protected function implodeClauses(array $clauses)
361
    {
362
        $str = '';
363
        $op = false;
364
        foreach ($clauses as $clause) {
365
            // an 'OR' token will change the operator used
366
            // when concatenating the next clause
367
            if ($clause == 'OR') {
368
                $op = ' OR ';
369
                continue;
370
            }
371
372
            if ($op && $str) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $op of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
373
                $str .= $op;
374
            }
375
376
            $str .= $clause;
377
            $op = ' AND ';
378
        }
379
380
        return $str;
381
    }
382
}
383