Completed
Push — master ( a545c2...b61720 )
by Lars
01:52
created

ActiveRecord::getDirty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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