Passed
Branch work-fleets (125b09)
by Sabine
07:19
created

DbQueryConstructor::compileLimit()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 3
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 6
ccs 0
cts 5
cp 0
crap 20
rs 9.2
1
<?php
2
3
/**
4
 * Class DbQueryConstructor
5
 *
6
 * @method DbQueryConstructor fields(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
7
 * @method DbQueryConstructor join(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
8
 * @method DbQueryConstructor where(array $value, int $mergeStrategy = HelperArray::MERGE_PHP)
9
 * @method DbQueryConstructor groupBy(array $value, int $mergeStrategy = HelperArray::MERGE_PHP)
10
 * @method DbQueryConstructor orderBy(array $value, int $mergeStrategy = HelperArray::MERGE_PHP)
11
 * @method DbQueryConstructor having(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
12
 * @method DbQueryConstructor setFetchOne(bool $fetchOne = true)
13
 * @method DbQueryConstructor setForUpdate(bool $forUpdate = true)
14
 * @method DbQueryConstructor setSkipLock(bool $skipLock = true)
15
 *
16
 */
17
class DbQueryConstructor extends DbSqlAware {
18
19
  const SELECT = 'SELECT';
20
  const INSERT = 'INSERT';
21
  const UPDATE = 'UPDATE';
22
  const DELETE = 'DELETE';
23
  const REPLACE = 'REPLACE';
24
25
  protected static $allowedOperations = array(
26
    self::SELECT,
27
  );
28
29
  /**
30
   * List of array properties names that should be merged on each call of setter
31
   *
32
   * @var string[] $propListArrayMerge
33
   */
34
  protected static $propListArrayMerge = array('fields', 'join', 'where', 'groupBy', 'orderBy', 'having');
35
  /**
36
   * List of setters that will simple set property with default value TRUE
37
   *
38
   * @var string[] $propListSetDefaultTrue
39
   */
40
  protected static $propListSetDefaultTrue = array('setFetchOne', 'setForUpdate', 'setSkipLock');
41
42
  public $operation = '';
43
44
  public $table = '';
45
  public $alias = '';
46
47
  public $idField = '';
48
49
  /**
50
   * @var array
51
   */
52
  public $fields = array();
53
54
  public $join = array();
55
56
  public $where = array();
57
  public $groupBy = array();
58
  public $orderBy = array();
59
  public $having = array();
60
61
  public $limit = 0;
62
  public $offset = 0;
63
64
  public $fetchOne = false;
65
  public $forUpdate = false;
66
  public $skipLock = false;
67
68
  public $variables = array();
69
70
  protected $_compiledQuery = array();
71
72
  /**
73
   * @param string $fieldName
74
   *
75
   * @return $this
76
   */
77 2
  public function setIdField($fieldName) {
78 2
    $this->idField = $fieldName;
79
80 2
    return $this;
81
  }
82
83
  /**
84
   * Sets internal variables
85
   *
86
   * @param array $values
87
   *
88
   * @return $this
89
   */
90
  public function variables($values) {
91
    $this->variables = $values;
92
93
    return $this;
94
  }
95
96
97
  /**
98
   * @param string $alias
99
   *
100
   * @return $this
101
   */
102 1
  public function setAlias($alias) {
103 1
    $this->alias = $alias;
104
105 1
    return $this;
106
  }
107
108
  /**
109
   * @return $this
110
   */
111 3
  public function select() {
112 3
    $this->operation = DbQueryConstructor::SELECT;
113
//    if (empty($this->fields) && $initFields) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
114
//      $this->fields = array('*');
115
//    }
116
117 3
    return $this;
118
  }
119
120
121
  /**
122
   * @param string $tableName
123
   * @param string $alias
124
   *
125
   * @return $this
126
   */
127 2
  public function from($tableName, $alias = '') {
128 2
    $this->table = $tableName;
129 2
    $this->setAlias($alias);
130
131 2
    return $this;
132
  }
133
134
  /**
135
   * @param mixed ...
136
   *
137
   * @return $this
138
   */
139 1
  public function field() {
140 1
    $arguments = func_get_args();
141
142
    // Special case - call method with array of fields
143 1
    if (count($arguments) == 1 && is_array($arguments[0])) {
144
      $arguments = array_shift($arguments);
145
    }
146
147 1
    foreach ($arguments as $arg) {
0 ignored issues
show
Bug introduced by
The expression $arguments of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
148 1
      $this->fields[] = $arg;
149 1
    }
150
151 1
    return $this;
152
  }
153
154
  public function fieldLiteral($field = '0', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
0 ignored issues
show
Unused Code introduced by
The parameter $alias 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...
155
    return $this->field(DbSqlLiteral::build($this->db)->literal($field));
156
  }
157
158
  public function fieldSingleFunction($functionName, $field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
159
    return $this->field(DbSqlLiteral::build($this->db)->buildSingleArgument($functionName, $field, $alias));
160
  }
161
162
  public function fieldCount($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
163
    return $this->field(DbSqlLiteral::build($this->db)->count($field, $alias));
164
  }
165
166
  public function fieldIsNull($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
167
    return $this->field(DbSqlLiteral::build($this->db)->isNull($field, $alias));
168
  }
169
170
  public function fieldMax($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
171
    return $this->field(DbSqlLiteral::build($this->db)->max($field, $alias));
172
  }
173
174
  /**
175
   * @param string  $name
176
   * @param mixed[] $arguments
177
   *
178
   * @return $this
179
   */
180 4
  public function __call($name, $arguments) {
181 4
    if (in_array($name, self::$propListArrayMerge)) {
182
//      array_unshift($arguments, '');
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
183
//      $arguments[0] = &$this->$name;
184
//      call_user_func_array('HelperArray::merge', $arguments);
185 3
      HelperArray::merge($this->$name, $arguments[0], HelperArray::keyExistsOr($arguments, 1, HelperArray::MERGE_PHP));
186 4
    } elseif (in_array($name, self::$propListSetDefaultTrue)) {
187 2
      $varName = lcfirst(substr($name, 3));
188 2
      if (!array_key_exists(0, $arguments)) {
189 2
        $arguments[0] = true;
190 2
      }
191 2
      $this->$varName = $arguments[0];
192 2
    }
193
    // TODO - make all setters protected ??
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
194
//    elseif(method_exists($this, $name)) {
195
//      call_user_func_array(array($this, $name), $arguments);
196
//    }
197
198 4
    return $this;
199
  }
200
201
  /**
202
   * @param int $limit
203
   *
204
   * @return $this
205
   */
206 2
  public function limit($limit) {
207 2
    $this->limit = is_numeric($limit) ? $limit : 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_numeric($limit) ? $limit : 0 can also be of type double or string. However, the property $limit is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
208
209 2
    return $this;
210
  }
211
212
  /**
213
   * @param int $offset
214
   *
215
   * @return $this
216
   */
217 2
  public function offset($offset) {
218 2
    $this->offset = is_numeric($offset) ? $offset : 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_numeric($offset) ? $offset : 0 can also be of type double or string. However, the property $offset is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
219
220 2
    return $this;
221
  }
222
223
  /**
224
   * @param string $className
225
   *
226
   * @return $this
227
   */
228
  // TODO - rewrite
229 2
  public function getParamsFromStaticClass($className) {
230 2
    if (is_string($className) && $className && class_exists($className)) {
231 2
      if(method_exists($className, 'getDb')) {
232 2
        $this->setDb($className::getDb());
233 2
      }
234 2
      $this->from($className::$_table);
235 2
      $this->setIdField($className::$_idField);
236 2
    }
237
238 2
    return $this;
239
  }
240
241
  /**
242
   * @param db_mysql|null $db
243
   * @param string|object|DBStaticRecord|DbSqlAware $className
244
   *
245
   * @return static
246
   */
247 1
  public static function build($db = null, $className = '') {
248
    /**
249
     * @var static $result
250
     */
251 1
    $result = parent::build(null);
252 1
    $result->getParamsFromStaticClass($className);
253
254 1
    return $result;
255
  }
256
257
  /**
258
   * Resets statement
259
   *
260
   * @param bool $full
261
   *
262
   * @return static
263
   */
264 1
  protected function _reset($full = true) {
265 1
    if ($full) {
266 1
      $this->operation = '';
267 1
      $this->table = '';
268 1
      $this->alias = '';
269 1
      $this->idField = '';
270 1
    }
271
272 1
    $this->fields = array();
273 1
    $this->where = array();
274 1
    $this->groupBy = array();
275 1
    $this->orderBy = array();
276 1
    $this->having = array();
277
278 1
    $this->limit = 0;
279 1
    $this->offset = 0;
280
281 1
    $this->fetchOne = false;
282 1
    $this->forUpdate = false;
283 1
    $this->skipLock = false;
284
285 1
    return $this;
286
  }
287
288
289
  /**
290
   * @param array $array
291
   *
292
   * @return array
293
   */
294
  protected function arrayEscape(&$array) {
295
    $result = array();
296
    foreach ($array as $key => &$value) {
297
      $result[$key] = $this->escapeString($value);
298
    }
299
300
    return $result;
301
  }
302
303
  protected function compileOperation() {
304
    if (empty($this->operation)) {
305
      throw new ExceptionDbOperationEmpty();
306
    }
307
308
    if (!in_array($this->operation, self::$allowedOperations)) {
309
      throw new ExceptionDbOperationRestricted();
310
    }
311
312
    $this->_compiledQuery[] = $this->escapeString($this->operation);
313
  }
314
315
  protected function compileSubject() {
316
    $this->_compiledQuery[] = $this->selectFieldsToString($this->fields);
317
  }
318
319
  protected function compileFrom() {
320
    $this->_compiledQuery[] = 'FROM `{{' . $this->escapeString($this->table) . '}}`';
321
    if (!empty($this->alias)) {
322
      $this->_compiledQuery[] = 'AS `' . $this->escapeString($this->alias) . '`';
323
    }
324
  }
325
326
  protected function compileJoin() {
327
    !empty($this->join) ? $this->_compiledQuery[] = implode(' ', $this->join) : false;
328
  }
329
330
  protected function compileWhere() {
331
    // TODO - fields should be escaped !!
332
    !empty($this->where) ? $this->_compiledQuery[] = 'WHERE ' . implode(' AND ', $this->where) : false;
333
  }
334
335
  protected function compileGroupBy() {
336
    // TODO - fields should be escaped !!
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
337
//    !empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . implode(',', $this->arrayEscape($this->groupBy)) : false;
338
    !empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . $this->selectFieldsToString($this->groupBy) : false;
339
  }
340
341
  protected function compileOrderBy() {
342
    // TODO - fields should be escaped !!
343
    !empty($this->orderBy) ? $this->_compiledQuery[] = 'ORDER BY ' . implode(',', $this->arrayEscape($this->orderBy)) : false;
344
  }
345
346
  protected function compileHaving() {
347
    // TODO - fields should be escaped !!
348
    !empty($this->having) ? $this->_compiledQuery[] = 'HAVING ' . implode(' AND ', $this->having) : false;
349
  }
350
351
  protected function compileLimit() {
352
    // TODO - fields should be escaped !!
353
    if ($limit = $this->fetchOne ? 1 : $this->limit) {
354
      $this->_compiledQuery[] = 'LIMIT ' . $limit . (!empty($this->offset) ? ' OFFSET ' . $this->offset : '');
355
    }
356
  }
357
358
  protected function compileForUpdate() {
359
    $this->_compiledQuery[] =
360
      // forUpdate flag forces select with row locking - didn't look at skipLock flag
361
      $this->forUpdate
362
      ||
363
      // Also row locked when transaction is up and skipLock flag is not set
364
      (classSupernova::db_transaction_check(false) && !$this->skipLock) ? 'FOR UPDATE' : '';
365
  }
366
367
  /**
368
   * @param array|mixed $fields
369
   *
370
   * @return string
371
   * @throws ExceptionDBFieldEmpty
372
   */
373 16
  protected function selectFieldsToString($fields) {
374 16
    HelperArray::makeArrayRef($fields);
375
376 16
    $result = array();
377 16
    foreach ($fields as $fieldName) {
378 15
      $string = $this->processField($fieldName);
379 15
      if ($string !== '') {
380 13
        $result[] = $string;
381 13
      }
382 16
    }
383
384 16
    if (empty($result)) {
385 3
      throw new ExceptionDBFieldEmpty();
386
    }
387
388 13
    return implode(',', $result);
389
  }
390
391
  /**
392
   * @param mixed $fieldName
393
   *
394
   * @return string
395
   */
396 13
  protected function processField($fieldName) {
397 13
    if (is_bool($fieldName)) {
398 4
      $result = (string)intval($fieldName);
399 13
    } elseif (is_numeric($fieldName)) {
400 5
      $result = $fieldName;
401 11
    } elseif (is_null($fieldName)) {
402 2
      $result = 'NULL';
403 2
    } else {
404
      // Field has other type - string or should be convertible to string
405 6
      $result = (string)$fieldName;
406 6
      if (!$fieldName instanceof DbSqlLiteral) {
407 5
        $result = $this->quoteField($fieldName);
0 ignored issues
show
Bug introduced by
It seems like $fieldName defined by parameter $fieldName on line 396 can also be of type array or object; however, DbSqlAware::quoteField() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
408 5
      }
409
    }
410
411 13
    return $result;
412
  }
413
414
415
  /**
416
   * @return string
417
   * @throws ExceptionDbOperationEmpty
418
   * @throws ExceptionDbOperationRestricted
419
   */
420 2
  public function __toString() {
421 2
    $this->_compiledQuery = array();
422
423 2
    $this->compileOperation();
424
    $this->compileSubject();
425
    $this->compileFrom();
426
    $this->compileJoin();
427
    $this->compileWhere();
428
    $this->compileGroupBy();
429
    $this->compileOrderBy();
430
    $this->compileHaving();
431
    $this->compileLimit();
432
    $this->compileForUpdate();
433
434
    return implode(' ', $this->_compiledQuery);
435
  }
436
437
  /**
438
   * @param bool $skip_query_check
439
   *
440
   * @return DbEmptyIterator|DbMysqliResultIterator
441
   */
442
  public function selectIterator($skip_query_check = false) {
443
    return $this->getDb()->doQueryIterator($this->select()->__toString(), $skip_query_check);
444
  }
445
446
  /**
447
   * @param bool $skip_query_check
448
   *
449
   * @return array
450
   */
451
  public function selectRow($skip_query_check = false) {
452
    $result = $this->getDb()->doQueryFetch($this->select()->setFetchOne()->__toString(), $skip_query_check);
453
454
    return is_array($result) ? $result : array();
455
  }
456
457
  /**
458
   * @param bool $skip_query_check
459
   *
460
   * @return mixed|null
461
   */
462
  public function selectValue($skip_query_check = false) {
463
    $result = $this->selectRow($skip_query_check);
464
465
    return is_array($result) ? reset($result) : null;
466
  }
467
468
}
469