Completed
Push — master ( ace86f...569799 )
by Lars
02:21
created

ActiveRecord::__unset()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 8
cp 0.875
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 8
nop 1
crap 4.0312
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 mixed $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 mixed $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
    if (\property_exists($this, $key) === true) {
324
      return $this->{$key};
325
    }
326
327 23
    return parent::get($key, $fallback, $array);
328
  }
329
330
  /**
331
   * helper function to add condition into WHERE.
332
   *
333
   * @param ActiveRecordExpressions $expression <p>The expression will be concat into WHERE or SET statement.</p>
334
   * @param string                  $operator   <p>The operator to concat this Expressions into WHERE or SET
335
   *                                            statement.</p>
336
   * @param string                  $name       <p>The Expression will contact to.</p>
337
   */
338 14
  protected function _addCondition(ActiveRecordExpressions $expression, string $operator, string $name = 'where')
339
  {
340 14
    if (!$this->{$name}) {
341
342 14
      $this->{$name} = new ActiveRecordExpressions(
343
          [
344 14
              'operator' => \strtoupper($name),
345 14
              'target'   => $expression,
346
          ]
347
      );
348
349
    } else {
350
351 4
      $this->{$name}->target = new ActiveRecordExpressions(
352
          [
353 4
              'source'   => $this->{$name}->target,
354 4
              'operator' => $operator,
355 4
              'target'   => $expression,
356
          ]
357
      );
358
359
    }
360 14
  }
361
362
  /**
363
   * helper function to make wrapper. Stored the expression in to array.
364
   *
365
   * @param ActiveRecordExpressions $exp      <p>The expression will be stored.</p>
366
   * @param string                  $operator <p>The operator to concat this Expressions into WHERE statement.</p>
367
   */
368 1
  protected function _addExpression(ActiveRecordExpressions $exp, string $operator)
369
  {
370
    if (
371 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...
372
        ||
373 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...
374
    ) {
375 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...
376
    } else {
377 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...
378
          [
379 1
              'operator' => $operator,
380 1
              'target'   => $exp,
381
          ]
382
      );
383
    }
384 1
  }
385
386
  /**
387
   * Helper function to build SQL with sql parts.
388
   *
389
   * @param string[] $sql_array <p>The SQL part will be build.</p>
390
   *
391
   * @return string
392
   */
393 21
  protected function _buildSql(array $sql_array = []): string
394
  {
395 21
    \array_walk($sql_array, [$this, '_buildSqlCallback'], $this);
396
397
    // DEBUG
398
    //echo 'SQL: ', implode(' ', $sql_array), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
399
400 21
    return \implode(' ', $sql_array);
401
  }
402
403
  /**
404
   * Helper function to build SQL with sql parts.
405
   *
406
   * @param string $sql_string_part <p>The SQL part will be build.</p>
407
   * @param int    $index           <p>The index of $n in $sql array.</p>
408
   * @param self   $active_record   <p>The reference to $this.</p>
409
   */
410 21
  private function _buildSqlCallback(string &$sql_string_part, int $index, self $active_record)
411
  {
412
    if (
413 21
        'select' === $sql_string_part
414
        &&
415 21
        null === $active_record->{$sql_string_part}
416
    ) {
417
418 14
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table . '.*';
419
420
    } elseif (
421
        (
422 21
            'update' === $sql_string_part
423
            ||
424 21
            'from' === $sql_string_part
425
        )
426
        &&
427 21
        null === $active_record->{$sql_string_part}
428
    ) {
429
430 17
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table;
431
432 21
    } elseif ('delete' === $sql_string_part) {
433
434 1
      $sql_string_part = \strtoupper($sql_string_part) . ' ';
435
436
    } else {
437
438 21
      $sql_string_part = (null !== $active_record->{$sql_string_part}) ? $active_record->{$sql_string_part} . ' ' : '';
439
440
    }
441 21
  }
442
443
  /**
444
   * Helper function to build place holder when make SQL expressions.
445
   *
446
   * @param mixed $value <p>The value will be bind to SQL, just store it in $this->params.</p>
447
   *
448
   * @return mixed $value
449
   */
450 18
  protected function _filterParam($value)
451
  {
452 18
    if (\is_array($value)) {
453 8
      foreach ($value as $key => $val) {
454 8
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
455
      }
456 11
    } elseif (\is_string($value)) {
457 3
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
458 3
      $value = $ph;
459
    }
460
461 18
    return $value;
462
  }
463
464
  /**
465
   * Helper function to add condition into WHERE.
466
   *
467
   * @param string $field           <p>The field name, the source of Expressions</p>
468
   * @param string $operator        <p>The operator for this condition.</p>
469
   * @param mixed  $value           <p>The target of the Expressions.</p>
470
   * @param string $operator_concat <p>The operator to concat this Expressions into WHERE or SET statement.</p>
471
   * @param string $name            <p>The Expression will contact to.</p>
472
   */
473 14
  public function addCondition(string $field, string $operator, $value, string $operator_concat = 'AND', string $name = 'where')
474
  {
475 14
    $value = $this->_filterParam($value);
476 14
    $expression = new ActiveRecordExpressions(
477
        [
478 14
            'source'   => ('where' === strtolower($name) ? $this->table . '.' : '') . $field,
479 14
            'operator' => $operator,
480 14
            'target'   => \is_array($value)
481 4
                ? new ActiveRecordExpressionsWrap(
482 4
                    'between' === \strtolower($operator)
483 1
                        ? ['target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ']
484 4
                        : ['target' => $value]
485 14
                ) : $value,
486
        ]
487
    );
488
489 14
    if ($expression) {
490 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...
491 14
        $this->_addCondition($expression, $operator_concat, $name);
492
      } else {
493 1
        $this->_addExpression($expression, $operator_concat);
494
      }
495
    }
496 14
  }
497
498
  /**
499
   * Helper function to copy an existing active record (and insert it into the database).
500
   *
501
   * @param bool $insert
502
   *
503
   * @return $this
504
   */
505 1
  public function copy(bool $insert = true): self
506
  {
507 1
    $new = clone $this;
508
509 1
    if ($insert) {
510 1
      $new->setPrimaryKey(null);
511 1
      $id = $new->insert();
512 1
      $new->setPrimaryKey($id);
513
    }
514
515 1
    return $new;
516
  }
517
518
  /**
519
   * Function to delete current record in database.
520
   *
521
   * @return bool
522
   */
523 1
  public function delete(): bool
524
  {
525 1
    $return = $this->execute(
526 1
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
527
            [
528 1
                'delete',
529
                'from',
530
                'where',
531
            ]
532
        ),
533 1
        $this->params
534
    );
535
536 1
    return $return !== false;
537
  }
538
539
  /**
540
   * Helper function to exec sql.
541
   *
542
   * @param string $sql   <p>The SQL need to be execute.</p>
543
   * @param array  $param <p>The param will be bind to the sql statement.</p>
544
   *
545
   * @return bool|int|Result              <p>
546
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
547
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
548
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
549
   *                                      "true" by e.g. "DROP"-queries<br />
550
   *                                      "false" on error
551
   *                                      </p>
552
   */
553 23
  public function execute(string $sql, array $param = [])
554
  {
555 23
    if (!$this->db instanceof DB) {
556 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...
557
    }
558
559 23
    return $this->db->query($sql, $param);
560
  }
561
562
  /**
563
   * Function to find one record and assign in to current object.
564
   *
565
   * @param mixed $id <p>
566
   *                  If call this function using this param, we will find the record by using this id.
567
   *                  If not set, just find the first record in database.
568
   *                  </p>
569
   *
570
   * @return false|$this <p>
571
   *                     If we could find the record, assign in to current object and return it,
572
   *                     otherwise return "false".
573
   *                     </p>
574
   */
575 11
  public function fetch($id = null)
576
  {
577 11
    if ($id) {
578 6
      $this->reset()->eq($this->primaryKeyName, $id);
579
    }
580
581 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...
582
        [
583 11
            'select',
584
            'from',
585
            'join',
586
            'where',
587
            'group',
588
            'having',
589
            'order',
590
            'limit',
591
        ]
592
    );
593
594 11
    $return = $this->query(
595 11
        $sqlQuery,
596 11
        $this->params,
597 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...
598 11
        true
599
    );
600
601 11
    return $return;
602
  }
603
604
  /**
605
   * Function to find all records in database.
606
   *
607
   * @param array|null $ids <p>
608
   *                        If call this function using this param, we will find the record by using this id's.
609
   *                        If not set, just find all records in database.
610
   *                        </p>
611
   *
612
   * @return $this[]
613
   */
614 6
  public function fetchAll(array $ids = null): array
615
  {
616 6
    if ($ids) {
617 3
      $this->reset()->in($this->primaryKeyName, $ids);
618
    }
619
620 6
    return $this->query(
621 6
        $this->_buildSql(
622
            [
623 6
                'select',
624
                'from',
625
                'join',
626
                'where',
627
                'groupBy',
628
                'having',
629
                'orderBy',
630
                'limit',
631
            ]
632
        ),
633 6
        $this->params,
634 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...
635
    );
636
  }
637
638
  /**
639
   * @param mixed $id
640
   *
641
   * @return $this
642
   *
643
   * @throws FetchingException <p>Will be thrown, if we can not find the id.</p>
644
   */
645 2
  public function fetchById($id): self
646
  {
647 2
    $obj = $this->fetchByIdIfExists($id);
648 2
    if ($obj === null) {
649 1
      throw new FetchingException("No row with primary key '$id' in table '$this->table'.");
650
    }
651
652 1
    return $obj;
653
  }
654
655
  /**
656
   * @param mixed $id
657
   *
658
   * @return $this|null
659
   */
660 4
  public function fetchByIdIfExists($id)
661
  {
662 4
    $list = $this->fetch($id);
663
664 4
    if (!$list) {
665 2
      return null;
666
    }
667
668 2
    return $list;
669
  }
670
671
  /**
672
   * @param array $ids
673
   *
674
   * @return $this[]
675
   */
676 2
  public function fetchByIds(array $ids): array
677
  {
678 2
    if (empty($ids)) {
679
      return [];
680
    }
681
682 2
    $list = $this->fetchAll($ids);
683 2
    if (\is_array($list) && \count($list) > 0) {
684 1
      return $list;
685
    }
686
687 1
    return [];
688
  }
689
690
  /**
691
   * @param array $ids
692
   *
693
   * @return $this[]
694
   */
695 1
  public function fetchByIdsPrimaryKeyAsArrayIndex(array $ids): array
696
  {
697 1
    $result = $this->fetchAll($ids);
698
699 1
    $resultNew = [];
700 1
    foreach ($result as $item) {
701 1
      $resultNew[$item->getPrimaryKey()] = $item;
702
    }
703
704 1
    return $resultNew;
705
  }
706
707
  /**
708
   * @param string $query
709
   *
710
   * @return $this[]|$this
711
   */
712 2
  public function fetchByQuery(string $query)
713
  {
714 2
    $list = $this->query(
715 2
        $query,
716 2
        $this->params,
717 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...
718
    );
719
720 2
    if (\is_array($list)) {
721 2
      if (\count($list) === 0) {
722
        return [];
723
      }
724
725 2
      return $list;
726
    }
727
728
    $this->array = $list->getArray();
729
730
    return $this;
731
  }
732
733
  /**
734
   * @return $this
735
   */
736 1
  public static function fetchEmpty(): self
737
  {
738 1
    $class = static::class;
739
740 1
    return new $class;
741
  }
742
743
  /**
744
   * @param string $query
745
   *
746
   * @return $this[]
747
   */
748 1
  public function fetchManyByQuery(string $query): array
749
  {
750 1
    $list = $this->fetchByQuery($query);
751
752 1
    if (!$list || empty($list)) {
753
      return [];
754
    }
755
756 1
    return $list;
757
  }
758
759
  /**
760
   * @param string $query
761
   *
762
   * @return $this|null
763
   */
764 1
  public function fetchOneByQuery(string $query)
765
  {
766 1
    $list = $this->fetchByQuery($query);
767
768 1
    if (!$list || empty($list)) {
769
      return null;
770
    }
771
772 1
    if (\is_array($list) && \count($list) > 0) {
773 1
      $this->array = $list[0]->getArray();
774
    } else {
775
      $this->array = $list->getArray();
776
    }
777
778 1
    return $this;
779
  }
780
781
  /**
782
   * @return array
783
   */
784 1
  public function getDirty(): array
785
  {
786 1
    return $this->dirty;
787
  }
788
789
  /**
790
   * @return array
791
   */
792
  public function getParams(): array
793
  {
794
    return $this->params;
795
  }
796
797
  /**
798
   * @return mixed|null
799
   */
800 13
  public function getPrimaryKey()
801
  {
802 13
    $id = $this->{$this->primaryKeyName};
803 13
    if ($id) {
804 12
      return $id;
805
    }
806
807 1
    return null;
808
  }
809
810
  /**
811
   * @return string
812
   */
813
  public function getPrimaryKeyName(): string
814
  {
815
    return $this->primaryKeyName;
816
  }
817
818
  /**
819
   * Helper function to get relation of this object.
820
   * There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
821
   *
822
   * @param string $name <p>The name of the relation (the array key from the definition).</p>
823
   *
824
   * @return mixed
825
   *
826
   * @throws ActiveRecordException <p>If the relation can't be found .</p>
827
   */
828 3
  protected function &getRelation(string $name)
829
  {
830 3
    $relation = $this->relations[$name];
831
    if (
832 3
        $relation instanceof self
833
        ||
834
        (
835 2
            \is_array($relation)
836
            &&
837 3
            $relation[0] instanceof self
838
        )
839
    ) {
840 3
      return $relation;
841
    }
842
843
    /* @var $obj ActiveRecord */
844 2
    $obj = new $relation[1];
845
846 2
    $this->relations[$name] = $obj;
847 2
    if (isset($relation[3]) && \is_array($relation[3])) {
848 1
      foreach ((array)$relation[3] as $func => $args) {
849 1
        \call_user_func_array([$obj, $func], (array)$args);
850
      }
851
    }
852
853 2
    $backref = $relation[4] ?? '';
854 2
    $relationInstanceOfSelf = ($relation instanceof self);
855
    if (
856 2
        $relationInstanceOfSelf === false
857
        &&
858 2
        self::HAS_ONE == $relation[0]
859
    ) {
860
861 1
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetch();
862
863 1
      if ($backref) {
864 1
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
865
      }
866
867
    } elseif (
868 2
        \is_array($relation)
869
        &&
870 2
        self::HAS_MANY == $relation[0]
871
    ) {
872
873 2
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetchAll();
874 2
      if ($backref) {
875 1
        foreach ($this->relations[$name] as $o) {
876 2
          $o->{$backref} = $this;
877
        }
878
      }
879
880
    } elseif (
881 2
        $relationInstanceOfSelf === false
882
        &&
883 2
        self::BELONGS_TO == $relation[0]
884
    ) {
885
886 2
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
887
888 2
      if ($backref) {
889 2
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
890
      }
891
892
    } else {
893
      throw new ActiveRecordException("Relation $name not found.");
894
    }
895
896 2
    return $this->relations[$name];
897
  }
898
899
  /**
900
   * @return string
901
   */
902
  public function getTable(): string
903
  {
904
    return $this->table;
905
  }
906
907
  /**
908
   * Helper function for "GROUP BY".
909
   *
910
   * @param mixed $args
911
   *
912
   * @return $this
913
   */
914
  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...
915
  {
916
    $this->__call('groupBy', \func_get_args());
917
918
    return $this;
919
  }
920
921
  /**
922
   * Function to build insert SQL, and insert current record into database.
923
   *
924
   * @return bool|int <p>
925
   *                  If insert was successful, it will return the new id,
926
   *                  otherwise it will return false or true (if there are no dirty data).
927
   *                  </p>
928
   */
929 4
  public function insert()
930
  {
931 4
    if (!$this->db instanceof DB) {
932 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...
933
    }
934
935 4
    if (\count($this->dirty) === 0) {
936
      return true;
937
    }
938
939 4
    $value = $this->_filterParam($this->dirty);
940 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...
941
        [
942 4
            'operator' => 'INSERT INTO ' . $this->table,
943 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => \array_keys($this->dirty)]),
944
        ]
945
    );
946 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...
947
        [
948 4
            'operator' => 'VALUES',
949 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => $value]),
950
        ]
951
    );
952
953 4
    $result = $this->execute($this->_buildSql(['insert', 'values']), $this->params);
954 4
    if ($result !== false) {
955 4
      $this->{$this->primaryKeyName} = $result;
956
957 4
      $this->resetDirty();
958 4
      $this->reset();
959
960 4
      return $result;
961
    }
962
963
    return false;
964
  }
965
966
  /**
967
   * @return bool
968
   */
969
  public function isNewDataAreDirty(): bool
970
  {
971
    return $this->new_data_are_dirty;
972
  }
973
974
  /**
975
   * Helper function to add condition into JOIN.
976
   *
977
   * @param string $table <p>The join table name.</p>
978
   * @param string $on    <p>The condition of ON.</p>
979
   * @param string $type  <p>The join type, like "LEFT", "INNER", "OUTER".</p>
980
   *
981
   * @return $this
982
   */
983 1
  public function join(string $table, string $on, string $type = 'LEFT'): self
984
  {
985 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...
986
        [
987 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...
988 1
            'operator' => $type . ' JOIN',
989 1
            'target'   => new ActiveRecordExpressions(
990
                [
991 1
                    'source'   => $table,
992 1
                    'operator' => 'ON',
993 1
                    'target'   => $on,
994
                ]
995
            ),
996
        ]
997
    );
998
999 1
    return $this;
1000
  }
1001
1002
  /**
1003
   * Helper function for "ORDER BY".
1004
   *
1005
   * @param mixed $args
1006
   *
1007
   * @return $this
1008
   */
1009 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...
1010
  {
1011 2
    $this->__call('orderBy', \func_get_args());
1012
1013 2
    return $this;
1014
  }
1015
1016
  /**
1017
   * Helper function to query one record by sql and params.
1018
   *
1019
   * @param string    $sql            <p>
1020
   *                                  The SQL query to find the record.
1021
   *                                  </p>
1022
   * @param array     $param          <p>
1023
   *                                  The param will be bind to the $sql query.
1024
   *                                  </p>
1025
   * @param null|self $obj            <p>
1026
   *                                  The object, if find record in database, we will assign the attributes into
1027
   *                                  this object.
1028
   *                                  </p>
1029
   * @param bool      $single         <p>
1030
   *                                  If set to true, we will find record and fetch in current object, otherwise
1031
   *                                  will find all records.
1032
   *                                  </p>
1033
   *
1034
   * @return bool|$this|$this[]
1035
   */
1036 17
  public function query(string $sql, array $param = [], self $obj = null, bool $single = false)
1037
  {
1038 17
    $result = $this->execute($sql, $param);
1039
1040 17
    if ($result === false) {
1041
      return false;
1042
    }
1043
1044 17
    $useObject = \is_object($obj);
1045 17
    if ($useObject === true) {
1046 17
      $called_class = $obj;
1047
    } else {
1048
      $called_class = static::class;
1049
    }
1050
1051 17
    $this->setNewDataAreDirty(false);
1052
1053 17
    if ($single) {
1054 11
      $return = $result->fetchObject($called_class, null, true);
1055
    } else {
1056 8
      $return = $result->fetchAllObject($called_class, null);
1057
    }
1058
1059 17
    $this->setNewDataAreDirty(true);
1060
1061 17
    return $return;
1062
  }
1063
1064
  /**
1065
   * Function to reset the $params and $sqlExpressions.
1066
   *
1067
   * @return $this
1068
   */
1069 23
  public function reset(): self
1070
  {
1071 23
    $this->params = [];
1072 23
    $this->sqlExpressions = [];
1073
1074 23
    return $this;
1075
  }
1076
1077
  /**
1078
   * Reset the dirty data.
1079
   *
1080
   * @return $this
1081
   */
1082 6
  public function resetDirty(): self
1083
  {
1084 6
    $this->dirty = [];
1085
1086 6
    return $this;
1087
  }
1088
1089
  /**
1090
   * set the DB connection.
1091
   *
1092
   * @param DB $db
1093
   */
1094
  public function setDb(DB $db)
1095
  {
1096
    $this->db = $db;
1097
  }
1098
1099
  /**
1100
   * @param bool $bool
1101
   */
1102 17
  public function setNewDataAreDirty(bool $bool)
1103
  {
1104 17
    $this->new_data_are_dirty = $bool;
1105 17
  }
1106
1107
  /**
1108
   * @param mixed $primaryKey
1109
   * @param bool  $dirty
1110
   *
1111
   * @return $this
1112
   */
1113 1
  public function setPrimaryKey($primaryKey, bool $dirty = true): self
1114
  {
1115 1
    if (\property_exists($this, $this->primaryKeyName)) {
1116
      $this->{$this->primaryKeyName} = $primaryKey;
1117
    }
1118
1119 1
    if ($dirty === true) {
1120 1
      $this->dirty[$this->primaryKeyName] = $primaryKey;
1121
    } else {
1122
      $this->array[$this->primaryKeyName] = $primaryKey;
1123
    }
1124
1125 1
    return $this;
1126
  }
1127
1128
  /**
1129
   * @param string $primaryKeyName
1130
   *
1131
   * @return $this
1132
   */
1133
  public function setPrimaryKeyName(string $primaryKeyName): self
1134
  {
1135
    $this->primaryKeyName = $primaryKeyName;
1136
1137
    return $this;
1138
  }
1139
1140
  /**
1141
   * @param string $table
1142
   */
1143
  public function setTable(string $table)
1144
  {
1145
    $this->table = $table;
1146
  }
1147
1148
  /**
1149
   * Function to build update SQL, and update current record in database, just write the dirty data into database.
1150
   *
1151
   * @return bool|int <p>
1152
   *                  If update was successful, it will return the affected rows as int,
1153
   *                  otherwise it will return false or true (if there are no dirty data).
1154
   *                  </p>
1155
   */
1156 2
  public function update()
1157
  {
1158 2
    if (\count($this->dirty) == 0) {
1159
      return true;
1160
    }
1161
1162 2
    foreach ($this->dirty as $field => $value) {
1163 2
      $this->addCondition($field, '=', $value, ',', 'set');
1164
    }
1165
1166 2
    $result = $this->execute(
1167 2
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
1168
            [
1169 2
                'update',
1170
                'set',
1171
                'where',
1172
            ]
1173
        ),
1174 2
        $this->params
1175
    );
1176 2
    if ($result !== false) {
1177 2
      $this->resetDirty();
1178 2
      $this->reset();
1179
1180 2
      return $result;
1181
    }
1182
1183
    return false;
1184
  }
1185
1186
  /**
1187
   * Make wrap when build the SQL expressions of WHERE.
1188
   *
1189
   * @param string $op <p>If given, this param will build one "ActiveRecordExpressionsWrap" and include the stored
1190
   *                   expressions add into WHERE, otherwise it will stored the expressions into an array.</p>
1191
   *
1192
   * @return $this
1193
   */
1194 1
  public function wrap($op = null): self
1195
  {
1196 1
    if (1 === \func_num_args()) {
1197 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...
1198 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...
1199 1
        $this->_addCondition(
1200 1
            new ActiveRecordExpressionsWrap(
1201
                [
1202 1
                    'delimiter' => ' ',
1203 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...
1204
                ]
1205
            ),
1206 1
            'or' === \strtolower($op) ? 'OR' : 'AND'
1207
        );
1208
      }
1209 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...
1210
    } else {
1211 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...
1212
    }
1213
1214 1
    return $this;
1215
  }
1216
}
1217