Completed
Push — master ( b61720...c5e1ae )
by Lars
09:19 queued 19s
created

ActiveRecord::fetchByIdsPrimaryKeyAsArrayIndex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 2
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 13
  public function getPrimaryKey()
191
  {
192 13
    $id = $this->{$this->primaryKeyName};
193 13
    if ($id) {
194 12
      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 23
  public function reset()
231
  {
232 23
    $this->params = array();
233 23
    $this->sqlExpressions = array();
234
235 23
    return $this;
236
  }
237
238
  /**
239
   * Reset the dirty data.
240
   *
241
   * @return $this
242
   */
243 6
  public function resetDirty()
244
  {
245 6
    $this->dirty = array();
246
247 6
    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
   * @return $this
544
   */
545 1
  public static function fetchEmpty()
546
  {
547 1
    $class = get_called_class();
548 1
    return new $class;
549
  }
550
551
  /**
552
   * Function to build insert SQL, and insert current record into database.
553
   *
554
   * @return bool|int <p>
555
   *                  If insert was successful, it will return the new id,
556
   *                  otherwise it will return false or true (if there are no dirty data).
557
   *                  </p>
558
   */
559 4
  public function insert()
560
  {
561 4
    if (!self::$db instanceof DB) {
562
      self::$db = DB::getInstance();
563
    }
564
565 4
    if (count($this->dirty) === 0) {
566
      return true;
567
    }
568
569 4
    $value = $this->_filterParam($this->dirty);
570 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...
571
        array(
572 4
            'operator' => 'INSERT INTO ' . $this->table,
573 4
            'target'   => new ActiveRecordExpressionsWrap(array('target' => array_keys($this->dirty))),
574
        )
575 4
    );
576 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...
577
        array(
578 4
            'operator' => 'VALUES',
579 4
            'target'   => new ActiveRecordExpressionsWrap(array('target' => $value)),
580
        )
581 4
    );
582
583 4
    $result = self::execute($this->_buildSql(array('insert', 'values')), $this->params);
584 4
    if ($result) {
585 4
      $this->{$this->primaryKeyName} = $result;
586
587 4
      $this->resetDirty();
588 4
      $this->reset();
589
590 4
      return $result;
591
    }
592
593
    return false;
594
  }
595
596
  /**
597
   * Helper function to copy an existing active record (and insert it into the database).
598
   *
599
   * @param bool $insert
600
   *
601
   * @return $this
602
   */
603 1
  public function copy($insert = true)
604
  {
605 1
    $new = clone $this;
606
607 1
    if ($insert) {
608 1
      $new->setPrimaryKey(null);
609 1
      $id = $new->insert();
610 1
      $new->setPrimaryKey($id);
611 1
    }
612
613 1
    return $new;
614
  }
615
616
  /**
617
   * Helper function to exec sql.
618
   *
619
   * @param string $sql   The SQL need to be execute.
620
   * @param array  $param The param will be bind to the sql statement.
621
   *
622
   * @return bool|int|Result              <p>
623
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
624
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
625
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
626
   *                                      "true" by e.g. "DROP"-queries<br />
627
   *                                      "false" on error
628
   *                                      </p>
629
   */
630 24
  public static function execute($sql, array $param = array())
631
  {
632 24
    if (!self::$db instanceof DB) {
633 1
      self::$db = DB::getInstance();
634 1
    }
635
636 24
    return self::$db->query($sql, $param);
637
  }
638
639
  /**
640
   * Helper function to query one record by sql and params.
641
   *
642
   * @param string            $sql    <p>
643
   *                                  The SQL query to find the record.
644
   *                                  </p>
645
   * @param array             $param  <p>
646
   *                                  The param will be bind to the $sql query.
647
   *                                  </p>
648
   * @param ActiveRecord|null $obj    <p>
649
   *                                  The object, if find record in database, we will assign the attributes into
650
   *                                  this object.
651
   *                                  </p>
652
   * @param bool              $single <p>
653
   *                                  If set to true, we will find record and fetch in current object, otherwise
654
   *                                  will find all records.
655
   *                                  </p>
656
   *
657
   * @return bool|$this|array
658
   */
659 17
  public static function query($sql, array $param = array(), ActiveRecord $obj = null, $single = false)
660
  {
661 17
    $result = self::execute($sql, $param);
662
663 17
    if (!$result) {
664
      return false;
665
    }
666
667 17
    $useObject = is_object($obj);
668 17
    if ($useObject === true) {
669 17
      $called_class = $obj;
670 17
    } else {
671
      $called_class = get_called_class();
672
    }
673
674 17
    self::setNewDataAreDirty(false);
675
676 17
    if ($single) {
677 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 669 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...
678 11
    } else {
679 8
      $return = $result->fetchAllObject($called_class, null);
0 ignored issues
show
Bug introduced by
It seems like $called_class defined by $obj on line 669 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...
680
    }
681
682 17
    self::setNewDataAreDirty(true);
683
684 17
    return $return;
685
  }
686
687
  /**
688
   * Helper function to get relation of this object.
689
   * There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
690
   *
691
   * @param string $name The name of the relation, the array key when defind the relation.
692
   *
693
   * @return mixed
694
   *
695
   * @throws ActiveRecordException <p>If the relation can't be found .</p>
696
   */
697 3
  protected function &getRelation($name)
698
  {
699 3
    $relation = $this->relations[$name];
700
    if (
701
        $relation instanceof self
702 3
        ||
703
        (
704 2
            is_array($relation)
705 2
            &&
706 2
            $relation[0] instanceof self
707 2
        )
708 3
    ) {
709 3
      return $relation;
710
    }
711
712
    /* @var $obj ActiveRecord */
713 2
    $obj = new $relation[1];
714
715 2
    $this->relations[$name] = $obj;
716 2
    if (isset($relation[3]) && is_array($relation[3])) {
717 1
      foreach ((array)$relation[3] as $func => $args) {
718 1
        call_user_func_array(array($obj, $func), (array)$args);
719 1
      }
720 1
    }
721
722 2
    $backref = isset($relation[4]) ? $relation[4] : '';
723
    if (
724 2
        (!$relation instanceof self)
725 2
        &&
726 2
        self::HAS_ONE == $relation[0]
727 2
    ) {
728
729 1
      $this->relations[$name] = $obj->eq($relation[2], $this->{$this->primaryKeyName})->fetch();
730
731 1
      if ($backref) {
732
        $this->relations[$name] && $backref && $obj->__set($backref, $this);
733
      }
734
735 1
    } elseif (
736 2
        is_array($relation)
737 2
        &&
738 2
        self::HAS_MANY == $relation[0]
739 2
    ) {
740
741 2
      $this->relations[$name] = $obj->eq($relation[2], $this->{$this->primaryKeyName})->fetchAll();
742 2
      if ($backref) {
743 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...
744 1
          $o->__set($backref, $this);
745 1
        }
746 1
      }
747
748 2
    } elseif (
749 2
        (!$relation instanceof self)
750 2
        &&
751 2
        self::BELONGS_TO == $relation[0]
752 2
    ) {
753
754 2
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
755
756 2
      if ($backref) {
757 1
        $this->relations[$name] && $backref && $obj->__set($backref, $this);
758 1
      }
759
760 2
    } else {
761
      throw new ActiveRecordException("Relation $name not found.");
762
    }
763
764 2
    return $this->relations[$name];
765
  }
766
767
  /**
768
   * Helper function to build SQL with sql parts.
769
   *
770
   * @param string       $n The SQL part will be build.
771
   * @param int          $i The index of $n in $sql array.
772
   * @param ActiveRecord $o The reference to $this
773
   */
774 21
  private function _buildSqlCallback(&$n, $i, $o)
775
  {
776
    if (
777
        'select' === $n
778 21
        &&
779 15
        null === $o->$n
780 21
    ) {
781
782 14
      $n = strtoupper($n) . ' ' . $o->table . '.*';
783
784 14
    } elseif (
785
        (
786
            'update' === $n
787 21
            ||
788
            'from' === $n
789 21
        )
790 21
        &&
791 17
        null === $o->$n
792 21
    ) {
793
794 17
      $n = strtoupper($n) . ' ' . $o->table;
795
796 21
    } elseif ('delete' === $n) {
797
798 1
      $n = strtoupper($n) . ' ';
799
800 1
    } else {
801
802 21
      $n = (null !== $o->$n) ? $o->$n . ' ' : '';
803
804
    }
805 21
  }
806
807
  /**
808
   * Helper function to build SQL with sql parts.
809
   *
810
   * @param array $sqls The SQL part will be build.
811
   *
812
   * @return string
813
   */
814 21
  protected function _buildSql($sqls = array())
815
  {
816 21
    array_walk($sqls, array($this, '_buildSqlCallback'), $this);
817
818
    // DEBUG
819
    //echo 'SQL: ', implode(' ', $sqls), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
820
821 21
    return implode(' ', $sqls);
822
  }
823
824
  /**
825
   * Magic function to make calls witch in function mapping stored in $operators and $sqlPart.
826
   * also can call function of DB object.
827
   *
828
   * @param string $name function name
829
   * @param array  $args The arguments of the function.
830
   *
831
   * @return $this|mixed Return the result of callback or the current object to make chain method calls.
832
   *
833
   * @throws ActiveRecordException
834
   */
835 16
  public function __call($name, $args)
836
  {
837 16
    if (!self::$db instanceof DB) {
838
      self::$db = DB::getInstance();
839
    }
840
841 16
    $nameTmp = strtolower($name);
842
843 16
    if (array_key_exists($nameTmp, self::$operators)) {
844
845 14
      $this->addCondition(
846 14
          $args[0],
847 14
          self::$operators[$nameTmp],
848 14
          isset($args[1]) ? $args[1] : null,
849 14
          (is_string(end($args)) && 'or' === strtolower(end($args))) ? 'OR' : 'AND'
850 14
      );
851
852 16
    } elseif (array_key_exists($nameTmp = str_replace('by', '', $nameTmp), $this->sqlParts)) {
853
854 11
      $this->$name = new ActiveRecordExpressions(
855
          array(
856 11
              'operator' => $this->sqlParts[$nameTmp],
857 11
              'target'   => implode(', ', $args),
858
          )
859 11
      );
860
861 11
    } elseif (is_callable($callback = array(self::$db, $name))) {
862
863
      return call_user_func_array($callback, $args);
864
865
    } else {
866
867
      throw new ActiveRecordException("Method $name not exist.");
868
869
    }
870
871 16
    return $this;
872
  }
873
874
  /**
875
   * Make wrap when build the SQL expressions of WHERE.
876
   *
877
   * @param string $op If give this param will build one WrapExpressions include the stored expressions add into WHERE.
878
   *                   otherwise wil stored the expressions into array.
879
   *
880
   * @return $this
881
   */
882 1
  public function wrap($op = null)
883
  {
884 1
    if (1 === func_num_args()) {
885 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...
886 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...
887 1
        $this->_addCondition(
888 1
            new ActiveRecordExpressionsWrap(
889
                array(
890 1
                    'delimiter' => ' ',
891 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...
892
                )
893 1
            ), 'or' === strtolower($op) ? 'OR' : 'AND'
894 1
        );
895 1
      }
896 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...
897 1
    } else {
898 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...
899
    }
900
901 1
    return $this;
902
  }
903
904
  /**
905
   * Helper function to build place holder when make SQL expressions.
906
   *
907
   * @param mixed $value The value will bind to SQL, just store it in $this->params.
908
   *
909
   * @return mixed $value
910
   */
911 18
  protected function _filterParam($value)
912
  {
913 18
    if (is_array($value)) {
914 8
      foreach ($value as $key => $val) {
915 8
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
916 8
      }
917 18
    } elseif (is_string($value)) {
918 3
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
919 3
      $value = $ph;
920 3
    }
921
922 18
    return $value;
923
  }
924
925
  /**
926
   * Helper function to add condition into WHERE.
927
   * create the SQL Expressions.
928
   *
929
   * @param string $field The field name, the source of Expressions
930
   * @param string $operator
931
   * @param mixed  $value The target of the Expressions
932
   * @param string $op    The operator to concat this Expressions into WHERE or SET statement.
933
   * @param string $name  The Expression will contact to.
934
   */
935 14
  public function addCondition($field, $operator, $value, $op = 'AND', $name = 'where')
936
  {
937 14
    $value = $this->_filterParam($value);
938 14
    $exp = new ActiveRecordExpressions(
939
        array(
940 14
            'source'   => ('where' == $name ? $this->table . '.' : '') . $field,
941 14
            'operator' => $operator,
942 14
            'target'   => is_array($value)
943 14
                ? new ActiveRecordExpressionsWrap(
944 4
                    'between' === strtolower($operator)
945 4
                        ? array('target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ')
946 4
                        : array('target' => $value)
947 14
                ) : $value,
948
        )
949 14
    );
950 14
    if ($exp) {
951 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...
952 14
        $this->_addCondition($exp, $op, $name);
953 14
      } else {
954 1
        $this->_addExpression($exp, $op);
955
      }
956 14
    }
957 14
  }
958
959
  /**
960
   * helper function to add condition into JOIN.
961
   * create the SQL Expressions.
962
   *
963
   * @param string $table The join table name
964
   * @param string $on    The condition of ON
965
   * @param string $type  The join type, like "LEFT", "INNER", "OUTER"
966
   *
967
   * @return $this
968
   */
969 1
  public function join($table, $on, $type = 'LEFT')
970
  {
971 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...
972
        array(
973 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...
974 1
            'operator' => $type . ' JOIN',
975 1
            'target'   => new ActiveRecordExpressions(
976 1
                array('source' => $table, 'operator' => 'ON', 'target' => $on)
977 1
            ),
978
        )
979 1
    );
980
981 1
    return $this;
982
  }
983
984
  /**
985
   * helper function to make wrapper. Stored the expression in to array.
986
   *
987
   * @param ActiveRecordExpressions $exp      The expression will be stored.
988
   * @param string                  $operator The operator to concat this Expressions into WHERE statment.
989
   */
990 1
  protected function _addExpression($exp, $operator)
991
  {
992 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...
993 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...
994 1
    } else {
995 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...
996
    }
997 1
  }
998
999
  /**
1000
   * helper function to add condition into WHERE.
1001
   *
1002
   * @param ActiveRecordExpressions $exp      The expression will be concat into WHERE or SET statment.
1003
   * @param string                  $operator the operator to concat this Expressions into WHERE or SET statment.
1004
   * @param string                  $name     The Expression will contact to.
1005
   */
1006 14
  protected function _addCondition($exp, $operator, $name = 'where')
1007
  {
1008 14
    if (!$this->$name) {
1009 14
      $this->$name = new ActiveRecordExpressions(array('operator' => strtoupper($name), 'target' => $exp));
1010 14
    } else {
1011 4
      $this->$name->target = new ActiveRecordExpressions(
1012
          array(
1013 4
              'source'   => $this->$name->target,
1014 4
              'operator' => $operator,
1015 4
              'target'   => $exp,
1016
          )
1017 4
      );
1018
    }
1019 14
  }
1020
1021
  /**
1022
   * @return array
1023
   */
1024 1
  public function getDirty()
1025
  {
1026 1
    return $this->dirty;
1027
  }
1028
1029
  /**
1030
   * @return bool
1031
   */
1032
  public static function isNewDataAreDirty()
1033
  {
1034
    return self::$new_data_are_dirty;
1035
  }
1036
1037
  /**
1038
   * @param bool $bool
1039
   */
1040 17
  public static function setNewDataAreDirty($bool)
1041
  {
1042 17
    self::$new_data_are_dirty = (bool)$bool;
1043 17
  }
1044
1045
  /**
1046
   * Magic function to SET values of the current object.
1047
   *
1048
   * @param mixed $var
1049
   * @param mixed $val
1050
   */
1051 23
  public function __set($var, $val)
1052
  {
1053
    if (
1054 23
        array_key_exists($var, $this->sqlExpressions)
1055
        ||
1056 23
        array_key_exists($var, $this->defaultSqlExpressions)
1057 23
    ) {
1058
1059 20
      $this->sqlExpressions[$var] = $val;
1060
1061 20
    } elseif (
1062 20
        array_key_exists($var, $this->relations)
1063 20
        &&
1064
        $val instanceof self
1065 20
    ) {
1066
1067 1
      $this->relations[$var] = $val;
1068
1069 1
    } else {
1070
1071 20
      $this->array[$var] = $val;
1072
1073 20
      if (self::$new_data_are_dirty === true) {
1074 7
        $this->dirty[$var] = $val;
1075 7
      }
1076
1077
    }
1078 23
  }
1079
1080
  /**
1081
   * Magic function to UNSET values of the current object.
1082
   *
1083
   * @param mixed $var
1084
   */
1085 1
  public function __unset($var)
1086
  {
1087 1
    if (array_key_exists($var, $this->sqlExpressions)) {
1088
      unset($this->sqlExpressions[$var]);
1089
    }
1090
1091 1
    if (isset($this->array[$var])) {
1092 1
      unset($this->array[$var]);
1093 1
    }
1094
1095 1
    if (isset($this->dirty[$var])) {
1096 1
      unset($this->dirty[$var]);
1097 1
    }
1098 1
  }
1099
1100
  /**
1101
   * Helper function for "GROUP BY".
1102
   *
1103
   * @param array $args
1104
   * @param null  $dummy <p>only needed for API compatibility with Arrayy</p>
1105
   *
1106
   * @return $this
1107
   */
1108
  public function group($args, $dummy = null)
1109
  {
1110
    $this->__call('group', func_get_args());
1111
1112
    return $this;
1113
  }
1114
1115
  /**
1116
   * Helper function for "ORDER BY".
1117
   *
1118
   * @param $args ...
1119
   *
1120
   * @return $this
1121
   */
1122 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...
1123
  {
1124 2
    $this->__call('order', func_get_args());
1125
1126 2
    return $this;
1127
  }
1128
1129
  /**
1130
   * Magic function to GET the values of current object.
1131
   *
1132
   * @param $var
1133
   *
1134
   * @return mixed
1135
   */
1136 23
  public function &__get($var)
1137
  {
1138 23
    if (array_key_exists($var, $this->sqlExpressions)) {
1139 20
      return $this->sqlExpressions[$var];
1140
    }
1141
1142 23
    if (array_key_exists($var, $this->relations)) {
1143 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 1143 which is incompatible with the return type of the parent method Arrayy\Arrayy::__get of type object|integer|double|string|null|boolean.
Loading history...
1144
    }
1145
1146 23
    if (isset($this->dirty[$var])) {
1147 4
      return $this->dirty[$var];
1148
    }
1149
1150 23
    return parent::__get($var);
1151
  }
1152
}
1153