Completed
Push — master ( cd7afc...70524f )
by Lars
04:05
created

ActiveRecord::join()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 9
cts 9
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
nc 1
nop 3
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Arrayy\Arrayy;
8
use voku\db\exceptions\ActiveRecordException;
9
use voku\db\exceptions\FetchingException;
10
11
/**
12
 * A simple implement of active record via Arrayy.
13
 *
14
 * @method $this select(string $dbProperty)
15
 * @method $this eq(string $dbProperty, string | int | null $value = null)
16
 * @method $this from(string $table)
17
 * @method $this where(string $where)
18
 * @method $this having(string $having)
19
 * @method $this limit(int $start, int | null $end = null)
20
 *
21
 * @method $this equal(string $dbProperty, string $value)
22
 * @method $this notEqual(string $dbProperty, string $value)
23
 * @method $this ne(string $dbProperty, string $value)
24
 * @method $this greaterThan(string $dbProperty, int $value)
25
 * @method $this gt(string $dbProperty, int $value)
26
 * @method $this lessThan(string $dbProperty, int $value)
27
 * @method $this lt(string $dbProperty, int $value)
28
 * @method $this greaterThanOrEqual(string $dbProperty, int $value)
29
 * @method $this ge(string $dbProperty, int $value)
30
 * @method $this gte(string $dbProperty, int $value)
31
 * @method $this lessThanOrEqual(string $dbProperty, int $value)
32
 * @method $this le(string $dbProperty, int $value)
33
 * @method $this lte(string $dbProperty, int $value)
34
 * @method $this between(string $dbProperty, array $value)
35
 * @method $this like(string $dbProperty, string $value)
36
 * @method $this in(string $dbProperty, array $value)
37
 * @method $this notIn(string $dbProperty, array $value)
38
 * @method $this isnull(string $dbProperty)
39
 * @method $this isNotNull(string $dbProperty)
40
 * @method $this notNull(string $dbProperty)
41
 */
42
abstract class ActiveRecord extends Arrayy
43
{
44
  const BELONGS_TO = 'belongs_to';
45
  const HAS_MANY   = 'has_many';
46
  const HAS_ONE    = 'has_one';
47
48
  const PREFIX = ':active_record';
49
50
  /**
51
   * Check "@property" types from class-phpdoc.
52
   *
53
   * @var bool
54
   */
55
  protected $checkPropertyTypes = true;
56
57
  /**
58
   * Check properties mismatch in the constructor.
59
   *
60
   * @var bool
61
   */
62
  protected $checkPropertiesMismatchInConstructor = false;
63
64
65
  /**
66
   * @var array <p>Mapping the function name and the operator, to build Expressions in WHERE condition.</p>
67
   *
68
   * call the function like this:
69
   * <pre>
70
   *   $user->isNotNull()->eq('id', 1);
71
   * </pre>
72
   *
73
   * the result in SQL:
74
   * <pre>
75
   *   WHERE user.id IS NOT NULL AND user.id = :ph1
76
   * </pre>
77
   */
78
  protected static $operators = [
79
      'equal'              => '=',
80
      'eq'                 => '=',
81
      'notequal'           => '<>',
82
      'ne'                 => '<>',
83
      'greaterthan'        => '>',
84
      'gt'                 => '>',
85
      'lessthan'           => '<',
86
      'lt'                 => '<',
87
      'greaterthanorequal' => '>=',
88
      'ge'                 => '>=',
89
      'gte'                => '>=',
90
      'lessthanorequal'    => '<=',
91
      'le'                 => '<=',
92
      'lte'                => '<=',
93
      'between'            => 'BETWEEN',
94
      'like'               => 'LIKE',
95
      'in'                 => 'IN',
96
      'notin'              => 'NOT IN',
97
      'isnull'             => 'IS NULL',
98
      'isnotnull'          => 'IS NOT NULL',
99
      'notnull'            => 'IS NOT NULL',
100
  ];
101
102
  /**
103
   * @var int <p>The count of bind params, using this count and const "PREFIX" (:ph) to generate place holder in
104
   *      SQL.</p>
105
   */
106
  private static $count = 0;
107
108
  /**
109
   * @var DB
110
   */
111
  protected $db;
112
113
  /**
114
   * @var array <p>Part of the SQL, mapping the function name and the operator to build SQL Part.</p>
115
   *
116
   * <br />
117
   *
118
   * call the function like this:
119
   * <pre>
120
   *      $user->orderBy('id DESC', 'name ASC')->limit(2, 1);
121
   * </pre>
122
   *
123
   * the result in SQL:
124
   * <pre>
125
   *      ORDER BY id DESC, name ASC LIMIT 2,1
126
   * </pre>
127
   */
128
  protected $sqlParts = [
129
      'select' => 'SELECT',
130
      'from'   => 'FROM',
131
      'set'    => 'SET',
132
      'where'  => 'WHERE',
133
      'group'  => 'GROUP BY',
134
      'having' => 'HAVING',
135
      'order'  => 'ORDER BY',
136
      'limit'  => 'LIMIT',
137
      'top'    => 'TOP',
138
  ];
139
140
  /**
141
   * @var array <p>The default sql expressions values.</p>
142
   */
143
  protected $defaultSqlExpressions = [
144
      'expressions' => [],
145
      'wrap'        => false,
146
      'select'      => null,
147
      'insert'      => null,
148
      'update'      => null,
149
      'set'         => null,
150
      'delete'      => 'DELETE ',
151
      'join'        => null,
152
      'from'        => null,
153
      'values'      => null,
154
      'where'       => null,
155
      'having'      => null,
156
      'limit'       => null,
157
      'order'       => null,
158
      'orderBy'     => null,
159
      'group'       => null,
160
  ];
161
162
  /**
163
   * @var array <p>Stored the Expressions of the SQL.</p>
164
   */
165
  protected $sqlExpressions = [];
166
167
  /**
168
   * @var string <p>The table name in database.</p>
169
   */
170
  protected $table;
171
172
  /**
173
   * @var string  <p>The primary key of this ActiveRecord, just support single primary key.</p>
174
   */
175
  protected $primaryKeyName = 'id';
176
177
  /**
178
   * @var array <p>Stored the dirty data of this object, when call "insert" or "update" function, will write this data
179
   *      into database.</p>
180
   */
181
  protected $dirty = [];
182
183
  /**
184
   * @var bool
185
   */
186
  protected $new_data_are_dirty = true;
187
188
  /**
189
   * @var array <p>Stored the params will bind to SQL when call DB->query().</p>
190
   */
191
  protected $params = [];
192
193
  /**
194
   * @var ActiveRecordExpressions[] <p>Stored the configure of the relation, or target of the relation.</p>
195
   */
196
  protected $relations = [];
197
198
  /**
199
   * Magic function to make calls witch in function mapping stored in $operators and $sqlPart.
200
   * also can call function of DB object.
201
   *
202
   * @param string $name <p>The name of the function.</p>
203
   * @param array  $args <p>The arguments of the function.</p>
204
   *
205
   * @return $this|mixed <p>Return the result of callback or the current object to make chain method calls.</p>
206
   *
207
   * @throws ActiveRecordException
208
   */
209 16
  public function __call(string $name, array $args = [])
210
  {
211 16
    if (!$this->db instanceof DB) {
212 14
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
213
    }
214
215 16
    $nameTmp = \strtolower($name);
216
217 16
    if (\array_key_exists($nameTmp, self::$operators)) {
218
219 14
      $this->addCondition(
220 14
          $args[0],
221 14
          self::$operators[$nameTmp],
222 14
          $args[1] ?? null,
223 14
          (\is_string(\end($args)) && 'or' === \strtolower(\end($args))) ? 'OR' : 'AND'
224
      );
225
226 11
    } elseif (\array_key_exists($nameTmp = \str_replace('by', '', $nameTmp), $this->sqlParts)) {
227
228 11
      $this->{$name} = new ActiveRecordExpressions(
229
          [
230 11
              'operator' => $this->sqlParts[$nameTmp],
231 11
              'target'   => \implode(', ', $args),
232
          ]
233
      );
234
235
    } elseif (\is_callable($callback = [$this->db, $name])) {
236
237
      return \call_user_func_array($callback, $args);
238
239
    } else {
240
241
      throw new ActiveRecordException("Method $name not exist.");
242
243
    }
244
245 16
    return $this;
246
  }
247
248
  /**
249
   * Magic function to GET the values of current object.
250
   *
251
   * @param mixed $var
252
   *
253
   * @return mixed
254
   */
255 23
  public function &__get($var)
256
  {
257 23
    if (\array_key_exists($var, $this->sqlExpressions)) {
258 20
      return $this->sqlExpressions[$var];
259
    }
260
261 23
    if (\array_key_exists($var, $this->relations)) {
262 3
      return $this->getRelation($var);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getRelation($var); of type voku\db\ActiveRecordExpr...|voku\db\ActiveRecord[] adds the type voku\db\ActiveRecord[] to the return on line 262 which is incompatible with the return type of the parent method Arrayy\Arrayy::__get of type object|integer|double|string|null|boolean.
Loading history...
263
    }
264
265 23
    if (isset($this->dirty[$var])) {
266 10
      return $this->dirty[$var];
267
    }
268
269 23
    return parent::__get($var);
270
  }
271
272
  /**
273
   * Magic function to SET values of the current object.
274
   *
275
   * @param string $var
276
   * @param mixed  $val
277
   */
278 24
  public function __set($var, $val)
279
  {
280
    if (
281 24
        \array_key_exists($var, $this->sqlExpressions)
282
        ||
283 24
        \array_key_exists($var, $this->defaultSqlExpressions)
284
    ) {
285
286 20
      $this->sqlExpressions[$var] = $val;
287
288
    } elseif (
289 21
        \array_key_exists($var, $this->relations)
290
        &&
291 21
        $val instanceof self
292
    ) {
293
294 1
      $this->relations[$var] = $val;
295
296
    } else {
297
298 21
      $this->set($var, $val);
299
300 21
      if ($this->new_data_are_dirty === true) {
301 15
        $this->dirty[$var] = $val;
302
      }
303
304
    }
305 24
  }
306
307
  /**
308
   * Magic function to UNSET values of the current object.
309
   *
310
   * @param mixed $var
311
   */
312 1
  public function __unset($var)
313
  {
314 1
    if (\array_key_exists($var, $this->sqlExpressions)) {
315
      unset($this->sqlExpressions[$var]);
316
    }
317
318 1
    if (isset($this->array[$var])) {
319 1
      unset($this->array[$var]);
320
    }
321
322 1
    if (isset($this->dirty[$var])) {
323 1
      unset($this->dirty[$var]);
324
    }
325 1
  }
326
327
  /**
328
   * Get a value from an array (optional using dot-notation).
329
   *
330
   * @param string $key       <p>The key to look for.</p>
331
   * @param mixed  $fallback  <p>Value to fallback to.</p>
332
   * @param array  $array     <p>The array to get from, if it's set to "null" we use the current array from the
333
   *                         class.</p>
334
   *
335
   * @return mixed
336
   */
337 23
  public function get($key, $fallback = null, array $array = null)
338
  {
339 23
    return parent::get($key, $fallback, $array);
340
  }
341
342
  /**
343
   * helper function to add condition into WHERE.
344
   *
345
   * @param ActiveRecordExpressions $expression <p>The expression will be concat into WHERE or SET statement.</p>
346
   * @param string                  $operator   <p>The operator to concat this Expressions into WHERE or SET
347
   *                                            statement.</p>
348
   * @param string                  $name       <p>The Expression will contact to.</p>
349
   */
350 14
  protected function _addCondition(ActiveRecordExpressions $expression, string $operator, string $name = 'where')
351
  {
352 14
    if (!$this->{$name}) {
353
354 14
      $this->{$name} = new ActiveRecordExpressions(
355
          [
356 14
              'operator' => \strtoupper($name),
357 14
              'target'   => $expression,
358
          ]
359
      );
360
361
    } else {
362
363 4
      $this->{$name}->target = new ActiveRecordExpressions(
364
          [
365 4
              'source'   => $this->{$name}->target,
366 4
              'operator' => $operator,
367 4
              'target'   => $expression,
368
          ]
369
      );
370
371
    }
372 14
  }
373
374
  /**
375
   * helper function to make wrapper. Stored the expression in to array.
376
   *
377
   * @param ActiveRecordExpressions $exp      <p>The expression will be stored.</p>
378
   * @param string                  $operator <p>The operator to concat this Expressions into WHERE statement.</p>
379
   */
380 1
  protected function _addExpression(ActiveRecordExpressions $exp, string $operator)
381
  {
382
    if (
383 1
        !\is_array($this->expressions)
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
384
        ||
385 1
        \count($this->expressions) === 0
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
386
    ) {
387 1
      $this->expressions = [$exp];
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
388
    } else {
389 1
      $this->expressions[] = new ActiveRecordExpressions(
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
390
          [
391 1
              'operator' => $operator,
392 1
              'target'   => $exp,
393
          ]
394
      );
395
    }
396 1
  }
397
398
  /**
399
   * Helper function to build SQL with sql parts.
400
   *
401
   * @param string[] $sql_array <p>The SQL part will be build.</p>
402
   *
403
   * @return string
404
   */
405 21
  protected function _buildSql(array $sql_array = []): string
406
  {
407 21
    \array_walk($sql_array, [$this, '_buildSqlCallback'], $this);
408
409
    // DEBUG
410
    //echo 'SQL: ', implode(' ', $sql_array), "\n", 'PARAMS: ', implode(', ', $this->params), "\n";
411
412 21
    return \implode(' ', $sql_array);
413
  }
414
415
  /**
416
   * Helper function to build SQL with sql parts.
417
   *
418
   * @param string $sql_string_part <p>The SQL part will be build.</p>
419
   * @param int    $index           <p>The index of $n in $sql array.</p>
420
   * @param self   $active_record   <p>The reference to $this.</p>
421
   */
422 21
  private function _buildSqlCallback(string &$sql_string_part, int $index, self $active_record)
423
  {
424
    if (
425 21
        'select' === $sql_string_part
426
        &&
427 21
        null === $active_record->{$sql_string_part}
428
    ) {
429
430 14
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table . '.*';
431
432
    } elseif (
433
        (
434 21
            'update' === $sql_string_part
435
            ||
436 21
            'from' === $sql_string_part
437
        )
438
        &&
439 21
        null === $active_record->{$sql_string_part}
440
    ) {
441
442 17
      $sql_string_part = \strtoupper($sql_string_part) . ' ' . $active_record->table;
443
444 21
    } elseif ('delete' === $sql_string_part) {
445
446 1
      $sql_string_part = \strtoupper($sql_string_part) . ' ';
447
448
    } else {
449
450 21
      $sql_string_part = (null !== $active_record->{$sql_string_part}) ? $active_record->{$sql_string_part} . ' ' : '';
451
452
    }
453 21
  }
454
455
  /**
456
   * Helper function to build place holder when make SQL expressions.
457
   *
458
   * @param mixed $value <p>The value will be bind to SQL, just store it in $this->params.</p>
459
   *
460
   * @return mixed $value
461
   */
462 18
  protected function _filterParam($value)
463
  {
464 18
    if (\is_array($value)) {
465 8
      foreach ($value as $key => $val) {
466 8
        $this->params[$value[$key] = self::PREFIX . ++self::$count] = $val;
467
      }
468 11
    } elseif (\is_string($value)) {
469 3
      $this->params[$ph = self::PREFIX . ++self::$count] = $value;
470 3
      $value = $ph;
471
    }
472
473 18
    return $value;
474
  }
475
476
  /**
477
   * Helper function to add condition into WHERE.
478
   *
479
   * @param string $field           <p>The field name, the source of Expressions</p>
480
   * @param string $operator        <p>The operator for this condition.</p>
481
   * @param mixed  $value           <p>The target of the Expressions.</p>
482
   * @param string $operator_concat <p>The operator to concat this Expressions into WHERE or SET statement.</p>
483
   * @param string $name            <p>The Expression will contact to.</p>
484
   */
485 14
  public function addCondition(string $field, string $operator, $value, string $operator_concat = 'AND', string $name = 'where')
486
  {
487 14
    $value = $this->_filterParam($value);
488 14
    $expression = new ActiveRecordExpressions(
489
        [
490 14
            'source'   => ('where' === strtolower($name) ? $this->table . '.' : '') . $field,
491 14
            'operator' => $operator,
492 14
            'target'   => \is_array($value)
493 4
                ? new ActiveRecordExpressionsWrap(
494 4
                    'between' === \strtolower($operator)
495 1
                        ? ['target' => $value, 'start' => ' ', 'end' => ' ', 'delimiter' => ' AND ']
496 4
                        : ['target' => $value]
497 14
                ) : $value,
498
        ]
499
    );
500
501 14
    if ($expression) {
502 14
      if (!$this->wrap) {
0 ignored issues
show
Documentation introduced by
The property wrap does not exist on object<voku\db\ActiveRecord>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
503 14
        $this->_addCondition($expression, $operator_concat, $name);
504
      } else {
505 1
        $this->_addExpression($expression, $operator_concat);
506
      }
507
    }
508 14
  }
509
510
  /**
511
   * Helper function to copy an existing active record (and insert it into the database).
512
   *
513
   * @param bool $insert
514
   *
515
   * @return $this
516
   */
517 1
  public function copy(bool $insert = true): self
518
  {
519 1
    $new = clone $this;
520
521 1
    if ($insert) {
522 1
      $new->setPrimaryKey(null);
523 1
      $id = $new->insert();
524 1
      $new->setPrimaryKey($id);
525
    }
526
527 1
    return $new;
528
  }
529
530
  /**
531
   * Function to delete current record in database.
532
   *
533
   * @return bool
534
   */
535 1
  public function delete(): bool
536
  {
537 1
    $return = $this->execute(
538 1
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
539
            [
540 1
                'delete',
541
                'from',
542
                'where',
543
            ]
544
        ),
545 1
        $this->params
546
    );
547
548 1
    return $return !== false;
549
  }
550
551
  /**
552
   * Helper function to exec sql.
553
   *
554
   * @param string $sql   <p>The SQL need to be execute.</p>
555
   * @param array  $param <p>The param will be bind to the sql statement.</p>
556
   *
557
   * @return bool|int|Result              <p>
558
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
559
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
560
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
561
   *                                      "true" by e.g. "DROP"-queries<br />
562
   *                                      "false" on error
563
   *                                      </p>
564
   */
565 23
  public function execute(string $sql, array $param = [])
566
  {
567 23
    if (!$this->db instanceof DB) {
568 3
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
569
    }
570
571 23
    return $this->db->query($sql, $param);
572
  }
573
574
  /**
575
   * Function to find one record and assign in to current object.
576
   *
577
   * @param mixed $id <p>
578
   *                  If call this function using this param, we will find the record by using this id.
579
   *                  If not set, just find the first record in database.
580
   *                  </p>
581
   *
582
   * @return false|$this <p>
583
   *                     If we could find the record, assign in to current object and return it,
584
   *                     otherwise return "false".
585
   *                     </p>
586
   */
587 11
  public function fetch($id = null)
588
  {
589 11
    if ($id) {
590 6
      $this->reset()->eq($this->primaryKeyName, $id);
591
    }
592
593 11
    $sqlQuery = $this->limit(1)->_buildSql(
0 ignored issues
show
Bug introduced by
The call to limit() misses a required argument $|.

This check looks for function calls that miss required arguments.

Loading history...
594
        [
595 11
            'select',
596
            'from',
597
            'join',
598
            'where',
599
            'group',
600
            'having',
601
            'order',
602
            'limit',
603
        ]
604
    );
605
606 11
    $return = $this->query(
607 11
        $sqlQuery,
608 11
        $this->params,
609 11
        $this->reset(),
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
610 11
        true
611
    );
612
613 11
    return $return;
614
  }
615
616
  /**
617
   * Function to find all records in database.
618
   *
619
   * @param array|null $ids <p>
620
   *                        If call this function using this param, we will find the record by using this id's.
621
   *                        If not set, just find all records in database.
622
   *                        </p>
623
   *
624
   * @return $this[]
625
   */
626 6
  public function fetchAll(array $ids = null): array
627
  {
628 6
    if ($ids) {
629 3
      $this->reset()->in($this->primaryKeyName, $ids);
630
    }
631
632 6
    return $this->query(
633 6
        $this->_buildSql(
634
            [
635 6
                'select',
636
                'from',
637
                'join',
638
                'where',
639
                'groupBy',
640
                'having',
641
                'orderBy',
642
                'limit',
643
            ]
644
        ),
645 6
        $this->params,
646 6
        $this->reset()
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
647
    );
648
  }
649
650
  /**
651
   * @param mixed $id
652
   *
653
   * @return $this
654
   *
655
   * @throws FetchingException <p>Will be thrown, if we can not find the id.</p>
656
   */
657 2
  public function fetchById($id): self
658
  {
659 2
    $obj = $this->fetchByIdIfExists($id);
660 2
    if ($obj === null) {
661 1
      throw new FetchingException("No row with primary key '$id' in table '$this->table'.");
662
    }
663
664 1
    return $obj;
665
  }
666
667
  /**
668
   * @param mixed $id
669
   *
670
   * @return $this|null
671
   */
672 4
  public function fetchByIdIfExists($id)
673
  {
674 4
    $list = $this->fetch($id);
675
676 4
    if (!$list) {
677 2
      return null;
678
    }
679
680 2
    return $list;
681
  }
682
683
  /**
684
   * @param array $ids
685
   *
686
   * @return $this[]
687
   */
688 2
  public function fetchByIds(array $ids): array
689
  {
690 2
    if (empty($ids)) {
691
      return [];
692
    }
693
694 2
    $list = $this->fetchAll($ids);
695 2
    if (\is_array($list) && \count($list) > 0) {
696 1
      return $list;
697
    }
698
699 1
    return [];
700
  }
701
702
  /**
703
   * @param array $ids
704
   *
705
   * @return $this[]
706
   */
707 1
  public function fetchByIdsPrimaryKeyAsArrayIndex(array $ids): array
708
  {
709 1
    $result = $this->fetchAll($ids);
710
711 1
    $resultNew = [];
712 1
    foreach ($result as $item) {
713 1
      $resultNew[$item->getPrimaryKey()] = $item;
714
    }
715
716 1
    return $resultNew;
717
  }
718
719
  /**
720
   * @param string $query
721
   *
722
   * @return $this[]|$this
723
   */
724 2
  public function fetchByQuery(string $query)
725
  {
726 2
    $list = $this->query(
727 2
        $query,
728 2
        $this->params,
729 2
        $this->reset()
0 ignored issues
show
Documentation introduced by
$this->reset() is of type this<voku\db\ActiveRecord>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
730
    );
731
732 2
    if (\is_array($list)) {
733 2
      if (\count($list) === 0) {
734
        return [];
735
      }
736
737 2
      return $list;
738
    }
739
740
    $this->array = $list->getArray();
741
742
    return $this;
743
  }
744
745
  /**
746
   * @return $this
747
   */
748 2
  public static function fetchEmpty(): self
749
  {
750 2
    $class = static::class;
751
752 2
    return new $class;
753
  }
754
755
  /**
756
   * @param string $query
757
   *
758
   * @return $this[]
759
   */
760 1
  public function fetchManyByQuery(string $query): array
761
  {
762 1
    $list = $this->fetchByQuery($query);
763
764 1
    if (!$list || empty($list)) {
765
      return [];
766
    }
767
768 1
    return $list;
769
  }
770
771
  /**
772
   * @param string $query
773
   *
774
   * @return $this|null
775
   */
776 1
  public function fetchOneByQuery(string $query)
777
  {
778 1
    $list = $this->fetchByQuery($query);
779
780 1
    if (!$list || empty($list)) {
781
      return null;
782
    }
783
784 1
    if (\is_array($list) && \count($list) > 0) {
785 1
      $this->array = $list[0]->getArray();
786
    } else {
787
      $this->array = $list->getArray();
788
    }
789
790 1
    return $this;
791
  }
792
793
  /**
794
   * @return array
795
   */
796 1
  public function getDirty(): array
797
  {
798 1
    return $this->dirty;
799
  }
800
801
  /**
802
   * @return array
803
   */
804
  public function getParams(): array
805
  {
806
    return $this->params;
807
  }
808
809
  /**
810
   * @return mixed|null
811
   */
812 13
  public function getPrimaryKey()
813
  {
814 13
    $id = $this->{$this->primaryKeyName};
815 13
    if ($id) {
816 12
      return $id;
817
    }
818
819 1
    return null;
820
  }
821
822
  /**
823
   * @return string
824
   */
825
  public function getPrimaryKeyName(): string
826
  {
827
    return $this->primaryKeyName;
828
  }
829
830
  /**
831
   * Helper function to get relation of this object.
832
   * There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
833
   *
834
   * @param string $name <p>The name of the relation (the array key from the definition).</p>
835
   *
836
   * @return mixed
837
   *
838
   * @throws ActiveRecordException <p>If the relation can't be found .</p>
839
   */
840 3
  protected function &getRelation(string $name)
841
  {
842 3
    $relation = $this->relations[$name];
843
    if (
844 3
        $relation instanceof self
845
        ||
846
        (
847 2
            \is_array($relation)
848
            &&
849 3
            $relation[0] instanceof self
850
        )
851
    ) {
852 3
      return $relation;
853
    }
854
855
    /* @var $obj ActiveRecord */
856 2
    $obj = new $relation[1];
857
858 2
    $this->relations[$name] = $obj;
859 2
    if (isset($relation[3]) && \is_array($relation[3])) {
860 1
      foreach ((array)$relation[3] as $func => $args) {
861 1
        \call_user_func_array([$obj, $func], (array)$args);
862
      }
863
    }
864
865 2
    $backref = $relation[4] ?? '';
866 2
    $relationInstanceOfSelf = ($relation instanceof self);
867
    if (
868 2
        $relationInstanceOfSelf === false
869
        &&
870 2
        self::HAS_ONE == $relation[0]
871
    ) {
872
873 1
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetch();
874
875 1
      if ($backref) {
876 1
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
877
      }
878
879
    } elseif (
880 2
        \is_array($relation)
881
        &&
882 2
        self::HAS_MANY == $relation[0]
883
    ) {
884
885 2
      $this->relations[$name] = $obj->eq((string)$relation[2], $this->{$this->primaryKeyName})->fetchAll();
886 2
      if ($backref) {
887 1
        foreach ($this->relations[$name] as $o) {
888 2
          $o->{$backref} = $this;
889
        }
890
      }
891
892
    } elseif (
893 2
        $relationInstanceOfSelf === false
894
        &&
895 2
        self::BELONGS_TO == $relation[0]
896
    ) {
897
898 2
      $this->relations[$name] = $obj->eq($obj->primaryKeyName, $this->{$relation[2]})->fetch();
899
900 2
      if ($backref) {
901 2
        $this->relations[$name] && $backref && $obj->{$backref} = $this;
902
      }
903
904
    } else {
905
      throw new ActiveRecordException("Relation $name not found.");
906
    }
907
908 2
    return $this->relations[$name];
909
  }
910
911
  /**
912
   * @return string
913
   */
914
  public function getTable(): string
915
  {
916
    return $this->table;
917
  }
918
919
  /**
920
   * Helper function for "GROUP BY".
921
   *
922
   * @param mixed $args
923
   *
924
   * @return $this
925
   */
926
  public function groupBy($args): self
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed.

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

Loading history...
927
  {
928
    $this->__call('groupBy', \func_get_args());
929
930
    return $this;
931
  }
932
933
  /**
934
   * Function to build insert SQL, and insert current record into database.
935
   *
936
   * @return bool|int <p>
937
   *                  If insert was successful, it will return the new id,
938
   *                  otherwise it will return false or true (if there are no dirty data).
939
   *                  </p>
940
   */
941 4
  public function insert()
942
  {
943 4
    if (!$this->db instanceof DB) {
944 3
      $this->db = DB::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\db\DB::getInstance() of type object<self> is incompatible with the declared type object<voku\db\DB> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
945
    }
946
947 4
    if (\count($this->dirty) === 0) {
948
      return true;
949
    }
950
951 4
    $value = $this->_filterParam($this->dirty);
952 4
    $this->insert = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property insert does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
953
        [
954 4
            'operator' => 'INSERT INTO ' . $this->table,
955 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => \array_keys($this->dirty)]),
956
        ]
957
    );
958 4
    $this->values = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property values does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
959
        [
960 4
            'operator' => 'VALUES',
961 4
            'target'   => new ActiveRecordExpressionsWrap(['target' => $value]),
962
        ]
963
    );
964
965 4
    $result = $this->execute($this->_buildSql(['insert', 'values']), $this->params);
966 4
    if ($result !== false) {
967 4
      $this->{$this->primaryKeyName} = $result;
968
969 4
      $this->resetDirty();
970 4
      $this->reset();
971
972 4
      return $result;
973
    }
974
975
    return false;
976
  }
977
978
  /**
979
   * @return bool
980
   */
981
  public function isNewDataAreDirty(): bool
982
  {
983
    return $this->new_data_are_dirty;
984
  }
985
986
  /**
987
   * Helper function to add condition into JOIN.
988
   *
989
   * @param string $table <p>The join table name.</p>
990
   * @param string $on    <p>The condition of ON.</p>
991
   * @param string $type  <p>The join type, like "LEFT", "INNER", "OUTER".</p>
992
   *
993
   * @return $this
994
   */
995 1
  public function join(string $table, string $on, string $type = 'LEFT'): self
996
  {
997 1
    $this->join = new ActiveRecordExpressions(
0 ignored issues
show
Documentation introduced by
The property join does not exist on object<voku\db\ActiveRecord>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1000 1
            'operator' => $type . ' JOIN',
1001 1
            'target'   => new ActiveRecordExpressions(
1002
                [
1003 1
                    'source'   => $table,
1004 1
                    'operator' => 'ON',
1005 1
                    'target'   => $on,
1006
                ]
1007
            ),
1008
        ]
1009
    );
1010
1011 1
    return $this;
1012
  }
1013
1014
  /**
1015
   * Helper function for "ORDER BY".
1016
   *
1017
   * @param mixed $args
1018
   *
1019
   * @return $this
1020
   */
1021 2
  public function orderBy($args): self
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed.

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

Loading history...
1022
  {
1023 2
    $this->__call('orderBy', \func_get_args());
1024
1025 2
    return $this;
1026
  }
1027
1028
  /**
1029
   * Helper function to query one record by sql and params.
1030
   *
1031
   * @param string    $sql            <p>
1032
   *                                  The SQL query to find the record.
1033
   *                                  </p>
1034
   * @param array     $param          <p>
1035
   *                                  The param will be bind to the $sql query.
1036
   *                                  </p>
1037
   * @param null|self $obj            <p>
1038
   *                                  The object, if find record in database, we will assign the attributes into
1039
   *                                  this object.
1040
   *                                  </p>
1041
   * @param bool      $single         <p>
1042
   *                                  If set to true, we will find record and fetch in current object, otherwise
1043
   *                                  will find all records.
1044
   *                                  </p>
1045
   *
1046
   * @return bool|$this|$this[]
1047
   */
1048 17
  public function query(string $sql, array $param = [], self $obj = null, bool $single = false)
1049
  {
1050 17
    $result = $this->execute($sql, $param);
1051
1052 17
    if ($result === false) {
1053
      return false;
1054
    }
1055
1056 17
    $useObject = \is_object($obj);
1057 17
    if ($useObject === true) {
1058 17
      $called_class = $obj;
1059
    } else {
1060
      $called_class = static::class;
1061
    }
1062
1063 17
    $this->setNewDataAreDirty(false);
1064
1065 17
    if ($single) {
1066 11
      $return = $result->fetchObject($called_class, null, true);
1067
    } else {
1068 8
      $return = $result->fetchAllObject($called_class, null);
1069
    }
1070
1071 17
    $this->setNewDataAreDirty(true);
1072
1073 17
    return $return;
1074
  }
1075
1076
  /**
1077
   * Function to reset the $params and $sqlExpressions.
1078
   *
1079
   * @return $this
1080
   */
1081 23
  public function reset(): self
1082
  {
1083 23
    $this->params = [];
1084 23
    $this->sqlExpressions = [];
1085
1086 23
    return $this;
1087
  }
1088
1089
  /**
1090
   * Reset the dirty data.
1091
   *
1092
   * @return $this
1093
   */
1094 6
  public function resetDirty(): self
1095
  {
1096 6
    $this->dirty = [];
1097
1098 6
    return $this;
1099
  }
1100
1101
  /**
1102
   * set the DB connection.
1103
   *
1104
   * @param DB $db
1105
   */
1106
  public function setDb(DB $db)
1107
  {
1108
    $this->db = $db;
1109
  }
1110
1111
  /**
1112
   * @param bool $bool
1113
   */
1114 17
  public function setNewDataAreDirty(bool $bool)
1115
  {
1116 17
    $this->new_data_are_dirty = $bool;
1117 17
  }
1118
1119
  /**
1120
   * @param mixed $primaryKey
1121
   * @param bool  $dirty
1122
   *
1123
   * @return $this
1124
   */
1125 1
  public function setPrimaryKey($primaryKey, bool $dirty = true): self
1126
  {
1127 1
    if (\property_exists($this, $this->primaryKeyName)) {
1128
      $this->{$this->primaryKeyName} = $primaryKey;
1129
    }
1130
1131 1
    if ($dirty === true) {
1132 1
      $this->dirty[$this->primaryKeyName] = $primaryKey;
1133
    } else {
1134
      $this->array[$this->primaryKeyName] = $primaryKey;
1135
    }
1136
1137 1
    return $this;
1138
  }
1139
1140
  /**
1141
   * @param string $primaryKeyName
1142
   *
1143
   * @return $this
1144
   */
1145
  public function setPrimaryKeyName(string $primaryKeyName): self
1146
  {
1147
    $this->primaryKeyName = $primaryKeyName;
1148
1149
    return $this;
1150
  }
1151
1152
  /**
1153
   * @param string $table
1154
   */
1155
  public function setTable(string $table)
1156
  {
1157
    $this->table = $table;
1158
  }
1159
1160
  /**
1161
   * Function to build update SQL, and update current record in database, just write the dirty data into database.
1162
   *
1163
   * @return bool|int <p>
1164
   *                  If update was successful, it will return the affected rows as int,
1165
   *                  otherwise it will return false or true (if there are no dirty data).
1166
   *                  </p>
1167
   */
1168 2
  public function update()
1169
  {
1170 2
    if (\count($this->dirty) == 0) {
1171
      return true;
1172
    }
1173
1174 2
    foreach ($this->dirty as $field => $value) {
1175 2
      $this->addCondition($field, '=', $value, ',', 'set');
1176
    }
1177
1178 2
    $result = $this->execute(
1179 2
        $this->eq($this->primaryKeyName, $this->{$this->primaryKeyName})->_buildSql(
1180
            [
1181 2
                'update',
1182
                'set',
1183
                'where',
1184
            ]
1185
        ),
1186 2
        $this->params
1187
    );
1188 2
    if ($result !== false) {
1189 2
      $this->resetDirty();
1190 2
      $this->reset();
1191
1192 2
      return $result;
1193
    }
1194
1195
    return false;
1196
  }
1197
1198
  /**
1199
   * Make wrap when build the SQL expressions of WHERE.
1200
   *
1201
   * @param string $op <p>If given, this param will build one "ActiveRecordExpressionsWrap" and include the stored
1202
   *                   expressions add into WHERE, otherwise it will stored the expressions into an array.</p>
1203
   *
1204
   * @return $this
1205
   */
1206 1
  public function wrap($op = null): self
1207
  {
1208 1
    if (1 === \func_num_args()) {
1209 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...
1210 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...
1211 1
        $this->_addCondition(
1212 1
            new ActiveRecordExpressionsWrap(
1213
                [
1214 1
                    'delimiter' => ' ',
1215 1
                    'target'    => $this->expressions,
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
1216
                ]
1217
            ),
1218 1
            'or' === \strtolower($op) ? 'OR' : 'AND'
1219
        );
1220
      }
1221 1
      $this->expressions = [];
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist. Did you mean defaultSqlExpressions?

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

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

Loading history...
1222
    } else {
1223 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...
1224
    }
1225
1226 1
    return $this;
1227
  }
1228
}
1229