Completed
Push — master ( 4e98a1...a4a40b )
by Lars
05:38 queued 10s
created

ActiveRecord::update()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0466

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 12
cts 14
cp 0.8571
rs 9.456
c 0
b 0
f 0
cc 4
nc 5
nop 0
crap 4.0466
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Arrayy\Arrayy;
8
use voku\db\exceptions\ActiveRecordException;
9
use voku\db\exceptions\FetchingException;
10
11
/**
12
 * A simple implement of active record via Arrayy.
13
 *
14
 * @method $this select(string $dbProperty)
15
 * @method $this eq(string $dbProperty, string | int | null $value = null)
16
 * @method $this from(string $table)
17
 * @method $this where(string $where)
18
 * @method $this having(string $having)
19
 * @method $this limit(int $start, int | null $end = null)
20
 *
21
 * @method $this equal(string $dbProperty, string $value)
22
 * @method $this notEqual(string $dbProperty, string $value)
23
 * @method $this ne(string $dbProperty, string $value)
24
 * @method $this greaterThan(string $dbProperty, int $value)
25
 * @method $this gt(string $dbProperty, int $value)
26
 * @method $this lessThan(string $dbProperty, int $value)
27
 * @method $this lt(string $dbProperty, int $value)
28
 * @method $this greaterThanOrEqual(string $dbProperty, int $value)
29
 * @method $this ge(string $dbProperty, int $value)
30
 * @method $this gte(string $dbProperty, int $value)
31
 * @method $this lessThanOrEqual(string $dbProperty, int $value)
32
 * @method $this le(string $dbProperty, int $value)
33
 * @method $this lte(string $dbProperty, int $value)
34
 * @method $this between(string $dbProperty, array $value)
35
 * @method $this like(string $dbProperty, string $value)
36
 * @method $this in(string $dbProperty, array $value)
37
 * @method $this notIn(string $dbProperty, array $value)
38
 * @method $this isnull(string $dbProperty)
39
 * @method $this isNotNull(string $dbProperty)
40
 * @method $this notNull(string $dbProperty)
41
 */
42
abstract class ActiveRecord extends Arrayy
43
{
44
  const BELONGS_TO = 'belongs_to';
45
  const HAS_MANY   = 'has_many';
46
  const HAS_ONE    = 'has_one';
47
48
  const PREFIX = ':active_record';
49
50
  /**
51
   * @var array <p>Mapping the function name and the operator, to build Expressions in WHERE condition.</p>
52
   *
53
   * call the function like this:
54
   * <pre>
55
   *   $user->isNotNull()->eq('id', 1);
56
   * </pre>
57
   *
58
   * the result in SQL:
59
   * <pre>
60
   *   WHERE user.id IS NOT NULL AND user.id = :ph1
61
   * </pre>
62
   */
63
  protected static $operators = [
64
      'equal'              => '=',
65
      'eq'                 => '=',
66
      'notequal'           => '<>',
67
      'ne'                 => '<>',
68
      'greaterthan'        => '>',
69
      'gt'                 => '>',
70
      'lessthan'           => '<',
71
      'lt'                 => '<',
72
      'greaterthanorequal' => '>=',
73
      'ge'                 => '>=',
74
      'gte'                => '>=',
75
      'lessthanorequal'    => '<=',
76
      'le'                 => '<=',
77
      'lte'                => '<=',
78
      'between'            => 'BETWEEN',
79
      'like'               => 'LIKE',
80
      'in'                 => 'IN',
81
      'notin'              => 'NOT IN',
82
      'isnull'             => 'IS NULL',
83
      'isnotnull'          => 'IS NOT NULL',
84
      'notnull'            => 'IS NOT NULL',
85
  ];
86
87
  /**
88
   * @var int <p>The count of bind params, using this count and const "PREFIX" (:ph) to generate place holder in
89
   *      SQL.</p>
90
   */
91
  private static $count = 0;
92
93
  /**
94
   * @var DB
95
   */
96
  protected $db;
97
98
  /**
99
   * @var array <p>Part of the SQL, mapping the function name and the operator to build SQL Part.</p>
100
   *
101
   * <br />
102
   *
103
   * call the function like this:
104
   * <pre>
105
   *      $user->orderBy('id DESC', 'name ASC')->limit(2, 1);
106
   * </pre>
107
   *
108
   * the result in SQL:
109
   * <pre>
110
   *      ORDER BY id DESC, name ASC LIMIT 2,1
111
   * </pre>
112
   */
113
  protected $sqlParts = [
114
      'select' => 'SELECT',
115
      'from'   => 'FROM',
116
      'set'    => 'SET',
117
      'where'  => 'WHERE',
118
      'group'  => 'GROUP BY',
119
      'having' => 'HAVING',
120
      'order'  => 'ORDER BY',
121
      'limit'  => 'LIMIT',
122
      'top'    => 'TOP',
123
  ];
124
125
  /**
126
   * @var array <p>The default sql expressions values.</p>
127
   */
128
  protected $defaultSqlExpressions = [
129
      'expressions' => [],
130
      'wrap'        => false,
131
      'select'      => null,
132
      'insert'      => null,
133
      'update'      => null,
134
      'set'         => null,
135
      'delete'      => 'DELETE ',
136
      'join'        => null,
137
      'from'        => null,
138
      'values'      => null,
139
      'where'       => null,
140
      'having'      => null,
141
      'limit'       => null,
142
      'order'       => null,
143
      'group'       => null,
144
  ];
145
146
  /**
147
   * @var array <p>Stored the Expressions of the SQL.</p>
148
   */
149
  protected $sqlExpressions = [];
150
151
  /**
152
   * @var string <p>The table name in database.</p>
153
   */
154
  protected $table;
155
156
  /**
157
   * @var string  <p>The primary key of this ActiveRecord, just support single primary key.</p>
158
   */
159
  protected $primaryKeyName = 'id';
160
161
  /**
162
   * @var array <p>Stored the dirty data of this object, when call "insert" or "update" function, will write this data
163
   *      into database.</p>
164
   */
165
  protected $dirty = [];
166
167
  /**
168
   * @var bool
169
   */
170
  protected $new_data_are_dirty = true;
171
172
  /**
173
   * @var array <p>Stored the params will bind to SQL when call DB->query().</p>
174
   */
175
  protected $params = [];
176
177
  /**
178
   * @var ActiveRecordExpressions[] <p>Stored the configure of the relation, or target of the relation.</p>
179
   */
180
  protected $relations = [];
181
182
  /**
183
   * Magic function to make calls witch in function mapping stored in $operators and $sqlPart.
184
   * also can call function of DB object.
185
   *
186
   * @param string $name <p>The name of the function.</p>
187
   * @param array  $args <p>The arguments of the function.</p>
188
   *
189
   * @return $this|mixed <p>Return the result of callback or the current object to make chain method calls.</p>
190
   *
191
   * @throws ActiveRecordException
192
   */
193 16
  public function __call(string $name, array $args = [])
194
  {
195 16
    if (!$this->db instanceof DB) {
196 14
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
197
    }
198
199 16
    $nameTmp = \strtolower($name);
200
201 16
    if (\array_key_exists($nameTmp, self::$operators)) {
202
203 14
      $this->addCondition(
204 14
          $args[0],
205 14
          self::$operators[$nameTmp],
206 14
          $args[1] ?? null,
207 14
          (\is_string(\end($args)) && 'or' === \strtolower(\end($args))) ? 'OR' : 'AND'
208
      );
209
210 11
    } elseif (\array_key_exists($nameTmp = \str_replace('by', '', $nameTmp), $this->sqlParts)) {
211
212 11
      $this->{$name} = new ActiveRecordExpressions(
213
          [
214 11
              'operator' => $this->sqlParts[$nameTmp],
215 11
              'target'   => \implode(', ', $args),
216
          ]
217
      );
218
219
    } elseif (\is_callable($callback = [$this->db, $name])) {
220
221
      return \call_user_func_array($callback, $args);
222
223
    } else {
224
225
      throw new ActiveRecordException("Method $name not exist.");
226
227
    }
228
229 16
    return $this;
230
  }
231
232
  /**
233
   * Magic function to GET the values of current object.
234
   *
235
   * @param mixed $var
236
   *
237
   * @return mixed
238
   */
239 23
  public function &__get($var)
240
  {
241 23
    if (\array_key_exists($var, $this->sqlExpressions)) {
242 20
      return $this->sqlExpressions[$var];
243
    }
244
245 23
    if (\array_key_exists($var, $this->relations)) {
246 3
      return $this->getRelation($var);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getRelation($var); of type voku\db\ActiveRecordExpr...|voku\db\ActiveRecord[] adds the type voku\db\ActiveRecord[] to the return on line 246 which is incompatible with the return type of the parent method Arrayy\Arrayy::__get of type object|integer|double|string|null|boolean.
Loading history...
247
    }
248
249 23
    if (isset($this->dirty[$var])) {
250 10
      return $this->dirty[$var];
251
    }
252
253 23
    return parent::__get($var);
254
  }
255
256
  /**
257
   * Magic function to SET values of the current object.
258
   *
259
   * @param string $var
260
   * @param mixed  $val
261
   */
262 23
  public function __set($var, $val)
263
  {
264
    if (
265 23
        \array_key_exists($var, $this->sqlExpressions)
266
        ||
267 23
        \array_key_exists($var, $this->defaultSqlExpressions)
268
    ) {
269
270 20
      $this->sqlExpressions[$var] = $val;
271
272
    } elseif (
273 20
        \array_key_exists($var, $this->relations)
274
        &&
275 20
        $val instanceof self
276
    ) {
277
278 1
      $this->relations[$var] = $val;
279
280
    } else {
281
282 20
      $this->set($var, $val);
283
284 20
      if ($this->new_data_are_dirty === true) {
285 15
        $this->dirty[$var] = $val;
286
      }
287
288
    }
289 23
  }
290
291
  /**
292
   * Magic function to UNSET values of the current object.
293
   *
294
   * @param mixed $var
295
   */
296 1
  public function __unset($var)
297
  {
298 1
    if (\array_key_exists($var, $this->sqlExpressions)) {
299
      unset($this->sqlExpressions[$var]);
300
    }
301
302 1
    if (isset($this->array[$var])) {
303 1
      unset($this->array[$var]);
304
    }
305
306 1
    if (isset($this->dirty[$var])) {
307 1
      unset($this->dirty[$var]);
308
    }
309 1
  }
310
311
  /**
312
   * Get a value from an array (optional using dot-notation).
313
   *
314
   * @param string $key       <p>The key to look for.</p>
315
   * @param mixed  $fallback  <p>Value to fallback to.</p>
316
   * @param array  $array     <p>The array to get from, if it's set to "null" we use the current array from the
317
   *                         class.</p>
318
   *
319
   * @return mixed
320
   */
321 23
  public function get($key, $fallback = null, array $array = null)
322
  {
323 23
    return parent::get($key, $fallback, $array);
324
  }
325
326
  /**
327
   * helper function to add condition into WHERE.
328
   *
329
   * @param ActiveRecordExpressions $expression <p>The expression will be concat into WHERE or SET statement.</p>
330
   * @param string                  $operator   <p>The operator to concat this Expressions into WHERE or SET
331
   *                                            statement.</p>
332
   * @param string                  $name       <p>The Expression will contact to.</p>
333
   */
334 14
  protected function _addCondition(ActiveRecordExpressions $expression, string $operator, string $name = 'where')
335
  {
336 14
    if (!$this->{$name}) {
337
338 14
      $this->{$name} = new ActiveRecordExpressions(
339
          [
340 14
              'operator' => \strtoupper($name),
341 14
              'target'   => $expression,
342
          ]
343
      );
344
345
    } else {
346
347 4
      $this->{$name}->target = new ActiveRecordExpressions(
348
          [
349 4
              'source'   => $this->{$name}->target,
350 4
              'operator' => $operator,
351 4
              'target'   => $expression,
352
          ]
353
      );
354
355
    }
356 14
  }
357
358
  /**
359
   * helper function to make wrapper. Stored the expression in to array.
360
   *
361
   * @param ActiveRecordExpressions $exp      <p>The expression will be stored.</p>
362
   * @param string                  $operator <p>The operator to concat this Expressions into WHERE statement.</p>
363
   */
364 1
  protected function _addExpression(ActiveRecordExpressions $exp, string $operator)
365
  {
366
    if (
367 1
        !\is_array($this->expressions)
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
368
        ||
369 1
        \count($this->expressions) === 0
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
370
    ) {
371 1
      $this->expressions = [$exp];
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
372
    } else {
373 1
      $this->expressions[] = new ActiveRecordExpressions(
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
374
          [
375 1
              'operator' => $operator,
376 1
              'target'   => $exp,
377
          ]
378
      );
379
    }
380 1
  }
381
382
  /**
383
   * Helper function to build SQL with sql parts.
384
   *
385
   * @param string[] $sql_array <p>The SQL part will be build.</p>
386
   *
387
   * @return string
388
   */
389 21
  protected function _buildSql(array $sql_array = []): string
390
  {
391 21
    \array_walk($sql_array, [$this, '_buildSqlCallback'], $this);
392
393
    // DEBUG
394
    //echo 'SQL: ', implode(' ', $sql_array), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
395
396 21
    return \implode(' ', $sql_array);
397
  }
398
399
  /**
400
   * Helper function to build SQL with sql parts.
401
   *
402
   * @param string $sql_string_part <p>The SQL part will be build.</p>
403
   * @param int    $index           <p>The index of $n in $sql array.</p>
404
   * @param self   $active_record   <p>The reference to $this.</p>
405
   */
406 21
  private function _buildSqlCallback(string &$sql_string_part, int $index, self $active_record)
407
  {
408
    if (
409 21
        'select' === $sql_string_part
410
        &&
411 21
        null === $active_record->{$sql_string_part}
412
    ) {
413
414 14
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table . '.*';
415
416
    } elseif (
417
        (
418 21
            'update' === $sql_string_part
419
            ||
420 21
            'from' === $sql_string_part
421
        )
422
        &&
423 21
        null === $active_record->{$sql_string_part}
424
    ) {
425
426 17
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table;
427
428 21
    } elseif ('delete' === $sql_string_part) {
429
430 1
      $sql_string_part = \strtoupper($sql_string_part) . ' ';
431
432
    } else {
433
434 21
      $sql_string_part = (null !== $active_record->{$sql_string_part}) ? $active_record->{$sql_string_part} . ' ' : '';
435
436
    }
437 21
  }
438
439
  /**
440
   * Helper function to build place holder when make SQL expressions.
441
   *
442
   * @param mixed $value <p>The value will be bind to SQL, just store it in $this->params.</p>
443
   *
444
   * @return mixed $value
445
   */
446 18
  protected function _filterParam($value)
447
  {
448 18
    if (\is_array($value)) {
449 8
      foreach ($value as $key => $val) {
450 8
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
451
      }
452 11
    } elseif (\is_string($value)) {
453 3
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
454 3
      $value = $ph;
455
    }
456
457 18
    return $value;
458
  }
459
460
  /**
461
   * Helper function to add condition into WHERE.
462
   *
463
   * @param string $field           <p>The field name, the source of Expressions</p>
464
   * @param string $operator        <p>The operator for this condition.</p>
465
   * @param mixed  $value           <p>The target of the Expressions.</p>
466
   * @param string $operator_concat <p>The operator to concat this Expressions into WHERE or SET statement.</p>
467
   * @param string $name            <p>The Expression will contact to.</p>
468
   */
469 14
  public function addCondition(string $field, string $operator, $value, string $operator_concat = 'AND', string $name = 'where')
470
  {
471 14
    $value = $this->_filterParam($value);
472 14
    $expression = new ActiveRecordExpressions(
473
        [
474 14
            'source'   => ('where' === strtolower($name) ? $this->table . '.' : '') . $field,
475 14
            'operator' => $operator,
476 14
            'target'   => \is_array($value)
477 4
                ? new ActiveRecordExpressionsWrap(
478 4
                    'between' === \strtolower($operator)
479 1
                        ? ['target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ']
480 4
                        : ['target' => $value]
481 14
                ) : $value,
482
        ]
483
    );
484
485 14
    if ($expression) {
486 14
      if (!$this->wrap) {
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
487 14
        $this->_addCondition($expression, $operator_concat, $name);
488
      } else {
489 1
        $this->_addExpression($expression, $operator_concat);
490
      }
491
    }
492 14
  }
493
494
  /**
495
   * Helper function to copy an existing active record (and insert it into the database).
496
   *
497
   * @param bool $insert
498
   *
499
   * @return $this
500
   */
501 1
  public function copy(bool $insert = true): self
502
  {
503 1
    $new = clone $this;
504
505 1
    if ($insert) {
506 1
      $new->setPrimaryKey(null);
507 1
      $id = $new->insert();
508 1
      $new->setPrimaryKey($id);
509
    }
510
511 1
    return $new;
512
  }
513
514
  /**
515
   * Function to delete current record in database.
516
   *
517
   * @return bool
518
   */
519 1
  public function delete(): bool
520
  {
521 1
    $return = $this->execute(
522 1
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
523
            [
524 1
                'delete',
525
                'from',
526
                'where',
527
            ]
528
        ),
529 1
        $this->params
530
    );
531
532 1
    return $return !== false;
533
  }
534
535
  /**
536
   * Helper function to exec sql.
537
   *
538
   * @param string $sql   <p>The SQL need to be execute.</p>
539
   * @param array  $param <p>The param will be bind to the sql statement.</p>
540
   *
541
   * @return bool|int|Result              <p>
542
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
543
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
544
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
545
   *                                      "true" by e.g. "DROP"-queries<br />
546
   *                                      "false" on error
547
   *                                      </p>
548
   */
549 23
  public function execute(string $sql, array $param = [])
550
  {
551 23
    if (!$this->db instanceof DB) {
552 3
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
553
    }
554
555 23
    return $this->db->query($sql, $param);
556
  }
557
558
  /**
559
   * Function to find one record and assign in to current object.
560
   *
561
   * @param mixed $id <p>
562
   *                  If call this function using this param, we will find the record by using this id.
563
   *                  If not set, just find the first record in database.
564
   *                  </p>
565
   *
566
   * @return false|$this <p>
567
   *                     If we could find the record, assign in to current object and return it,
568
   *                     otherwise return "false".
569
   *                     </p>
570
   */
571 11
  public function fetch($id = null)
572
  {
573 11
    if ($id) {
574 6
      $this->reset()->eq($this->primaryKeyName, $id);
575
    }
576
577 11
    $sqlQuery = $this->limit(1)->_buildSql(
0 ignored issues
show
Bug introduced by
The call to limit() misses a required argument $|.

This check looks for function calls that miss required arguments.

Loading history...
578
        [
579 11
            'select',
580
            'from',
581
            'join',
582
            'where',
583
            'group',
584
            'having',
585
            'order',
586
            'limit',
587
        ]
588
    );
589
590 11
    $return = $this->query(
591 11
        $sqlQuery,
592 11
        $this->params,
593 11
        $this->reset(),
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

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...
594 11
        true
595
    );
596
597 11
    return $return;
598
  }
599
600
  /**
601
   * Function to find all records in database.
602
   *
603
   * @param array|null $ids <p>
604
   *                        If call this function using this param, we will find the record by using this id's.
605
   *                        If not set, just find all records in database.
606
   *                        </p>
607
   *
608
   * @return $this[]
609
   */
610 6
  public function fetchAll(array $ids = null): array
611
  {
612 6
    if ($ids) {
613 3
      $this->reset()->in($this->primaryKeyName, $ids);
614
    }
615
616 6
    return $this->query(
617 6
        $this->_buildSql(
618
            [
619 6
                'select',
620
                'from',
621
                'join',
622
                'where',
623
                'groupBy',
624
                'having',
625
                'orderBy',
626
                'limit',
627
            ]
628
        ),
629 6
        $this->params,
630 6
        $this->reset()
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

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...
631
    );
632
  }
633
634
  /**
635
   * @param mixed $id
636
   *
637
   * @return $this
638
   *
639
   * @throws FetchingException <p>Will be thrown, if we can not find the id.</p>
640
   */
641 2
  public function fetchById($id): self
642
  {
643 2
    $obj = $this->fetchByIdIfExists($id);
644 2
    if ($obj === null) {
645 1
      throw new FetchingException("No row with primary key '$id' in table '$this->table'.");
646
    }
647
648 1
    return $obj;
649
  }
650
651
  /**
652
   * @param mixed $id
653
   *
654
   * @return $this|null
655
   */
656 4
  public function fetchByIdIfExists($id)
657
  {
658 4
    $list = $this->fetch($id);
659
660 4
    if (!$list) {
661 2
      return null;
662
    }
663
664 2
    return $list;
665
  }
666
667
  /**
668
   * @param array $ids
669
   *
670
   * @return $this[]
671
   */
672 2
  public function fetchByIds(array $ids): array
673
  {
674 2
    if (empty($ids)) {
675
      return [];
676
    }
677
678 2
    $list = $this->fetchAll($ids);
679 2
    if (\is_array($list) && \count($list) > 0) {
680 1
      return $list;
681
    }
682
683 1
    return [];
684
  }
685
686
  /**
687
   * @param array $ids
688
   *
689
   * @return $this[]
690
   */
691 1
  public function fetchByIdsPrimaryKeyAsArrayIndex(array $ids): array
692
  {
693 1
    $result = $this->fetchAll($ids);
694
695 1
    $resultNew = [];
696 1
    foreach ($result as $item) {
697 1
      $resultNew[$item->getPrimaryKey()] = $item;
698
    }
699
700 1
    return $resultNew;
701
  }
702
703
  /**
704
   * @param string $query
705
   *
706
   * @return $this[]|$this
707
   */
708 2
  public function fetchByQuery(string $query)
709
  {
710 2
    $list = $this->query(
711 2
        $query,
712 2
        $this->params,
713 2
        $this->reset()
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

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...
714
    );
715
716 2
    if (\is_array($list)) {
717 2
      if (\count($list) === 0) {
718
        return [];
719
      }
720
721 2
      return $list;
722
    }
723
724
    $this->array = $list->getArray();
725
726
    return $this;
727
  }
728
729
  /**
730
   * @return $this
731
   */
732 1
  public static function fetchEmpty(): self
733
  {
734 1
    $class = static::class;
735
736 1
    return new $class;
737
  }
738
739
  /**
740
   * @param string $query
741
   *
742
   * @return $this[]
743
   */
744 1
  public function fetchManyByQuery(string $query): array
745
  {
746 1
    $list = $this->fetchByQuery($query);
747
748 1
    if (!$list || empty($list)) {
749
      return [];
750
    }
751
752 1
    return $list;
753
  }
754
755
  /**
756
   * @param string $query
757
   *
758
   * @return $this|null
759
   */
760 1
  public function fetchOneByQuery(string $query)
761
  {
762 1
    $list = $this->fetchByQuery($query);
763
764 1
    if (!$list || empty($list)) {
765
      return null;
766
    }
767
768 1
    if (\is_array($list) && \count($list) > 0) {
769 1
      $this->array = $list[0]->getArray();
770
    } else {
771
      $this->array = $list->getArray();
772
    }
773
774 1
    return $this;
775
  }
776
777
  /**
778
   * @return array
779
   */
780 1
  public function getDirty(): array
781
  {
782 1
    return $this->dirty;
783
  }
784
785
  /**
786
   * @return array
787
   */
788
  public function getParams(): array
789
  {
790
    return $this->params;
791
  }
792
793
  /**
794
   * @return mixed|null
795
   */
796 13
  public function getPrimaryKey()
797
  {
798 13
    $id = $this->{$this->primaryKeyName};
799 13
    if ($id) {
800 12
      return $id;
801
    }
802
803 1
    return null;
804
  }
805
806
  /**
807
   * @return string
808
   */
809
  public function getPrimaryKeyName(): string
810
  {
811
    return $this->primaryKeyName;
812
  }
813
814
  /**
815
   * Helper function to get relation of this object.
816
   * There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
817
   *
818
   * @param string $name <p>The name of the relation (the array key from the definition).</p>
819
   *
820
   * @return mixed
821
   *
822
   * @throws ActiveRecordException <p>If the relation can't be found .</p>
823
   */
824 3
  protected function &getRelation(string $name)
825
  {
826 3
    $relation = $this->relations[$name];
827
    if (
828 3
        $relation instanceof self
829
        ||
830
        (
831 2
            \is_array($relation)
832
            &&
833 3
            $relation[0] instanceof self
834
        )
835
    ) {
836 3
      return $relation;
837
    }
838
839
    /* @var $obj ActiveRecord */
840 2
    $obj = new $relation[1];
841
842 2
    $this->relations[$name] = $obj;
843 2
    if (isset($relation[3]) && \is_array($relation[3])) {
844 1
      foreach ((array)$relation[3] as $func => $args) {
845 1
        \call_user_func_array([$obj, $func], (array)$args);
846
      }
847
    }
848
849 2
    $backref = $relation[4] ?? '';
850 2
    $relationInstanceOfSelf = ($relation instanceof self);
851
    if (
852 2
        $relationInstanceOfSelf === false
853
        &&
854 2
        self::HAS_ONE == $relation[0]
855
    ) {
856
857 1
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetch();
858
859 1
      if ($backref) {
860 1
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
861
      }
862
863
    } elseif (
864 2
        \is_array($relation)
865
        &&
866 2
        self::HAS_MANY == $relation[0]
867
    ) {
868
869 2
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetchAll();
870 2
      if ($backref) {
871 1
        foreach ($this->relations[$name] as $o) {
872 2
          $o->{$backref} = $this;
873
        }
874
      }
875
876
    } elseif (
877 2
        $relationInstanceOfSelf === false
878
        &&
879 2
        self::BELONGS_TO == $relation[0]
880
    ) {
881
882 2
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
883
884 2
      if ($backref) {
885 2
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
886
      }
887
888
    } else {
889
      throw new ActiveRecordException("Relation $name not found.");
890
    }
891
892 2
    return $this->relations[$name];
893
  }
894
895
  /**
896
   * @return string
897
   */
898
  public function getTable(): string
899
  {
900
    return $this->table;
901
  }
902
903
  /**
904
   * Helper function for "GROUP BY".
905
   *
906
   * @param mixed $args
907
   *
908
   * @return $this
909
   */
910
  public function groupBy($args): self
0 ignored issues
show
Unused Code introduced by
The parameter $args 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...
911
  {
912
    $this->__call('groupBy', \func_get_args());
913
914
    return $this;
915
  }
916
917
  /**
918
   * Function to build insert SQL, and insert current record into database.
919
   *
920
   * @return bool|int <p>
921
   *                  If insert was successful, it will return the new id,
922
   *                  otherwise it will return false or true (if there are no dirty data).
923
   *                  </p>
924
   */
925 4
  public function insert()
926
  {
927 4
    if (!$this->db instanceof DB) {
928 3
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
929
    }
930
931 4
    if (\count($this->dirty) === 0) {
932
      return true;
933
    }
934
935 4
    $value = $this->_filterParam($this->dirty);
936 4
    $this->insert = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property insert does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
937
        [
938 4
            'operator' => 'INSERT INTO ' . $this->table,
939 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => \array_keys($this->dirty)]),
940
        ]
941
    );
942 4
    $this->values = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property values does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
943
        [
944 4
            'operator' => 'VALUES',
945 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => $value]),
946
        ]
947
    );
948
949 4
    $result = $this->execute($this->_buildSql(['insert', 'values']), $this->params);
950 4
    if ($result !== false) {
951 4
      $this->{$this->primaryKeyName} = $result;
952
953 4
      $this->resetDirty();
954 4
      $this->reset();
955
956 4
      return $result;
957
    }
958
959
    return false;
960
  }
961
962
  /**
963
   * @return bool
964
   */
965
  public function isNewDataAreDirty(): bool
966
  {
967
    return $this->new_data_are_dirty;
968
  }
969
970
  /**
971
   * Helper function to add condition into JOIN.
972
   *
973
   * @param string $table <p>The join table name.</p>
974
   * @param string $on    <p>The condition of ON.</p>
975
   * @param string $type  <p>The join type, like "LEFT", "INNER", "OUTER".</p>
976
   *
977
   * @return $this
978
   */
979 1
  public function join(string $table, string $on, string $type = 'LEFT'): self
980
  {
981 1
    $this->join = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property join does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
982
        [
983 1
            'source'   => $this->join ?: '',
0 ignored issues
show
Documentation introduced by
The property join does not exist on object<voku\db\ActiveRecord>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
984 1
            'operator' => $type . ' JOIN',
985 1
            'target'   => new ActiveRecordExpressions(
986
                [
987 1
                    'source'   => $table,
988 1
                    'operator' => 'ON',
989 1
                    'target'   => $on,
990
                ]
991
            ),
992
        ]
993
    );
994
995 1
    return $this;
996
  }
997
998
  /**
999
   * Helper function for "ORDER BY".
1000
   *
1001
   * @param mixed $args
1002
   *
1003
   * @return $this
1004
   */
1005 2
  public function orderBy($args): self
0 ignored issues
show
Unused Code introduced by
The parameter $args 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...
1006
  {
1007 2
    $this->__call('orderBy', \func_get_args());
1008
1009 2
    return $this;
1010
  }
1011
1012
  /**
1013
   * Helper function to query one record by sql and params.
1014
   *
1015
   * @param string    $sql            <p>
1016
   *                                  The SQL query to find the record.
1017
   *                                  </p>
1018
   * @param array     $param          <p>
1019
   *                                  The param will be bind to the $sql query.
1020
   *                                  </p>
1021
   * @param null|self $obj            <p>
1022
   *                                  The object, if find record in database, we will assign the attributes into
1023
   *                                  this object.
1024
   *                                  </p>
1025
   * @param bool      $single         <p>
1026
   *                                  If set to true, we will find record and fetch in current object, otherwise
1027
   *                                  will find all records.
1028
   *                                  </p>
1029
   *
1030
   * @return bool|$this|$this[]
1031
   */
1032 17
  public function query(string $sql, array $param = [], self $obj = null, bool $single = false)
1033
  {
1034 17
    $result = $this->execute($sql, $param);
1035
1036 17
    if ($result === false) {
1037 1
      return false;
1038
    }
1039
1040 17
    $useObject = \is_object($obj);
1041 17
    if ($useObject === true) {
1042 17
      $called_class = $obj;
1043
    } else {
1044
      $called_class = static::class;
1045
    }
1046
1047 17
    $this->setNewDataAreDirty(false);
1048
1049 17
    if ($single) {
1050 11
      $return = $result->fetchObject($called_class, null, true);
1051
    } else {
1052 8
      $return = $result->fetchAllObject($called_class, null);
1053
    }
1054
1055 17
    $this->setNewDataAreDirty(true);
1056
1057 17
    return $return;
1058
  }
1059
1060
  /**
1061
   * Function to reset the $params and $sqlExpressions.
1062
   *
1063
   * @return $this
1064
   */
1065 23
  public function reset(): self
1066
  {
1067 23
    $this->params = [];
1068 23
    $this->sqlExpressions = [];
1069
1070 23
    return $this;
1071
  }
1072
1073
  /**
1074
   * Reset the dirty data.
1075
   *
1076
   * @return $this
1077
   */
1078 6
  public function resetDirty(): self
1079
  {
1080 6
    $this->dirty = [];
1081
1082 6
    return $this;
1083
  }
1084
1085
  /**
1086
   * set the DB connection.
1087
   *
1088
   * @param DB $db
1089
   */
1090
  public function setDb(DB $db)
1091
  {
1092
    $this->db = $db;
1093
  }
1094
1095
  /**
1096
   * @param bool $bool
1097
   */
1098 17
  public function setNewDataAreDirty(bool $bool)
1099
  {
1100 17
    $this->new_data_are_dirty = $bool;
1101 17
  }
1102
1103
  /**
1104
   * @param mixed $primaryKey
1105
   * @param bool  $dirty
1106
   *
1107
   * @return $this
1108
   */
1109 1
  public function setPrimaryKey($primaryKey, bool $dirty = true): self
1110
  {
1111 1
    if (\property_exists($this, $this->primaryKeyName)) {
1112
      $this->{$this->primaryKeyName} = $primaryKey;
1113
    }
1114
1115 1
    if ($dirty === true) {
1116 1
      $this->dirty[$this->primaryKeyName] = $primaryKey;
1117
    } else {
1118
      $this->array[$this->primaryKeyName] = $primaryKey;
1119
    }
1120
1121 1
    return $this;
1122
  }
1123
1124
  /**
1125
   * @param string $primaryKeyName
1126
   *
1127
   * @return $this
1128
   */
1129
  public function setPrimaryKeyName(string $primaryKeyName): self
1130
  {
1131
    $this->primaryKeyName = $primaryKeyName;
1132
1133
    return $this;
1134
  }
1135
1136
  /**
1137
   * @param string $table
1138
   */
1139
  public function setTable(string $table)
1140
  {
1141
    $this->table = $table;
1142
  }
1143
1144
  /**
1145
   * Function to build update SQL, and update current record in database, just write the dirty data into database.
1146
   *
1147
   * @return bool|int <p>
1148
   *                  If update was successful, it will return the affected rows as int,
1149
   *                  otherwise it will return false or true (if there are no dirty data).
1150
   *                  </p>
1151
   */
1152 2
  public function update()
1153
  {
1154 2
    if (\count($this->dirty) == 0) {
1155
      return true;
1156
    }
1157
1158 2
    foreach ($this->dirty as $field => $value) {
1159 2
      $this->addCondition($field, '=', $value, ',', 'set');
1160
    }
1161
1162 2
    $result = $this->execute(
1163 2
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
1164
            [
1165 2
                'update',
1166
                'set',
1167
                'where',
1168
            ]
1169
        ),
1170 2
        $this->params
1171
    );
1172 2
    if ($result !== false) {
1173 2
      $this->resetDirty();
1174 2
      $this->reset();
1175
1176 2
      return $result;
1177
    }
1178
1179
    return false;
1180
  }
1181
1182
  /**
1183
   * Make wrap when build the SQL expressions of WHERE.
1184
   *
1185
   * @param string $op <p>If given, this param will build one "ActiveRecordExpressionsWrap" and include the stored
1186
   *                   expressions add into WHERE, otherwise it will stored the expressions into an array.</p>
1187
   *
1188
   * @return $this
1189
   */
1190 1
  public function wrap($op = null): self
1191
  {
1192 1
    if (1 === \func_num_args()) {
1193 1
      $this->wrap = false;
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1194 1
      if (\is_array($this->expressions) && \count($this->expressions) > 0) {
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1195 1
        $this->_addCondition(
1196 1
            new ActiveRecordExpressionsWrap(
1197
                [
1198 1
                    'delimiter' => ' ',
1199 1
                    'target'    => $this->expressions,
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1200
                ]
1201
            ),
1202 1
            'or' === \strtolower($op) ? 'OR' : 'AND'
1203
        );
1204
      }
1205 1
      $this->expressions = [];
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1206
    } else {
1207 1
      $this->wrap = true;
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1208
    }
1209
1210 1
    return $this;
1211
  }
1212
}
1213