Completed
Push — master ( 8331f7...6ba9a8 )
by Jared
02:15
created

WhereStatement::buildExists()   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 2
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 #5
80
                if (is_array($value)) {
81
                    call_user_func_array([$this, 'addCondition'], $value);
82
                // handles #6
83
                } elseif (!is_numeric($key)) {
84
                    $this->addCondition($key, $value);
85
                // handles #7
86
                } else {
87
                    $this->addCondition($value);
88
                }
89
            }
90
91
            return $this;
92
        }
93
94
        // handles #4
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
    public function addConditionOr($field, $value = false, $operator = '=')
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $operator is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
116
    {
117
        $this->conditions[] = ['OR'];
118
119
        return call_user_func_array([$this, 'addCondition'], func_get_args());
120
    }
121
122
    /**
123
     * Adds a between condition to the query.
124
     *
125
     * @param string $field
126
     * @param mixed  $a     first between value
127
     * @param mixed  $b     second between value
128
     *
129
     * @return self
130
     */
131
    public function addBetweenCondition($field, $a, $b)
132
    {
133
        $this->conditions[] = [$field, 'BETWEEN', $a, $b];
134
135
        return $this;
136
    }
137
138
    /**
139
     * Adds a not between condition to the query.
140
     *
141
     * @param string $field
142
     * @param mixed  $a     first between value
143
     * @param mixed  $b     second between value
144
     *
145
     * @return self
146
     */
147
    public function addNotBetweenCondition($field, $a, $b)
148
    {
149
        $this->conditions[] = [$field, 'NOT BETWEEN', $a, $b];
150
151
        return $this;
152
    }
153
154
    /**
155
     * Adds an exists condition to the query.
156
     *
157
     * @param callable $f
158
     *
159
     * @return self
160
     */
161
    public function addExistsCondition(callable $f)
162
    {
163
        $this->conditions[] = ['EXISTS', $f];
164
165
        return $this;
166
    }
167
168
    /**
169
     * Adds a not exists condition to the query.
170
     *
171
     * @param callable $f
172
     *
173
     * @return self
174
     */
175
    public function addNotExistsCondition(callable $f)
176
    {
177
        $this->conditions[] = ['NOT EXISTS', $f];
178
179
        return $this;
180
    }
181
182
    /**
183
     * Gets the conditions for this statement.
184
     *
185
     * @return array
186
     */
187
    public function getConditions()
188
    {
189
        return $this->conditions;
190
    }
191
192
    public function build()
193
    {
194
        // reset the parameterized values
195
        $this->values = [];
196
197
        // build clause from conditions
198
        $clauses = [];
199
        foreach ($this->conditions as $condition) {
200
            $clauses[] = $this->buildClause($condition);
201
        }
202
203
        // remove empty values
204
        $clauses = array_filter($clauses);
205
206
        if (count($clauses) == 0) {
207
            return '';
208
        }
209
210
        $sql = (!$this->having) ? 'WHERE ' : 'HAVING ';
211
212
        return $sql.$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. ['identifier', 'BETWEEN', 'value', 'value']
225
     * 4. ['EXISTS', function(SelectQuery $query) {}]
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 (in_array($cond[0], ['EXISTS', 'NOT EXISTS'])) {
242
            return $this->buildExists($cond[1], $cond[0] == 'EXISTS');
243
        }
244
245
        // escape an identifier
246
        if (is_string($cond[0]) || !is_callable($cond[0])) {
247
            $cond[0] = $this->escapeIdentifier($cond[0]);
248
249
        // handle a subquery
250
        // NOTE string callables are not supported
251
        // as subquery functions
252
        } elseif (is_callable($cond[0])) {
253
            $cond[0] = $this->buildSubquery($cond[0]);
254
        }
255
256
        if (count($cond) === 1 || empty($cond[0])) {
257
            return $cond[0];
258
        }
259
260
        // handle BETWEEN conditions
261
        if (in_array($cond[1], ['BETWEEN', 'NOT BETWEEN'])) {
262
            return $this->buildBetween($cond[0], $cond[2], $cond[3], $cond[1] == 'BETWEEN');
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 array values, i.e. for IN conditions
271
        if (is_array($cond[2])) {
272
            $cond[2] = $this->parameterizeValues($cond[2]);
273
        // otherwise parameterize the value
274
        } else {
275
            $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...
276
        }
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 bool     $isExists
303
     * @param callable $f
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 $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
    /**
347
     * Implodes a list of WHERE clauses.
348
     *
349
     * @param array $clauses
350
     *
351
     * @return string
352
     */
353
    protected function implodeClauses(array $clauses)
354
    {
355
        $str = false;
356
        $or = false;
357
        foreach ($clauses as $clause) {
358
            // an 'OR' token will change the operator used
359
            // when concatenating the next clause
360
            if ($clause == 'OR') {
361
                $or = true;
362
                continue;
363
            }
364
365
            if (!$str) { // first clause needs no operator
366
                $str = $clause;
367
            } elseif ($or) {
368
                $str .= " OR $clause";
369
            } else {
370
                $str .= " AND $clause";
371
            }
372
373
            $or = false;
374
        }
375
376
        return $str;
377
    }
378
}
379