WhereStatement   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 374
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 3
dl 0
loc 374
rs 8.5599
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A isHaving() 0 4 1
B addCondition() 0 32 7
A addOrCondition() 0 6 1
A addBetweenCondition() 0 6 1
A addNotBetweenCondition() 0 6 1
A addExistsCondition() 0 6 1
A addNotExistsCondition() 0 6 1
A getConditions() 0 4 1
A build() 0 20 4
C buildClause() 0 47 15
A buildSubquery() 0 10 1
A buildExists() 0 6 2
A buildBetween() 0 6 2
A buildNull() 0 6 2
A buildIn() 0 6 2
A implodeClauses() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like WhereStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WhereStatement, and based on these observations, apply Extract Interface, too.

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