Completed
Push — master ( d589d2...129dc0 )
by Lars
01:42
created

ActiveRecord::update()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4.0105

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 21
cts 23
cp 0.913
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 5
nop 0
crap 4.0105
1
<?php
2
3
namespace voku\db;
4
5
use Arrayy\Arrayy;
6
use Symfony\Component\PropertyAccess\PropertyAccess;
7
use voku\db\exceptions\ActiveRecordException;
8
use voku\db\exceptions\FetchingException;
9
10
/**
11
 * A simple implement of active record via Arrayy.
12
 *
13
 * @method $this select(string $dbProperty)
14
 * @method $this eq(string $dbProperty, string | null $value = null)
15
 * @method $this from(string $table)
16
 * @method $this where(string $where)
17
 * @method $this having(string $having)
18
 * @method $this limit(int $start, int | null $end = null)
19
 *
20
 * @method $this equal(string $dbProperty, string $value)
21
 * @method $this notEqual(string $dbProperty, string $value)
22
 * @method $this ne(string $dbProperty, string $value)
23
 * @method $this greaterThan(string $dbProperty, int $value)
24
 * @method $this gt(string $dbProperty, int $value)
25
 * @method $this lessThan(string $dbProperty, int $value)
26
 * @method $this lt(string $dbProperty, int $value)
27
 * @method $this greaterThanOrEqual(string $dbProperty, int $value)
28
 * @method $this ge(string $dbProperty, int $value)
29
 * @method $this gte(string $dbProperty, int $value)
30
 * @method $this lessThanOrEqual(string $dbProperty, int $value)
31
 * @method $this le(string $dbProperty, int $value)
32
 * @method $this lte(string $dbProperty, int $value)
33
 * @method $this between(string $dbProperty, array $value)
34
 * @method $this like(string $dbProperty, string $value)
35
 * @method $this in(string $dbProperty, array $value)
36
 * @method $this notIn(string $dbProperty, array $value)
37
 * @method $this isnull(string $dbProperty)
38
 * @method $this isNotNull(string $dbProperty)
39
 * @method $this notNull(string $dbProperty)
40
 */
41
abstract class ActiveRecord extends Arrayy
42
{
43
  /**
44
   * @var DB static property to connect database.
45
   */
46
  protected static $db;
47
48
  /**
49
   * @var array maping the function name and the operator, to build Expressions in WHERE condition.
50
   *
51
   * user can call it like this:
52
   * <pre>
53
   *   $user->isnotnull()->eq('id', 1);
54
   * </pre>
55
   *
56
   * will create Expressions can explain to SQL:
57
   * <pre>
58
   *   WHERE user.id IS NOT NULL AND user.id = :ph1
59
   * </pre>
60
   */
61
  protected static $operators = array(
62
      'equal'              => '=',
63
      'eq'                 => '=',
64
      'notequal'           => '<>',
65
      'ne'                 => '<>',
66
      'greaterthan'        => '>',
67
      'gt'                 => '>',
68
      'lessthan'           => '<',
69
      'lt'                 => '<',
70
      'greaterthanorequal' => '>=',
71
      'ge'                 => '>=',
72
      'gte'                => '>=',
73
      'lessthanorequal'    => '<=',
74
      'le'                 => '<=',
75
      'lte'                => '<=',
76
      'between'            => 'BETWEEN',
77
      'like'               => 'LIKE',
78
      'in'                 => 'IN',
79
      'notin'              => 'NOT IN',
80
      'isnull'             => 'IS NULL',
81
      'isnotnull'          => 'IS NOT NULL',
82
      'notnull'            => 'IS NOT NULL',
83
  );
84
85
  /**
86
   * @var array Part of SQL, maping the function name and the operator to build SQL Part.
87
   * <pre>call function like this:
88
   *      $user->order('id DESC', 'name ASC')->limit(2, 1);
89
   *  can explain to SQL:
90
   *      ORDER BY id DESC, name ASC LIMIT 2,1</pre>
91
   */
92
  protected $sqlParts = array(
93
      'select' => 'SELECT',
94
      'from'   => 'FROM',
95
      'set'    => 'SET',
96
      'where'  => 'WHERE',
97
      'group'  => 'GROUP BY',
98
      'having' => 'HAVING',
99
      'order'  => 'ORDER BY',
100
      'limit'  => 'LIMIT',
101
      'top'    => 'TOP',
102
  );
103
104
  /**
105
   * @var array Static property to stored the default Sql Expressions values.
106
   */
107
  protected $defaultSqlExpressions = array(
108
      'expressions' => array(),
109
      'wrap'        => false,
110
      'select'      => null,
111
      'insert'      => null,
112
      'update'      => null,
113
      'set'         => null,
114
      'delete'      => 'DELETE ',
115
      'join'        => null,
116
      'from'        => null,
117
      'values'      => null,
118
      'where'       => null,
119
      'having'      => null,
120
      'limit'       => null,
121
      'order'       => null,
122
      'group'       => null,
123
  );
124
125
  /**
126
   * @var array Stored the Expressions of the SQL.
127
   */
128
  protected $sqlExpressions = array();
129
130
  /**
131
   * @var string  The table name in database.
132
   */
133
  protected $table;
134
135
  /**
136
   * @var string  The primary key of this ActiveRecord, just suport single primary key.
137
   */
138
  protected $primaryKeyName = 'id';
139
140
  /**
141
   * @var array Stored the drity data of this object, when call "insert" or "update" function, will write this data
142
   *      into database.
143
   */
144
  protected $dirty = array();
145
146
  /**
147
   * @var bool
148
   */
149
  protected static $new_data_are_dirty = true;
150
151
  /**
152
   * @var array Stored the params will bind to SQL when call DB->query(),
153
   */
154
  protected $params = array();
155
156
  /**
157
   * @var ActiveRecordExpressions[] Stored the configure of the relation, or target of the relation.
158
   */
159
  protected $relations = array();
160
161
  /**
162
   * @var int The count of bind params, using this count and const "PREFIX" (:ph) to generate place holder in SQL.
163
   */
164
  private static $count = 0;
165
166
  const BELONGS_TO = 'belongs_to';
167
  const HAS_MANY   = 'has_many';
168
  const HAS_ONE    = 'has_one';
169
170
  const PREFIX = ':active_record';
171
172
  /**
173
   * @return array
174
   */
175
  public function getParams()
176
  {
177
    return $this->params;
178
  }
179
180
  /**
181 4
   * @return string
182
   */
183 4
  public function getPrimaryKeyName()
184 4
  {
185 4
    return $this->primaryKeyName;
186
  }
187
188
  /**
189
   * @return mixed|null
190
   */
191
  public function getPrimaryKey()
192
  {
193
    $id = $this->{$this->primaryKeyName};
194
    if ($id) {
195
      return $id;
196
    }
197
198
    return null;
199
  }
200
201
  /**
202
   * @param mixed $primaryKey
203
   * @param bool  $dirty
204 12
   *
205
   * @return $this
206 12
   */
207 12
  public function setPrimaryKey($primaryKey, $dirty = true)
208
  {
209 12
    if ($dirty === true) {
210
      $this->dirty[$this->primaryKeyName] = $primaryKey;
211
    } else {
212
      $this->array[$this->primaryKeyName] = $primaryKey;
213
    }
214
215
    return $this;
216
  }
217 4
218
  /**
219 4
   * @return string
220
   */
221 4
  public function getTable()
222
  {
223
    return $this->table;
224
  }
225
226
  /**
227
   * Function to reset the $params and $sqlExpressions.
228
   *
229
   * @return $this
230
   */
231
  public function reset()
232
  {
233
    $this->params = array();
234
    $this->sqlExpressions = array();
235
236
    return $this;
237
  }
238
239
  /**
240
   * Reset the dirty data.
241
   *
242 7
   * @return $this
243
   */
244 7
  public function resetDirty()
245 2
  {
246 2
    $this->array = array();
247
248 7
    return $this;
249 7
  }
250
251 7
  /**
252 7
   * set the DB connection.
253 7
   *
254 7
   * @param DB $db
255 7
   */
256 7
  public static function setDb($db)
257 7
  {
258 7
    self::$db = $db;
259
  }
260 7
261 7
  /**
262 7
   * Function to find one record and assign in to current object.
263
   *
264 7
   * @param mixed $id <p>
265
   *                  If call this function using this param, we will find the record by using this id.
266
   *                  If not set, just find the first record in database.
267
   *                  </p>
268
   *
269
   * @return false|$this <p>
270
   *                     If we could find the record, assign in to current object and return it,
271
   *                     otherwise return "false".
272 3
   *                     </p>
273
   */
274 3 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...
275 3
  {
276
    if ($id) {
277 3
      $this->reset()->eq($this->primaryKeyName, $id);
278 3
    }
279 3
280 3
    return self::query(
281 3
        $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...
282 3
            array(
283 3
                'select',
284 3
                'from',
285
                'join',
286 3
                'where',
287 3
                'group',
288 3
                'having',
289 3
                'order',
290
                'limit',
291
            )
292
        ),
293
        $this->params,
294
        $this->reset(),
295
        true
296
    );
297 1
  }
298
299 1
  /**
300 1
   * @param string $query
301
   *
302 1
   * @return $this[]
303 1
   */
304 1
  public function fetchManyByQuery($query)
305
  {
306 1
    $list = $this->fetchByQuery($query);
307 1
308 1
    if (!$list || empty($list)) {
309
      return array();
310
    }
311
312
    return $list;
313
  }
314
315
  /**
316
   * @param string $query
317
   *
318
   * @return $this|null
319
   */
320
  public function fetchOneByQuery($query)
321
  {
322
    $list = $this->fetchByQuery($query);
323
324
    if (!$list || empty($list)) {
325
      return null;
326
    }
327
328
    if (is_array($list) && count($list) > 0) {
329
      $this->array = $list[0]->getArray();
330
    } else {
331
      $this->array = $list->getArray();
332
    }
333
334
    return $this;
335
  }
336
337
  /**
338
   * @param mixed $id
339 2
   *
340
   * @return $this
341 2
   *
342
   * @throws FetchingException <p>Will be thrown, if we can not find the id.</p>
343
   */
344
  public function fetchById($id)
345 2
  {
346 2
    $obj = $this->fetchByIdIfExists($id);
347 2
    if ($obj === null) {
348
      throw new FetchingException("No row with primary key '$id' in table '$this->table'.");
349 2
    }
350 2
351
    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...
352 2
  }
353 2
354 2
  /**
355
   * @param mixed $id
356 2
   *
357 2
   * @return $this|null
358 2
   */
359 2
  public function fetchByIdIfExists($id)
360 2
  {
361 2
    $list = $this->fetch($id);
362
363 2
    if (!$list || $list->isEmpty()) {
364
      return null;
365
    }
366
367
    return $list;
368
  }
369
370
  /**
371
   * @param array $ids
372
   *
373
   * @return $this[]
374
   */
375
  public function fetchByIds($ids)
376
  {
377 2
    if (empty($ids)) {
378
      return array();
379 2
    }
380
381
    $list = $this->fetchAll($ids);
382
    if (is_array($list) && count($list) > 0) {
383 2
      return $list;
384
    }
385
386
    return array();
387 2
  }
388 2
389
  /**
390 2
   * @param string $query
391 2
   *
392
   * @return $this[]|$this
393 2
   */
394 2
  public function fetchByQuery($query)
395
  {
396 2
    $list = self::query(
397 2
        $query,
398
        $this->params,
399 2
        $this->reset()
400
    );
401 2
402 2
    if (is_array($list)) {
403 2
      if (count($list) === 0) {
404
        return array();
405 2
      }
406 2
407
      return $list;
408 2
    }
409
410
    $this->array = $list->getArray();
411
412
    return $this;
413
  }
414
415
  /**
416
   * @param array $ids
417
   *
418
   * @return $this[]
419
   */
420
  public function fetchByIdsPrimaryKeyAsArrayIndex($ids)
421
  {
422
    $result = $this->fetchAll($ids);
423
424
    $resultNew = array();
425
    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...
426
      $resultNew[$item->getPrimaryKey()] = $item;
427
    }
428 13
429
    return $resultNew;
430 13
  }
431 1
432 1
  /**
433
   * Function to find all records in database.
434 13
   *
435
   * @param array|null $ids <p>
436
   *                        If call this function using this param, we will find the record by using this id's.
437
   *                        If not set, just find all records in database.
438
   *                        </p>
439
   *
440
   * @return $this[]
441
   */
442 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...
443
  {
444
    if ($ids) {
445
      $this->reset()->in($this->primaryKeyName, $ids);
446
    }
447
448
    return self::query(
449
        $this->_buildSql(
450
            array(
451
                'select',
452
                'from',
453
                'join',
454
                'where',
455
                'group',
456
                'having',
457 8
                'order',
458
                'limit',
459 8
            )
460
        ),
461 8
        $this->params,
462
        $this->reset()
463
    );
464
  }
465 8
466 8
  /**
467 8
   * Function to delete current record in database.
468 8
   *
469
   * @return bool
470
   */
471
  public function delete()
472 8
  {
473
    return self::execute(
474 8
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
475 7
            array(
476 7
                'delete',
477 3
                'from',
478
                'where',
479
            )
480 8
        ),
481
        $this->params
482 8
    );
483
  }
484
485
  /**
486
   * @param string $primaryKeyName
487
   *
488
   * @return $this
489
   */
490
  public function setPrimaryKeyName($primaryKeyName)
491
  {
492
    $this->primaryKeyName = $primaryKeyName;
493
494
    return $this;
495 3
  }
496
497 3
  /**
498
   * @param string $table
499
   */
500 3
  public function setTable($table)
501
  {
502 2
    $this->table = $table;
503 2
  }
504 2
505 2
  /**
506 3
   * Function to build update SQL, and update current record in database, just write the dirty data into database.
507 3
   *
508
   * @return bool|int <p>
509
   *                  If update was successful, it will return the affected rows as int,
510
   *                  otherwise it will return false or true (if there are no dirty data).
511 2
   *                  </p>
512
   */
513 2
  public function update()
514 2
  {
515 1
    if (count($this->dirty) == 0) {
516 1
      return true;
517 1
    }
518 1
519
    foreach ($this->dirty as $field => $value) {
520 2
      $this->addCondition($field, '=', $value, ',', 'set');
521
    }
522 2
523 2
    $result = self::execute(
524 2
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
525 2
            array(
526
                'update',
527 1
                'set',
528
                'where',
529 1
            )
530
        ),
531
        $this->params
532
    );
533 1
    if ($result) {
534 2
      $this->resetDirty();
535 2
      $this->reset();
536 2
537 2
      return $result;
538
    }
539 2
540 2
    return false;
541 1
  }
542 1
543 1
  /**
544 1
   * Function to build insert SQL, and insert current record into database.
545
   *
546 2
   * @return bool|int <p>
547 2
   *                  If insert was successful, it will return the new id,
548 2
   *                  otherwise it will return false or true (if there are no dirty data).
549 2
   *                  </p>
550 2
   */
551
  public function insert()
552 2
  {
553
    if (!self::$db instanceof DB) {
554 2
      self::$db = DB::getInstance();
555 1
    }
556 1
557
    if (count($this->dirty) === 0) {
558 2
      return true;
559
    }
560
561
    $value = $this->_filterParam($this->dirty);
562 2
    $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...
563
        array(
564
            'operator' => 'INSERT INTO ' . $this->table,
565
            'target'   => new ActiveRecordExpressionsWrap(array('target' => array_keys($this->dirty))),
566
        )
567
    );
568
    $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...
569
        array(
570
            'operator' => 'VALUES',
571
            'target'   => new ActiveRecordExpressionsWrap(array('target' => $value)),
572 12
        )
573
    );
574
575
    $result = self::execute($this->_buildSql(array('insert', 'values')), $this->params);
576 12
    if ($result) {
577 8
      $this->{$this->primaryKeyName} = $result;
578 12
579
      $this->resetDirty();
580 7
      $this->reset();
581
582 7
      return $result;
583
    }
584
585 12
    return false;
586
  }
587 12
588 12
  /**
589 10
   * Helper function to copy an existing active record (and insert it into the database).
590 12
   *
591
   * @param bool $insert
592 10
   *
593
   * @return $this
594 12
   */
595
  public function copy($insert = true) {
596 1
    $new = clone $this;
597
598 1
    if ($insert) {
599
      $new->setPrimaryKey(null);
600 12
      $id = $new->insert();
601
      $new->setPrimaryKey($id);
602
    }
603 12
604
    return $new;
605
  }
606
607
  /**
608
   * Helper function to exec sql.
609
   *
610
   * @param string $sql   The SQL need to be execute.
611
   * @param array  $param The param will be bind to the sql statement.
612 12
   *
613
   * @return bool|int|Result              <p>
614 12
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
615
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
616
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
617
   *                                      "true" by e.g. "DROP"-queries<br />
618
   *                                      "false" on error
619 12
   *                                      </p>
620
   */
621
  public static function execute($sql, array $param = array())
622
  {
623
    if (!self::$db instanceof DB) {
624
      self::$db = DB::getInstance();
625
    }
626
627
    return self::$db->query($sql, $param);
628
  }
629
630
  /**
631
   * Helper function to query one record by sql and params.
632
   *
633 9
   * @param string            $sql    <p>
634
   *                                  The SQL query to find the record.
635 9
   *                                  </p>
636
   * @param array             $param  <p>
637
   *                                  The param will be bind to the $sql query.
638
   *                                  </p>
639 9
   * @param ActiveRecord|null $obj    <p>
640
   *                                  The object, if find record in database, we will assign the attributes into
641 9
   *                                  this object.
642
   *                                  </p>
643 7
   * @param bool              $single <p>
644 7
   *                                  If set to true, we will find record and fetch in current object, otherwise
645 7
   *                                  will find all records.
646 7
   *                                  </p>
647 7
   *
648 7
   * @return bool|$this|array
649
   */
650 9
  public static function query($sql, array $param = array(), ActiveRecord $obj = null, $single = false)
651
  {
652 7
    $result = self::execute($sql, $param);
653
654 7
    if (!$result) {
655 7
      return false;
656
    }
657 7
658
    $useObject = is_object($obj);
659 7
    if ($useObject === true) {
660
      $called_class = $obj;
661
    } else {
662
      $called_class = get_called_class();
663
    }
664
665
    self::setNewDataAreDirty(false);
666
667
    if ($single) {
668
      $return = $result->fetchObject($called_class, null, true);
0 ignored issues
show
Bug introduced by
It seems like $called_class defined by $obj on line 660 can also be of type null; however, voku\db\Result::fetchObject() does only seem to accept string|object, maybe add an additional type check?

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

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

    return array();
}

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

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

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

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

Loading history...
735 7
          $o->__set($backref, $this);
736 7
        }
737
      }
738 7
739 7
    } elseif (
740 7
        (!$relation instanceof self)
741 7
        &&
742 1
        self::BELONGS_TO == $relation[0]
743 1
    ) {
744 1
745 7
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
746
747 7
      if ($backref) {
748 7
        $this->relations[$name] && $backref && $obj->__set($backref, $this);
749 7
      }
750 7
751 7
    } else {
752 1
      throw new ActiveRecordException("Relation $name not found.");
753
    }
754 7
755 7
    return $this->relations[$name];
756
  }
757
758
  /**
759
   * Helper function to build SQL with sql parts.
760
   *
761
   * @param string       $n The SQL part will be build.
762
   * @param int          $i The index of $n in $sql array.
763
   * @param ActiveRecord $o The reference to $this
764
   */
765
  private function _buildSqlCallback(&$n, $i, $o)
766
  {
767 1
    if (
768
        'select' === $n
769 1
        &&
770
        null === $o->$n
771 1
    ) {
772 1
773 1
      $n = strtoupper($n) . ' ' . $o->table . '.*';
774 1
775 1
    } elseif (
776
        (
777 1
            'update' === $n
778
            ||
779 1
            'from' === $n
780
        )
781
        &&
782
        null === $o->$n
783
    ) {
784
785
      $n = strtoupper($n) . ' ' . $o->table;
786
787
    } elseif ('delete' === $n) {
788 1
789
      $n = strtoupper($n) . ' ';
790 1
791 1
    } else {
792 1
793 1
      $n = (null !== $o->$n) ? $o->$n . ' ' : '';
794
795 1
    }
796
  }
797
798
  /**
799
   * Helper function to build SQL with sql parts.
800
   *
801
   * @param array $sqls The SQL part will be build.
802
   *
803
   * @return string
804 7
   */
805
  protected function _buildSql($sqls = array())
806 7
  {
807 7
    array_walk($sqls, array($this, '_buildSqlCallback'), $this);
808 7
809 4
    // DEBUG
810
    //echo 'SQL: ', implode(' ', $sqls), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
811 4
812 4
    return implode(' ', $sqls);
813 4
  }
814
815 4
  /**
816
   * Magic function to make calls witch in function mapping stored in $operators and $sqlPart.
817 7
   * also can call function of DB object.
818
   *
819
   * @param string $name function name
820
   * @param array  $args The arguments of the function.
821
   *
822 1
   * @return $this|mixed Return the result of callback or the current object to make chain method calls.
823
   *
824 1
   * @throws ActiveRecordException
825
   */
826
  public function __call($name, $args)
827
  {
828
    if (!self::$db instanceof DB) {
829
      self::$db = DB::getInstance();
830
    }
831
832
    $nameTmp = strtolower($name);
833
834
    if (array_key_exists($nameTmp, self::$operators)) {
835
836
      $this->addCondition(
837
          $args[0],
838 8
          self::$operators[$nameTmp],
839
          isset($args[1]) ? $args[1] : null,
840 8
          (is_string(end($args)) && 'or' === strtolower(end($args))) ? 'OR' : 'AND'
841 8
      );
842
843
    } elseif (array_key_exists($nameTmp = str_replace('by', '', $nameTmp), $this->sqlParts)) {
844
845
      $this->$name = new ActiveRecordExpressions(
846
          array(
847
              'operator' => $this->sqlParts[$nameTmp],
848
              'target'   => implode(', ', $args),
849 12
          )
850
      );
851
852 12
    } elseif (is_callable($callback = array(self::$db, $name))) {
853
854 12
      return call_user_func_array($callback, $args);
855 12
856
    } else {
857 11
858
      throw new ActiveRecordException("Method $name not exist.");
859 11
860 12
    }
861 12
862
    return $this;
863 12
  }
864
865 1
  /**
866
   * Make wrap when build the SQL expressions of WHERE.
867 1
   *
868
   * @param string $op If give this param will build one WrapExpressions include the stored expressions add into WHERE.
869 12
   *                   otherwise wil stored the expressions into array.
870
   *
871 12
   * @return $this
872 5
   */
873 5
  public function wrap($op = null)
874
  {
875
    if (1 === func_num_args()) {
876 12
      $this->wrap = false;
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

Loading history...
878
        $this->_addCondition(
879
            new ActiveRecordExpressionsWrap(
880
                array(
881
                    'delimiter' => ' ',
882
                    'target'    => $this->expressions,
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
883 1
                )
884
            ), 'or' === strtolower($op) ? 'OR' : 'AND'
885 1
        );
886
      }
887
      $this->expressions = array();
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
890 1
    }
891 1
892
    return $this;
893 1
  }
894 1
895 1
  /**
896 1
   * Helper function to build place holder when make SQL expressions.
897
   *
898
   * @param mixed $value The value will bind to SQL, just store it in $this->params.
899
   *
900
   * @return mixed $value
901
   */
902
  protected function _filterParam($value)
903
  {
904
    if (is_array($value)) {
905
      foreach ($value as $key => $val) {
906
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
907
      }
908
    } elseif (is_string($value)) {
909
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
910
      $value = $ph;
911
    }
912
913
    return $value;
914
  }
915
916
  /**
917
   * Helper function to add condition into WHERE.
918
   * create the SQL Expressions.
919
   *
920 2
   * @param string $field The field name, the source of Expressions
921
   * @param string $operator
922 2
   * @param mixed  $value The target of the Expressions
923
   * @param string $op    The operator to concat this Expressions into WHERE or SET statement.
924 2
   * @param string $name  The Expression will contact to.
925
   */
926
  public function addCondition($field, $operator, $value, $op = 'AND', $name = 'where')
927
  {
928
    $value = $this->_filterParam($value);
929
    $exp = new ActiveRecordExpressions(
930
        array(
931
            'source'   => ('where' == $name ? $this->table . '.' : '') . $field,
932
            'operator' => $operator,
933
            'target'   => is_array($value)
934 12
                ? new ActiveRecordExpressionsWrap(
935
                    'between' === strtolower($operator)
936 12
                        ? array('target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ')
937 12
                        : array('target' => $value)
938
                ) : $value,
939
        )
940 12
    );
941 11
    if ($exp) {
942
      if (!$this->wrap) {
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
943
        $this->_addCondition($exp, $op, $name);
944 10
      } else {
945 3
        $this->_addExpression($exp, $op);
946
      }
947
    }
948 10
  }
949
950
  /**
951
   * helper function to add condition into JOIN.
952
   * create the SQL Expressions.
953
   *
954
   * @param string $table The join table name
955
   * @param string $on    The condition of ON
956
   * @param string $type  The join type, like "LEFT", "INNER", "OUTER"
957
   *
958
   * @return $this
959
   */
960
  public function join($table, $on, $type = 'LEFT')
961
  {
962
    $this->join = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property join does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
965
            'operator' => $type . ' JOIN',
966
            'target'   => new ActiveRecordExpressions(
967
                array('source' => $table, 'operator' => 'ON', 'target' => $on)
968
            ),
969
        )
970
    );
971
972
    return $this;
973
  }
974
975
  /**
976
   * helper function to make wrapper. Stored the expression in to array.
977
   *
978
   * @param ActiveRecordExpressions $exp      The expression will be stored.
979
   * @param string                  $operator The operator to concat this Expressions into WHERE statment.
980
   */
981
  protected function _addExpression($exp, $operator)
982
  {
983
    if (!is_array($this->expressions) || count($this->expressions) == 0) {
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
984
      $this->expressions = array($exp);
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
985
    } else {
986
      $this->expressions[] = new ActiveRecordExpressions(array('operator' => $operator, 'target' => $exp));
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
987
    }
988
  }
989
990
  /**
991
   * helper function to add condition into WHERE.
992
   *
993
   * @param ActiveRecordExpressions $exp      The expression will be concat into WHERE or SET statment.
994
   * @param string                  $operator the operator to concat this Expressions into WHERE or SET statment.
995
   * @param string                  $name     The Expression will contact to.
996
   */
997
  protected function _addCondition($exp, $operator, $name = 'where')
998
  {
999
    if (!$this->$name) {
1000
      $this->$name = new ActiveRecordExpressions(array('operator' => strtoupper($name), 'target' => $exp));
1001
    } else {
1002
      $this->$name->target = new ActiveRecordExpressions(
1003
          array(
1004
              'source'   => $this->$name->target,
1005
              'operator' => $operator,
1006
              'target'   => $exp,
1007
          )
1008
      );
1009
    }
1010
  }
1011
1012
  /**
1013
   * @return array
1014
   */
1015
  public function getDirty()
1016
  {
1017
    return $this->dirty;
1018
  }
1019
1020
  /**
1021
   * @return bool
1022
   */
1023
  public static function isNewDataAreDirty()
1024
  {
1025
    return self::$new_data_are_dirty;
1026
  }
1027
1028
  /**
1029
   * @param bool $bool
1030
   */
1031
  public static function setNewDataAreDirty($bool)
1032
  {
1033
    self::$new_data_are_dirty = (bool)$bool;
1034
  }
1035
1036
  /**
1037
   * Magic function to SET values of the current object.
1038
   *
1039
   * @param mixed $var
1040
   * @param mixed $val
1041
   */
1042
  public function __set($var, $val)
1043
  {
1044
    if (
1045
        array_key_exists($var, $this->sqlExpressions)
1046
        ||
1047
        array_key_exists($var, $this->defaultSqlExpressions)
1048
    ) {
1049
1050
      $this->sqlExpressions[$var] = $val;
1051
1052
    } elseif (
1053
        array_key_exists($var, $this->relations)
1054
        &&
1055
        $val instanceof self
1056
    ) {
1057
1058
      $this->relations[$var] = $val;
1059
1060
    } else {
1061
1062
      $this->array[$var] = $val;
1063
1064
      if (self::$new_data_are_dirty === true) {
1065
        $this->dirty[$var] = $val;
1066
      }
1067
1068
    }
1069
  }
1070
1071
  /**
1072
   * Magic function to UNSET values of the current object.
1073
   *
1074
   * @param mixed $var
1075
   */
1076
  public function __unset($var)
1077
  {
1078
    if (array_key_exists($var, $this->sqlExpressions)) {
1079
      unset($this->sqlExpressions[$var]);
1080
    }
1081
1082
    if (isset($this->array[$var])) {
1083
      unset($this->array[$var]);
1084
    }
1085
1086
    if (isset($this->dirty[$var])) {
1087
      unset($this->dirty[$var]);
1088
    }
1089
  }
1090
1091
  /**
1092
   * Helper function for "GROUP BY".
1093
   *
1094
   * @param array $args
1095
   * @param null  $dummy <p>only needed for API compatibility with Arrayy</p>
1096
   *
1097
   * @return $this
1098
   */
1099
  public function group($args, $dummy = null)
1100
  {
1101
    $this->__call('group', func_get_args());
1102
1103
    return $this;
1104
  }
1105
1106
  /**
1107
   * Helper function for "ORDER BY".
1108
   *
1109
   * @param $args ...
1110
   *
1111
   * @return $this
1112
   */
1113
  public function order($args)
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1114
  {
1115
    $this->__call('order', func_get_args());
1116
1117
    return $this;
1118
  }
1119
1120
  /**
1121
   * Magic function to GET the values of current object.
1122
   *
1123
   * @param $var
1124
   *
1125
   * @return mixed
1126
   */
1127
  public function &__get($var)
1128
  {
1129
    if (isset($this->dirty[$var])) {
1130
      return $this->dirty[$var];
1131
    }
1132
1133
    if (array_key_exists($var, $this->sqlExpressions)) {
1134
      return $this->sqlExpressions[$var];
1135
    }
1136
1137
    if (array_key_exists($var, $this->relations)) {
1138
      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 1138 which is incompatible with the return type of the parent method Arrayy\Arrayy::__get of type object|integer|double|string|null|boolean.
Loading history...
1139
    }
1140
1141
    return parent::__get($var);
1142
  }
1143
}
1144