Completed
Push — master ( ae7a6b...04cd6f )
by Lars
01:51
created

ActiveRecord::__set()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.0073

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 16
cts 17
cp 0.9412
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 4
nop 2
crap 6.0073
1
<?php
2
3
namespace voku\db;
4
5
use Arrayy\Arrayy;
6
use voku\db\exceptions\ActiveRecordException;
7
use voku\db\exceptions\FetchingException;
8
9
/**
10
 * A simple implement of active record via Arrayy.
11
 *
12
 * @method $this select(string $dbProperty)
13
 * @method $this eq(string $dbProperty, string | null $value = null)
14
 * @method $this from(string $table)
15
 * @method $this where(string $where)
16
 * @method $this having(string $having)
17
 * @method $this limit(int $start, int | null $end = null)
18
 *
19
 * @method $this equal(string $dbProperty, string $value)
20
 * @method $this notEqual(string $dbProperty, string $value)
21
 * @method $this ne(string $dbProperty, string $value)
22
 * @method $this greaterThan(string $dbProperty, int $value)
23
 * @method $this gt(string $dbProperty, int $value)
24
 * @method $this lessThan(string $dbProperty, int $value)
25
 * @method $this lt(string $dbProperty, int $value)
26
 * @method $this greaterThanOrEqual(string $dbProperty, int $value)
27
 * @method $this ge(string $dbProperty, int $value)
28
 * @method $this gte(string $dbProperty, int $value)
29
 * @method $this lessThanOrEqual(string $dbProperty, int $value)
30
 * @method $this le(string $dbProperty, int $value)
31
 * @method $this lte(string $dbProperty, int $value)
32
 * @method $this between(string $dbProperty, array $value)
33
 * @method $this like(string $dbProperty, string $value)
34
 * @method $this in(string $dbProperty, array $value)
35
 * @method $this notIn(string $dbProperty, array $value)
36
 * @method $this isnull(string $dbProperty)
37
 * @method $this isNotNull(string $dbProperty)
38
 * @method $this notNull(string $dbProperty)
39
 */
40
abstract class ActiveRecord extends Arrayy
41
{
42
  /**
43
   * @var DB static property to connect database.
44
   */
45
  protected static $db;
46
47
  /**
48
   * @var array maping the function name and the operator, to build Expressions in WHERE condition.
49
   *
50
   * user can call it like this:
51
   * <pre>
52
   *   $user->isnotnull()->eq('id', 1);
53
   * </pre>
54
   *
55
   * will create Expressions can explain to SQL:
56
   * <pre>
57
   *   WHERE user.id IS NOT NULL AND user.id = :ph1
58
   * </pre>
59
   */
60
  protected static $operators = array(
61
      'equal'              => '=',
62
      'eq'                 => '=',
63
      'notequal'           => '<>',
64
      'ne'                 => '<>',
65
      'greaterthan'        => '>',
66
      'gt'                 => '>',
67
      'lessthan'           => '<',
68
      'lt'                 => '<',
69
      'greaterthanorequal' => '>=',
70
      'ge'                 => '>=',
71
      'gte'                => '>=',
72
      'lessthanorequal'    => '<=',
73
      'le'                 => '<=',
74
      'lte'                => '<=',
75
      'between'            => 'BETWEEN',
76
      'like'               => 'LIKE',
77
      'in'                 => 'IN',
78
      'notin'              => 'NOT IN',
79
      'isnull'             => 'IS NULL',
80
      'isnotnull'          => 'IS NOT NULL',
81
      'notnull'            => 'IS NOT NULL',
82
  );
83
84
  /**
85
   * @var array Part of SQL, maping the function name and the operator to build SQL Part.
86
   * <pre>call function like this:
87
   *      $user->order('id DESC', 'name ASC')->limit(2, 1);
88
   *  can explain to SQL:
89
   *      ORDER BY id DESC, name ASC LIMIT 2,1</pre>
90
   */
91
  protected $sqlParts = array(
92
      'select' => 'SELECT',
93
      'from'   => 'FROM',
94
      'set'    => 'SET',
95
      'where'  => 'WHERE',
96
      'group'  => 'GROUP BY',
97
      'having' => 'HAVING',
98
      'order'  => 'ORDER BY',
99
      'limit'  => 'LIMIT',
100
      'top'    => 'TOP',
101
  );
102
103
  /**
104
   * @var array Static property to stored the default Sql Expressions values.
105
   */
106
  protected $defaultSqlExpressions = array(
107
      'expressions' => array(),
108
      'wrap'        => false,
109
      'select'      => null,
110
      'insert'      => null,
111
      'update'      => null,
112
      'set'         => null,
113
      'delete'      => 'DELETE ',
114
      'join'        => null,
115
      'from'        => null,
116
      'values'      => null,
117
      'where'       => null,
118
      'having'      => null,
119
      'limit'       => null,
120
      'order'       => null,
121
      'group'       => null,
122
  );
123
124
  /**
125
   * @var array Stored the Expressions of the SQL.
126
   */
127
  protected $sqlExpressions = array();
128
129
  /**
130
   * @var string  The table name in database.
131
   */
132
  protected $table;
133
134
  /**
135
   * @var string  The primary key of this ActiveRecord, just suport single primary key.
136
   */
137
  protected $primaryKeyName = 'id';
138
139
  /**
140
   * @var array Stored the drity data of this object, when call "insert" or "update" function, will write this data
141
   *      into database.
142
   */
143
  protected $dirty = array();
144
145
  /**
146
   * @var bool
147
   */
148
  protected static $new_data_are_dirty = true;
149
150
  /**
151
   * @var array Stored the params will bind to SQL when call DB->query(),
152
   */
153
  protected $params = array();
154
155
  /**
156
   * @var ActiveRecordExpressions[] Stored the configure of the relation, or target of the relation.
157
   */
158
  protected $relations = array();
159
160
  /**
161
   * @var int The count of bind params, using this count and const "PREFIX" (:ph) to generate place holder in SQL.
162
   */
163
  private static $count = 0;
164
165
  const BELONGS_TO = 'belongs_to';
166
  const HAS_MANY   = 'has_many';
167
  const HAS_ONE    = 'has_one';
168
169
  const PREFIX = ':active_record';
170
171
  /**
172
   * @return array
173
   */
174
  public function getParams()
175
  {
176
    return $this->params;
177
  }
178
179
  /**
180
   * @return string
181
   */
182
  public function getPrimaryKeyName()
183
  {
184
    return $this->primaryKeyName;
185
  }
186
187
  /**
188
   * @return mixed|null
189
   */
190 12
  public function getPrimaryKey()
191
  {
192 12
    $id = $this->{$this->primaryKeyName};
193 12
    if ($id) {
194 11
      return $id;
195
    }
196
197 1
    return null;
198
  }
199
200
  /**
201
   * @param mixed $primaryKey
202
   * @param bool  $dirty
203
   *
204
   * @return $this
205
   */
206 1
  public function setPrimaryKey($primaryKey, $dirty = true)
207
  {
208 1
    if ($dirty === true) {
209 1
      $this->dirty[$this->primaryKeyName] = $primaryKey;
210 1
    } else {
211
      $this->array[$this->primaryKeyName] = $primaryKey;
212
    }
213
214 1
    return $this;
215
  }
216
217
  /**
218
   * @return string
219
   */
220
  public function getTable()
221
  {
222
    return $this->table;
223
  }
224
225
  /**
226
   * Function to reset the $params and $sqlExpressions.
227
   *
228
   * @return $this
229
   */
230 22
  public function reset()
231
  {
232 22
    $this->params = array();
233 22
    $this->sqlExpressions = array();
234
235 22
    return $this;
236
  }
237
238
  /**
239
   * Reset the dirty data.
240
   *
241
   * @return $this
242
   */
243 5
  public function resetDirty()
244
  {
245 5
    $this->dirty = array();
246
247 5
    return $this;
248
  }
249
250
  /**
251
   * set the DB connection.
252
   *
253
   * @param DB $db
254
   */
255
  public static function setDb($db)
256
  {
257
    self::$db = $db;
258
  }
259
260
  /**
261
   * Function to find one record and assign in to current object.
262
   *
263
   * @param mixed $id <p>
264
   *                  If call this function using this param, we will find the record by using this id.
265
   *                  If not set, just find the first record in database.
266
   *                  </p>
267
   *
268
   * @return false|$this <p>
269
   *                     If we could find the record, assign in to current object and return it,
270
   *                     otherwise return "false".
271
   *                     </p>
272
   */
273 11 View Code Duplication
  public function fetch($id = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
  {
275 11
    if ($id) {
276 6
      $this->reset()->eq($this->primaryKeyName, $id);
277 6
    }
278
279 11
    return self::query(
280 11
        $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...
281
            array(
282 11
                'select',
283 11
                'from',
284 11
                'join',
285 11
                'where',
286 11
                'group',
287 11
                'having',
288 11
                'order',
289 11
                'limit',
290
            )
291 11
        ),
292 11
        $this->params,
293 11
        $this->reset(),
294
        true
295 11
    );
296
  }
297
298
  /**
299
   * @param string $query
300
   *
301
   * @return $this[]
302
   */
303 1
  public function fetchManyByQuery($query)
304
  {
305 1
    $list = $this->fetchByQuery($query);
306
307 1
    if (!$list || empty($list)) {
308
      return array();
309
    }
310
311 1
    return $list;
312
  }
313
314
  /**
315
   * @param string $query
316
   *
317
   * @return $this|null
318
   */
319 1
  public function fetchOneByQuery($query)
320
  {
321 1
    $list = $this->fetchByQuery($query);
322
323 1
    if (!$list || empty($list)) {
324
      return null;
325
    }
326
327 1
    if (is_array($list) && count($list) > 0) {
328 1
      $this->array = $list[0]->getArray();
329 1
    } else {
330
      $this->array = $list->getArray();
331
    }
332
333 1
    return $this;
334
  }
335
336
  /**
337
   * @param mixed $id
338
   *
339
   * @return $this
340
   *
341
   * @throws FetchingException <p>Will be thrown, if we can not find the id.</p>
342
   */
343 2
  public function fetchById($id)
344
  {
345 2
    $obj = $this->fetchByIdIfExists($id);
346 2
    if ($obj === null) {
347 1
      throw new FetchingException("No row with primary key '$id' in table '$this->table'.");
348
    }
349
350 1
    return $obj;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $obj; (boolean|voku\db\ActiveRecord|array) is incompatible with the return type documented by voku\db\ActiveRecord::fetchById of type voku\db\ActiveRecord.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
351
  }
352
353
  /**
354
   * @param mixed $id
355
   *
356
   * @return $this|null
357
   */
358 4
  public function fetchByIdIfExists($id)
359
  {
360 4
    $list = $this->fetch($id);
361
362 4
    if (!$list || $list->isEmpty()) {
363 2
      return null;
364
    }
365
366 2
    return $list;
367
  }
368
369
  /**
370
   * @param array $ids
371
   *
372
   * @return $this[]
373
   */
374 2
  public function fetchByIds($ids)
375
  {
376 2
    if (empty($ids)) {
377
      return array();
378
    }
379
380 2
    $list = $this->fetchAll($ids);
381 2
    if (is_array($list) && count($list) > 0) {
382 1
      return $list;
383
    }
384
385 1
    return array();
386
  }
387
388
  /**
389
   * @param string $query
390
   *
391
   * @return $this[]|$this
392
   */
393 2
  public function fetchByQuery($query)
394
  {
395 2
    $list = self::query(
396 2
        $query,
397 2
        $this->params,
398 2
        $this->reset()
399 2
    );
400
401 2
    if (is_array($list)) {
402 2
      if (count($list) === 0) {
403
        return array();
404
      }
405
406 2
      return $list;
407
    }
408
409
    $this->array = $list->getArray();
410
411
    return $this;
412
  }
413
414
  /**
415
   * @param array $ids
416
   *
417
   * @return $this[]
418
   */
419 1
  public function fetchByIdsPrimaryKeyAsArrayIndex($ids)
420
  {
421 1
    $result = $this->fetchAll($ids);
422
423 1
    $resultNew = array();
424 1
    foreach ($result as $item) {
0 ignored issues
show
Bug introduced by
The expression $result of type boolean|object<voku\db\ActiveRecord>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
425 1
      $resultNew[$item->getPrimaryKey()] = $item;
426 1
    }
427
428 1
    return $resultNew;
429
  }
430
431
  /**
432
   * Function to find all records in database.
433
   *
434
   * @param array|null $ids <p>
435
   *                        If call this function using this param, we will find the record by using this id's.
436
   *                        If not set, just find all records in database.
437
   *                        </p>
438
   *
439
   * @return $this[]
440
   */
441 6 View Code Duplication
  public function fetchAll(array $ids = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
442
  {
443 6
    if ($ids) {
444 3
      $this->reset()->in($this->primaryKeyName, $ids);
445 3
    }
446
447 6
    return self::query(
448 6
        $this->_buildSql(
449
            array(
450 6
                'select',
451 6
                'from',
452 6
                'join',
453 6
                'where',
454 6
                'group',
455 6
                'having',
456 6
                'order',
457 6
                'limit',
458
            )
459 6
        ),
460 6
        $this->params,
461 6
        $this->reset()
462 6
    );
463
  }
464
465
  /**
466
   * Function to delete current record in database.
467
   *
468
   * @return bool
469
   */
470 1
  public function delete()
471
  {
472 1
    return self::execute(
473 1
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
474
            array(
475 1
                'delete',
476 1
                'from',
477 1
                'where',
478
            )
479 1
        ),
480 1
        $this->params
481 1
    );
482
  }
483
484
  /**
485
   * @param string $primaryKeyName
486
   *
487
   * @return $this
488
   */
489
  public function setPrimaryKeyName($primaryKeyName)
490
  {
491
    $this->primaryKeyName = $primaryKeyName;
492
493
    return $this;
494
  }
495
496
  /**
497
   * @param string $table
498
   */
499
  public function setTable($table)
500
  {
501
    $this->table = $table;
502
  }
503
504
  /**
505
   * Function to build update SQL, and update current record in database, just write the dirty data into database.
506
   *
507
   * @return bool|int <p>
508
   *                  If update was successful, it will return the affected rows as int,
509
   *                  otherwise it will return false or true (if there are no dirty data).
510
   *                  </p>
511
   */
512 2
  public function update()
513
  {
514 2
    if (count($this->dirty) == 0) {
515
      return true;
516
    }
517
518 2
    foreach ($this->dirty as $field => $value) {
519 2
      $this->addCondition($field, '=', $value, ',', 'set');
520 2
    }
521
522 2
    $result = self::execute(
523 2
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
524
            array(
525 2
                'update',
526 2
                'set',
527 2
                'where',
528
            )
529 2
        ),
530 2
        $this->params
531 2
    );
532 2
    if ($result) {
533 2
      $this->resetDirty();
534 2
      $this->reset();
535
536 2
      return $result;
537
    }
538
539
    return false;
540
  }
541
542
  /**
543
   * Function to build insert SQL, and insert current record into database.
544
   *
545
   * @return bool|int <p>
546
   *                  If insert was successful, it will return the new id,
547
   *                  otherwise it will return false or true (if there are no dirty data).
548
   *                  </p>
549
   */
550 3
  public function insert()
551
  {
552 3
    if (!self::$db instanceof DB) {
553
      self::$db = DB::getInstance();
554
    }
555
556 3
    if (count($this->dirty) === 0) {
557
      return true;
558
    }
559
560 3
    $value = $this->_filterParam($this->dirty);
561 3
    $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...
562
        array(
563 3
            'operator' => 'INSERT INTO ' . $this->table,
564 3
            'target'   => new ActiveRecordExpressionsWrap(array('target' => array_keys($this->dirty))),
565
        )
566 3
    );
567 3
    $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...
568
        array(
569 3
            'operator' => 'VALUES',
570 3
            'target'   => new ActiveRecordExpressionsWrap(array('target' => $value)),
571
        )
572 3
    );
573
574 3
    $result = self::execute($this->_buildSql(array('insert', 'values')), $this->params);
575 3
    if ($result) {
576 3
      $this->{$this->primaryKeyName} = $result;
577
578 3
      $this->resetDirty();
579 3
      $this->reset();
580
581 3
      return $result;
582
    }
583
584
    return false;
585
  }
586
587
  /**
588
   * Helper function to copy an existing active record (and insert it into the database).
589
   *
590
   * @param bool $insert
591
   *
592
   * @return $this
593
   */
594 1
  public function copy($insert = true)
595
  {
596 1
    $new = clone $this;
597
598 1
    if ($insert) {
599 1
      $new->setPrimaryKey(null);
600 1
      $id = $new->insert();
601 1
      $new->setPrimaryKey($id);
602 1
    }
603
604 1
    return $new;
605
  }
606
607
  /**
608
   * Helper function to exec sql.
609
   *
610
   * @param string $sql   The SQL need to be execute.
611
   * @param array  $param The param will be bind to the sql statement.
612
   *
613
   * @return bool|int|Result              <p>
614
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
615
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
616
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
617
   *                                      "true" by e.g. "DROP"-queries<br />
618
   *                                      "false" on error
619
   *                                      </p>
620
   */
621 23
  public static function execute($sql, array $param = array())
622
  {
623 23
    if (!self::$db instanceof DB) {
624 1
      self::$db = DB::getInstance();
625 1
    }
626
627 23
    return self::$db->query($sql, $param);
628
  }
629
630
  /**
631
   * Helper function to query one record by sql and params.
632
   *
633
   * @param string            $sql    <p>
634
   *                                  The SQL query to find the record.
635
   *                                  </p>
636
   * @param array             $param  <p>
637
   *                                  The param will be bind to the $sql query.
638
   *                                  </p>
639
   * @param ActiveRecord|null $obj    <p>
640
   *                                  The object, if find record in database, we will assign the attributes into
641
   *                                  this object.
642
   *                                  </p>
643
   * @param bool              $single <p>
644
   *                                  If set to true, we will find record and fetch in current object, otherwise
645
   *                                  will find all records.
646
   *                                  </p>
647
   *
648
   * @return bool|$this|array
649
   */
650 17
  public static function query($sql, array $param = array(), ActiveRecord $obj = null, $single = false)
651
  {
652 17
    $result = self::execute($sql, $param);
653
654 17
    if (!$result) {
655
      return false;
656
    }
657
658 17
    $useObject = is_object($obj);
659 17
    if ($useObject === true) {
660 17
      $called_class = $obj;
661 17
    } else {
662
      $called_class = get_called_class();
663
    }
664
665 17
    self::setNewDataAreDirty(false);
666
667 17
    if ($single) {
668 11
      $return = $result->fetchObject($called_class, null, true);
0 ignored issues
show
Bug introduced by
It seems like $called_class defined by $obj on line 660 can also be of type null; however, voku\db\Result::fetchObject() does only seem to accept string|object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
669 11
    } else {
670 8
      $return = $result->fetchAllObject($called_class, null);
0 ignored issues
show
Bug introduced by
It seems like $called_class defined by $obj on line 660 can also be of type null; however, voku\db\Result::fetchAllObject() does only seem to accept string|object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
671
    }
672
673 17
    self::setNewDataAreDirty(true);
674
675 17
    return $return;
676
  }
677
678
  /**
679
   * Helper function to get relation of this object.
680
   * There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
681
   *
682
   * @param string $name The name of the relation, the array key when defind the relation.
683
   *
684
   * @return mixed
685
   *
686
   * @throws ActiveRecordException <p>If the relation can't be found .</p>
687
   */
688 3
  protected function &getRelation($name)
689
  {
690 3
    $relation = $this->relations[$name];
691
    if (
692
        $relation instanceof self
693 3
        ||
694
        (
695 2
            is_array($relation)
696 2
            &&
697 2
            $relation[0] instanceof self
698 2
        )
699 3
    ) {
700 3
      return $relation;
701
    }
702
703
    /* @var $obj ActiveRecord */
704 2
    $obj = new $relation[1];
705
706 2
    $this->relations[$name] = $obj;
707 2
    if (isset($relation[3]) && is_array($relation[3])) {
708 1
      foreach ((array)$relation[3] as $func => $args) {
709 1
        call_user_func_array(array($obj, $func), (array)$args);
710 1
      }
711 1
    }
712
713 2
    $backref = isset($relation[4]) ? $relation[4] : '';
714
    if (
715 2
        (!$relation instanceof self)
716 2
        &&
717 2
        self::HAS_ONE == $relation[0]
718 2
    ) {
719
720 1
      $this->relations[$name] = $obj->eq($relation[2], $this->{$this->primaryKeyName})->fetch();
721
722 1
      if ($backref) {
723
        $this->relations[$name] && $backref && $obj->__set($backref, $this);
724
      }
725
726 1
    } elseif (
727 2
        is_array($relation)
728 2
        &&
729 2
        self::HAS_MANY == $relation[0]
730 2
    ) {
731
732 2
      $this->relations[$name] = $obj->eq($relation[2], $this->{$this->primaryKeyName})->fetchAll();
733 2
      if ($backref) {
734 1
        foreach ($this->relations[$name] as $o) {
0 ignored issues
show
Bug introduced by
The expression $this->relations[$name] of type object<voku\db\ActiveRec...veRecord>|boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
735 1
          $o->__set($backref, $this);
736 1
        }
737 1
      }
738
739 2
    } elseif (
740 2
        (!$relation instanceof self)
741 2
        &&
742 2
        self::BELONGS_TO == $relation[0]
743 2
    ) {
744
745 2
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
746
747 2
      if ($backref) {
748 1
        $this->relations[$name] && $backref && $obj->__set($backref, $this);
749 1
      }
750
751 2
    } else {
752
      throw new ActiveRecordException("Relation $name not found.");
753
    }
754
755 2
    return $this->relations[$name];
756
  }
757
758
  /**
759
   * Helper function to build SQL with sql parts.
760
   *
761
   * @param string       $n The SQL part will be build.
762
   * @param int          $i The index of $n in $sql array.
763
   * @param ActiveRecord $o The reference to $this
764
   */
765 20
  private function _buildSqlCallback(&$n, $i, $o)
766
  {
767
    if (
768
        'select' === $n
769 20
        &&
770 15
        null === $o->$n
771 20
    ) {
772
773 14
      $n = strtoupper($n) . ' ' . $o->table . '.*';
774
775 14
    } elseif (
776
        (
777
            'update' === $n
778 20
            ||
779
            'from' === $n
780 20
        )
781 20
        &&
782 17
        null === $o->$n
783 20
    ) {
784
785 17
      $n = strtoupper($n) . ' ' . $o->table;
786
787 20
    } elseif ('delete' === $n) {
788
789 1
      $n = strtoupper($n) . ' ';
790
791 1
    } else {
792
793 20
      $n = (null !== $o->$n) ? $o->$n . ' ' : '';
794
795
    }
796 20
  }
797
798
  /**
799
   * Helper function to build SQL with sql parts.
800
   *
801
   * @param array $sqls The SQL part will be build.
802
   *
803
   * @return string
804
   */
805 20
  protected function _buildSql($sqls = array())
806
  {
807 20
    array_walk($sqls, array($this, '_buildSqlCallback'), $this);
808
809
    // DEBUG
810
    //echo 'SQL: ', implode(' ', $sqls), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
811
812 20
    return implode(' ', $sqls);
813
  }
814
815
  /**
816
   * Magic function to make calls witch in function mapping stored in $operators and $sqlPart.
817
   * also can call function of DB object.
818
   *
819
   * @param string $name function name
820
   * @param array  $args The arguments of the function.
821
   *
822
   * @return $this|mixed Return the result of callback or the current object to make chain method calls.
823
   *
824
   * @throws ActiveRecordException
825
   */
826 16
  public function __call($name, $args)
827
  {
828 16
    if (!self::$db instanceof DB) {
829
      self::$db = DB::getInstance();
830
    }
831
832 16
    $nameTmp = strtolower($name);
833
834 16
    if (array_key_exists($nameTmp, self::$operators)) {
835
836 14
      $this->addCondition(
837 14
          $args[0],
838 14
          self::$operators[$nameTmp],
839 14
          isset($args[1]) ? $args[1] : null,
840 14
          (is_string(end($args)) && 'or' === strtolower(end($args))) ? 'OR' : 'AND'
841 14
      );
842
843 16
    } elseif (array_key_exists($nameTmp = str_replace('by', '', $nameTmp), $this->sqlParts)) {
844
845 11
      $this->$name = new ActiveRecordExpressions(
846
          array(
847 11
              'operator' => $this->sqlParts[$nameTmp],
848 11
              'target'   => implode(', ', $args),
849
          )
850 11
      );
851
852 11
    } elseif (is_callable($callback = array(self::$db, $name))) {
853
854
      return call_user_func_array($callback, $args);
855
856
    } else {
857
858
      throw new ActiveRecordException("Method $name not exist.");
859
860
    }
861
862 16
    return $this;
863
  }
864
865
  /**
866
   * Make wrap when build the SQL expressions of WHERE.
867
   *
868
   * @param string $op If give this param will build one WrapExpressions include the stored expressions add into WHERE.
869
   *                   otherwise wil stored the expressions into array.
870
   *
871
   * @return $this
872
   */
873 1
  public function wrap($op = null)
874
  {
875 1
    if (1 === func_num_args()) {
876 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...
877 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...
878 1
        $this->_addCondition(
879 1
            new ActiveRecordExpressionsWrap(
880
                array(
881 1
                    'delimiter' => ' ',
882 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...
883
                )
884 1
            ), 'or' === strtolower($op) ? 'OR' : 'AND'
885 1
        );
886 1
      }
887 1
      $this->expressions = array();
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...
888 1
    } else {
889 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...
890
    }
891
892 1
    return $this;
893
  }
894
895
  /**
896
   * Helper function to build place holder when make SQL expressions.
897
   *
898
   * @param mixed $value The value will bind to SQL, just store it in $this->params.
899
   *
900
   * @return mixed $value
901
   */
902 17
  protected function _filterParam($value)
903
  {
904 17
    if (is_array($value)) {
905 7
      foreach ($value as $key => $val) {
906 7
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
907 7
      }
908 17
    } elseif (is_string($value)) {
909 3
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
910 3
      $value = $ph;
911 3
    }
912
913 17
    return $value;
914
  }
915
916
  /**
917
   * Helper function to add condition into WHERE.
918
   * create the SQL Expressions.
919
   *
920
   * @param string $field The field name, the source of Expressions
921
   * @param string $operator
922
   * @param mixed  $value The target of the Expressions
923
   * @param string $op    The operator to concat this Expressions into WHERE or SET statement.
924
   * @param string $name  The Expression will contact to.
925
   */
926 14
  public function addCondition($field, $operator, $value, $op = 'AND', $name = 'where')
927
  {
928 14
    $value = $this->_filterParam($value);
929 14
    $exp = new ActiveRecordExpressions(
930
        array(
931 14
            'source'   => ('where' == $name ? $this->table . '.' : '') . $field,
932 14
            'operator' => $operator,
933 14
            'target'   => is_array($value)
934 14
                ? new ActiveRecordExpressionsWrap(
935 4
                    'between' === strtolower($operator)
936 4
                        ? array('target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ')
937 4
                        : array('target' => $value)
938 14
                ) : $value,
939
        )
940 14
    );
941 14
    if ($exp) {
942 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...
943 14
        $this->_addCondition($exp, $op, $name);
944 14
      } else {
945 1
        $this->_addExpression($exp, $op);
946
      }
947 14
    }
948 14
  }
949
950
  /**
951
   * helper function to add condition into JOIN.
952
   * create the SQL Expressions.
953
   *
954
   * @param string $table The join table name
955
   * @param string $on    The condition of ON
956
   * @param string $type  The join type, like "LEFT", "INNER", "OUTER"
957
   *
958
   * @return $this
959
   */
960 1
  public function join($table, $on, $type = 'LEFT')
961
  {
962 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...
963
        array(
964 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...
965 1
            'operator' => $type . ' JOIN',
966 1
            'target'   => new ActiveRecordExpressions(
967 1
                array('source' => $table, 'operator' => 'ON', 'target' => $on)
968 1
            ),
969
        )
970 1
    );
971
972 1
    return $this;
973
  }
974
975
  /**
976
   * helper function to make wrapper. Stored the expression in to array.
977
   *
978
   * @param ActiveRecordExpressions $exp      The expression will be stored.
979
   * @param string                  $operator The operator to concat this Expressions into WHERE statment.
980
   */
981 1
  protected function _addExpression($exp, $operator)
982
  {
983 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...
984 1
      $this->expressions = array($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...
985 1
    } else {
986 1
      $this->expressions[] = new ActiveRecordExpressions(array('operator' => $operator, 'target' => $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...
987
    }
988 1
  }
989
990
  /**
991
   * helper function to add condition into WHERE.
992
   *
993
   * @param ActiveRecordExpressions $exp      The expression will be concat into WHERE or SET statment.
994
   * @param string                  $operator the operator to concat this Expressions into WHERE or SET statment.
995
   * @param string                  $name     The Expression will contact to.
996
   */
997 14
  protected function _addCondition($exp, $operator, $name = 'where')
998
  {
999 14
    if (!$this->$name) {
1000 14
      $this->$name = new ActiveRecordExpressions(array('operator' => strtoupper($name), 'target' => $exp));
1001 14
    } else {
1002 4
      $this->$name->target = new ActiveRecordExpressions(
1003
          array(
1004 4
              'source'   => $this->$name->target,
1005 4
              'operator' => $operator,
1006 4
              'target'   => $exp,
1007
          )
1008 4
      );
1009
    }
1010 14
  }
1011
1012
  /**
1013
   * @return array
1014
   */
1015 1
  public function getDirty()
1016
  {
1017 1
    return $this->dirty;
1018
  }
1019
1020
  /**
1021
   * @return bool
1022
   */
1023
  public static function isNewDataAreDirty()
1024
  {
1025
    return self::$new_data_are_dirty;
1026
  }
1027
1028
  /**
1029
   * @param bool $bool
1030
   */
1031 17
  public static function setNewDataAreDirty($bool)
1032
  {
1033 17
    self::$new_data_are_dirty = (bool)$bool;
1034 17
  }
1035
1036
  /**
1037
   * Magic function to SET values of the current object.
1038
   *
1039
   * @param mixed $var
1040
   * @param mixed $val
1041
   */
1042 22
  public function __set($var, $val)
1043
  {
1044
    if (
1045 22
        array_key_exists($var, $this->sqlExpressions)
1046
        ||
1047 22
        array_key_exists($var, $this->defaultSqlExpressions)
1048 22
    ) {
1049
1050 19
      $this->sqlExpressions[$var] = $val;
1051
1052 19
    } elseif (
1053 19
        array_key_exists($var, $this->relations)
1054 19
        &&
1055
        $val instanceof self
1056 19
    ) {
1057
1058 1
      $this->relations[$var] = $val;
1059
1060 1
    } else {
1061
1062 19
      $this->array[$var] = $val;
1063
1064 19
      if (self::$new_data_are_dirty === true) {
1065 6
        $this->dirty[$var] = $val;
1066 6
      }
1067
1068
    }
1069 22
  }
1070
1071
  /**
1072
   * Magic function to UNSET values of the current object.
1073
   *
1074
   * @param mixed $var
1075
   */
1076 1
  public function __unset($var)
1077
  {
1078 1
    if (array_key_exists($var, $this->sqlExpressions)) {
1079
      unset($this->sqlExpressions[$var]);
1080
    }
1081
1082 1
    if (isset($this->array[$var])) {
1083 1
      unset($this->array[$var]);
1084 1
    }
1085
1086 1
    if (isset($this->dirty[$var])) {
1087 1
      unset($this->dirty[$var]);
1088 1
    }
1089 1
  }
1090
1091
  /**
1092
   * Helper function for "GROUP BY".
1093
   *
1094
   * @param array $args
1095
   * @param null  $dummy <p>only needed for API compatibility with Arrayy</p>
1096
   *
1097
   * @return $this
1098
   */
1099
  public function group($args, $dummy = null)
1100
  {
1101
    $this->__call('group', func_get_args());
1102
1103
    return $this;
1104
  }
1105
1106
  /**
1107
   * Helper function for "ORDER BY".
1108
   *
1109
   * @param $args ...
1110
   *
1111
   * @return $this
1112
   */
1113 2
  public function order($args)
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...
1114
  {
1115 2
    $this->__call('order', func_get_args());
1116
1117 2
    return $this;
1118
  }
1119
1120
  /**
1121
   * Magic function to GET the values of current object.
1122
   *
1123
   * @param $var
1124
   *
1125
   * @return mixed
1126
   */
1127 22
  public function &__get($var)
1128
  {
1129 22
    if (array_key_exists($var, $this->sqlExpressions)) {
1130 19
      return $this->sqlExpressions[$var];
1131
    }
1132
1133 22
    if (array_key_exists($var, $this->relations)) {
1134 3
      return $this->getRelation($var);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getRelation($var); of type voku\db\ActiveRecordExpr...iveRecord|boolean|array adds the type array to the return on line 1134 which is incompatible with the return type of the parent method Arrayy\Arrayy::__get of type object|integer|double|string|null|boolean.
Loading history...
1135
    }
1136
1137 22
    if (isset($this->dirty[$var])) {
1138 3
      return $this->dirty[$var];
1139
    }
1140
1141 22
    return parent::__get($var);
1142
  }
1143
}
1144