Completed
Push — work-fleets ( 961997...006942 )
by SuperNova.WS
06:22
created

DbQueryConstructor::selectRowToArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 5
ccs 0
cts 2
cp 0
crap 6
rs 9.4285
1
<?php
2
use Exception\ExceptionDBFieldEmpty;
3
use Exception\ExceptionDbOperationEmpty;
4
use Exception\ExceptionDbOperationRestricted;
5
6
/**
7
 * Class DbQueryConstructor
8
 *
9
 * @method DbQueryConstructor fields(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
10
 * @method DbQueryConstructor join(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
11
 * @method DbQueryConstructor where(array|string $value, int $mergeStrategy = HelperArray::MERGE_PHP)
12
 * @method DbQueryConstructor groupBy(array|string $value, int $mergeStrategy = HelperArray::MERGE_PHP)
13
 * @method DbQueryConstructor orderBy(array|string $value, int $mergeStrategy = HelperArray::MERGE_PHP)
14
 * @method DbQueryConstructor having(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP)
15
 * @method DbQueryConstructor setFetchOne(bool $fetchOne = true)
16
 * @method DbQueryConstructor setForUpdate(bool $forUpdate = true)
17
 * @method DbQueryConstructor setSkipLock(bool $skipLock = true)
18
 *
19
 */
20
class DbQueryConstructor extends DbSqlAware {
21
22
  const SELECT = 'SELECT';
23
  const INSERT = 'INSERT';
24
  const UPDATE = 'UPDATE';
25
  const DELETE = 'DELETE';
26
  const REPLACE = 'REPLACE';
27
28
  protected static $allowedOperations = array(
29
    self::SELECT,
30
  );
31
32
  /**
33
   * List of array properties names that should be merged on each call of setter
34
   *
35
   * @var string[] $propListArrayMerge
36
   */
37
  protected static $propListArrayMerge = array('fields', 'join', 'where', 'groupBy', 'orderBy', 'having');
38
  /**
39
   * List of setters that will simple set property with default value TRUE
40
   *
41
   * @var string[] $propListSetDefaultTrue
42
   */
43
  protected static $propListSetDefaultTrue = array('setFetchOne', 'setForUpdate', 'setSkipLock');
44
45
  public $operation = '';
46
47
  public $table = '';
48
  public $alias = '';
49
50
  public $idField = '';
51
52
  /**
53
   * @var array
54
   */
55
  public $fields = array();
56
57
  public $join = array();
58
59
  public $where = array();
60
  public $groupBy = array();
61
  public $orderBy = array();
62
  public $having = array();
63
64
  public $limit = 0;
65
  public $offset = 0;
66
67
  public $fetchOne = false;
68
  public $forUpdate = false;
69
  public $skipLock = false;
70
71
  public $variables = array();
72
73
  protected $_compiledQuery = array();
74
75
  /**
76
   * @param string $fieldName
77
   *
78
   * @return $this
79
   */
80 2
  public function setIdField($fieldName) {
81 2
    $this->idField = $fieldName;
82
83 2
    return $this;
84
  }
85
86
  /**
87
   * Sets internal variables
88
   *
89
   * @param array $values
90
   *
91
   * @return $this
92
   */
93
  public function variables($values) {
94
    $this->variables = $values;
95
96
    return $this;
97
  }
98
99
100
  /**
101
   * @param string $alias
102
   *
103
   * @return $this
104
   */
105 1
  public function setAlias($alias) {
106 1
    $this->alias = $alias;
107
108 1
    return $this;
109
  }
110
111
  /**
112
   * @return $this
113
   */
114 3
  public function select() {
115 3
    $this->operation = DbQueryConstructor::SELECT;
116
//    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...
117
//      $this->fields = array('*');
118
//    }
119
120 3
    return $this;
121
  }
122
123
124
  /**
125
   * @param string $tableName
126
   * @param string $alias
127
   *
128
   * @return $this
129
   */
130 2
  public function from($tableName, $alias = '') {
131 2
    $this->table = $tableName;
132 2
    $this->setAlias($alias);
133
134 2
    return $this;
135
  }
136
137
  /**
138
   * @param mixed ...
139
   *
140
   * @return $this
141
   */
142 1
  public function field() {
143 1
    $arguments = func_get_args();
144
145
    // Special case - call method with array of fields
146 1
    if (count($arguments) == 1 && is_array($arguments[0])) {
147
      $arguments = array_shift($arguments);
148
    }
149
150 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...
151 1
      $this->fields[] = $arg;
152 1
    }
153
154 1
    return $this;
155
  }
156
157
  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...
158
    return $this->field(DbSqlLiteral::build($this->db)->literal($field));
159
  }
160
161
  public function fieldSingleFunction($functionName, $field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
162
    return $this->field(DbSqlLiteral::build($this->db)->buildSingleArgument($functionName, $field, $alias));
163
  }
164
165
  public function fieldCount($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
166
    return $this->field(DbSqlLiteral::build($this->db)->count($field, $alias));
167
  }
168
169
  public function fieldIsNull($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
170
    return $this->field(DbSqlLiteral::build($this->db)->isNull($field, $alias));
171
  }
172
173
  public function fieldMax($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) {
174
    return $this->field(DbSqlLiteral::build($this->db)->max($field, $alias));
175
  }
176
177
  /**
178
   * @param string  $name
179
   * @param mixed[] $arguments
180
   *
181
   * @return $this
182
   */
183 4
  public function __call($name, $arguments) {
184 4
    if (in_array($name, self::$propListArrayMerge)) {
185
//      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...
186
//      $arguments[0] = &$this->$name;
187
//      call_user_func_array('HelperArray::merge', $arguments);
188 3
      HelperArray::merge($this->$name, $arguments[0], HelperArray::keyExistsOr($arguments, 1, HelperArray::MERGE_PHP));
189 4
    } elseif (in_array($name, self::$propListSetDefaultTrue)) {
190 2
      $varName = lcfirst(substr($name, 3));
191 2
      if (!array_key_exists(0, $arguments)) {
192 2
        $arguments[0] = true;
193 2
      }
194 2
      $this->$varName = $arguments[0];
195 2
    }
196
    // 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...
197
//    elseif(method_exists($this, $name)) {
198
//      call_user_func_array(array($this, $name), $arguments);
199
//    }
200
201 4
    return $this;
202
  }
203
204
  /**
205
   * @param int $limit
206
   *
207
   * @return $this
208
   */
209 2
  public function limit($limit) {
210 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...
211
212 2
    return $this;
213
  }
214
215
  /**
216
   * @param int $offset
217
   *
218
   * @return $this
219
   */
220 2
  public function offset($offset) {
221 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...
222
223 2
    return $this;
224
  }
225
226
  /**
227
   * @param string $className
228
   *
229
   * @return $this
230
   */
231
  // TODO - rewrite
232 2
  public function getParamsFromStaticClass($className) {
233 2
    if (is_string($className) && $className && class_exists($className)) {
234 2
      if(method_exists($className, 'getDb')) {
235
        $this->setDb($className::getDb());
236
      }
237 2
      $this->from($className::$_table);
238 2
      $this->setIdField($className::$_idField);
239 2
    }
240
241 2
    return $this;
242
  }
243
244
  /**
245
   * @param db_mysql|null $db
246
   * @param string|object|DbSqlAware $className
247
   *
248
   * @return static
249
   */
250 1
  public static function build($db = null, $className = '') {
251
    /**
252
     * @var static $result
253
     */
254 1
    $result = parent::build(null);
255 1
    $result->getParamsFromStaticClass($className);
256
257 1
    return $result;
258
  }
259
260
  /**
261
   * Resets statement
262
   *
263
   * @param bool $full
264
   *
265
   * @return static
266
   */
267 1
  protected function _reset($full = true) {
268 1
    if ($full) {
269 1
      $this->operation = '';
270 1
      $this->table = '';
271 1
      $this->alias = '';
272 1
      $this->idField = '';
273 1
    }
274
275 1
    $this->fields = array();
276 1
    $this->where = array();
277 1
    $this->groupBy = array();
278 1
    $this->orderBy = array();
279 1
    $this->having = array();
280
281 1
    $this->limit = 0;
282 1
    $this->offset = 0;
283
284 1
    $this->fetchOne = false;
285 1
    $this->forUpdate = false;
286 1
    $this->skipLock = false;
287
288 1
    return $this;
289
  }
290
291
292
  /**
293
   * @param array $array
294
   *
295
   * @return array
296
   */
297
  protected function arrayEscape(&$array) {
298
    $result = array();
299
    foreach ($array as $key => &$value) {
300
      $result[$key] = $this->escapeString($value);
301
    }
302
303
    return $result;
304
  }
305
306
  protected function compileOperation() {
307
    if (empty($this->operation)) {
308
      throw new ExceptionDbOperationEmpty();
309
    }
310
311
    if (!in_array($this->operation, self::$allowedOperations)) {
312
      throw new ExceptionDbOperationRestricted();
313
    }
314
315
    $this->_compiledQuery[] = $this->escapeString($this->operation);
316
  }
317
318
  protected function compileSubject() {
319
    $this->_compiledQuery[] = $this->selectFieldsToString($this->fields);
320
  }
321
322
  protected function compileFrom() {
323
    $this->_compiledQuery[] = 'FROM `{{' . $this->escapeString($this->table) . '}}`';
324
    if (!empty($this->alias)) {
325
      $this->_compiledQuery[] = 'AS `' . $this->escapeString($this->alias) . '`';
326
    }
327
  }
328
329
  protected function compileJoin() {
330
    !empty($this->join) ? $this->_compiledQuery[] = implode(' ', $this->join) : false;
331
  }
332
333
  protected function compileWhere() {
334
    // TODO - fields should be escaped !!
335
    !empty($this->where) ? $this->_compiledQuery[] = 'WHERE ' . implode(' AND ', $this->where) : false;
336
  }
337
338
  protected function compileGroupBy() {
339
    // 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...
340
//    !empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . implode(',', $this->arrayEscape($this->groupBy)) : false;
341
    !empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . $this->selectFieldsToString($this->groupBy) : false;
342
  }
343
344
  protected function compileOrderBy() {
345
    // TODO - fields should be escaped !!
346
    !empty($this->orderBy) ? $this->_compiledQuery[] = 'ORDER BY ' . implode(',', $this->arrayEscape($this->orderBy)) : false;
347
  }
348
349
  protected function compileHaving() {
350
    // TODO - fields should be escaped !!
351
    !empty($this->having) ? $this->_compiledQuery[] = 'HAVING ' . implode(' AND ', $this->having) : false;
352
  }
353
354
  protected function compileLimit() {
355
    // TODO - fields should be escaped !!
356
    if ($limit = $this->fetchOne ? 1 : $this->limit) {
357
      $this->_compiledQuery[] = 'LIMIT ' . $limit . (!empty($this->offset) ? ' OFFSET ' . $this->offset : '');
358
    }
359
  }
360
361
  protected function compileForUpdate() {
362
    $this->_compiledQuery[] =
363
      // forUpdate flag forces select with row locking - didn't look at skipLock flag
364
      $this->forUpdate
365
      ||
366
      // Also row locked when transaction is up and skipLock flag is not set
367
      ($this->db->getTransaction()->check(false) && !$this->skipLock) ? 'FOR UPDATE' : '';
368
  }
369
370
  /**
371
   * @param array|mixed $fields
372
   *
373
   * @return string
374
   * @throws ExceptionDBFieldEmpty
375
   */
376 16
  protected function selectFieldsToString($fields) {
377 16
    HelperArray::makeArrayRef($fields);
378
379 16
    $result = array();
380 16
    foreach ($fields as $fieldName) {
381 15
      $string = $this->processField($fieldName);
382 15
      if ($string !== '') {
383 13
        $result[] = $string;
384 13
      }
385 16
    }
386
387 16
    if (empty($result)) {
388 3
      throw new ExceptionDBFieldEmpty();
389
    }
390
391 13
    return implode(',', $result);
392
  }
393
394
  /**
395
   * @param mixed $fieldName
396
   *
397
   * @return string
398
   */
399 13
  protected function processField($fieldName) {
400 13
    if (is_bool($fieldName)) {
401 4
      $result = (string)intval($fieldName);
402 13
    } elseif (is_numeric($fieldName)) {
403 5
      $result = $fieldName;
404 11
    } elseif (is_null($fieldName)) {
405 2
      $result = 'NULL';
406 2
    } else {
407
      // Field has other type - string or should be convertible to string
408 6
      $result = (string)$fieldName;
409 6
      if (!$fieldName instanceof DbSqlLiteral) {
410 5
        $result = $this->quoteField($fieldName);
0 ignored issues
show
Bug introduced by
It seems like $fieldName defined by parameter $fieldName on line 399 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...
411 5
      }
412
    }
413
414 13
    return $result;
415
  }
416
417
418
  /**
419
   * @return string
420
   * @throws ExceptionDbOperationEmpty
421
   * @throws ExceptionDbOperationRestricted
422
   */
423 2
  public function __toString() {
424 2
    $this->_compiledQuery = array();
425
426 2
    $this->compileOperation();
427
    $this->compileSubject();
428
    $this->compileFrom();
429
    $this->compileJoin();
430
    $this->compileWhere();
431
    $this->compileGroupBy();
432
    $this->compileOrderBy();
433
    $this->compileHaving();
434
    $this->compileLimit();
435
    $this->compileForUpdate();
436
437
    return implode(' ', $this->_compiledQuery);
438
  }
439
440
  /**
441
   * @return DbEmptyIterator|DbMysqliResultIterator
442
   */
443
  public function selectIterator() {
444
    $result = $this->getDb()->getOperator()->doSelectIterator($this->select()->__toString());
445
446
    return $result;
447
  }
448
449
  /**
450
   * @return array
451
   */
452
  public function selectRowToArray() {
453
    $result = $this->getDb()->doSelectFetchArray($this->select()->setFetchOne()->__toString());
454
455
    return is_array($result) ? $result : array();
456
  }
457
458
  /**
459
   * @return mixed|null
460
   */
461
  public function selectValue() {
462
    $result = $this->selectRowToArray();
463
464
    return is_array($result) ? reset($result) : null;
465
  }
466
467
}
468