Completed
Branch 7-dev (bf2895)
by Oscar
03:53
created

Row::__set()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.8657
c 0
b 0
f 0
cc 6
nc 5
nop 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace SimpleCrud;
5
6
use BadMethodCallException;
7
use JsonSerializable;
8
use RuntimeException;
9
use SimpleCrud\Events\BeforeSaveRow;
10
use SimpleCrud\Query\Select;
11
12
/**
13
 * Stores the data of an table row.
14
 */
15
class Row implements JsonSerializable
16
{
17
    private $table;
18
    private $values = [];
19
    private $changes = [];
20
    private $data = [];
21
22
    public function __construct(Table $table, array $values)
23
    {
24
        $this->table = $table;
25
26
        if (empty($values['id'])) {
27
            $this->values = $table->getDefaults();
28
            $this->changes = $table->getDefaults($values);
29
            unset($this->changes['id']);
30
        } else {
31
            $this->values = $table->getDefaults($values);
32
        }
33
    }
34
35
    public function __debugInfo(): array
36
    {
37
        return [
38
            'table' => (string) $this->table,
39
            'values' => $this->values,
40
            'changes' => $this->changes,
41
            'data' => $this->data,
42
        ];
43
    }
44
45
    public function __call(string $name, array $arguments): Select
46
    {
47
        $db = $this->table->getDatabase();
48
49
        //Relations
50
        if (isset($db->$name)) {
51
            return $this->select($db->$name);
52
        }
53
54
        throw new BadMethodCallException(
55
            sprintf('Invalid method call %s', $name)
56
        );
57
    }
58
59
    public function setData(array $data): self
60
    {
61
        $this->data = $data + $this->data;
62
63
        return $this;
64
    }
65
66
    /**
67
     * @param Row|RowCollection|null $row
68
     */
69
    public function link(Table $table, $row = null): self
70
    {
71
        return $this->setData([$table->getName() => $row]);
72
    }
73
74
    /**
75
     * @see JsonSerializable
76
     */
77
    public function jsonSerialize()
78
    {
79
        return $this->toArray();
80
    }
81
82
    /**
83
     * Magic method to stringify the values.
84
     */
85
    public function __toString()
86
    {
87
        return json_encode($this, JSON_NUMERIC_CHECK);
88
    }
89
90
    /**
91
     * Returns the table associated with this row
92
     */
93
    public function getTable(): Table
94
    {
95
        return $this->table;
96
    }
97
98
    /**
99
     * Returns the value of:
100
     * - a value field
101
     * - a related table
102
     */
103
    public function &__get(string $name)
104
    {
105
        if ($name === 'id') {
106
            return $this->values['id'];
107
        }
108
109
        //It's a value
110
        if ($valueName = $this->getValueName($name)) {
111
            $value = $this->getValue($valueName);
112
            return $value;
113
        }
114
115
        //It's custom data
116
        if (array_key_exists($name, $this->data)) {
117
            return $this->data[$name];
118
        }
119
120
        $db = $this->table->getDatabase();
121
122
        if (isset($db->$name)) {
123
            $this->setData([
124
                $name => $this->select($db->$name)->run(),
125
            ]);
126
127
            return $this->data[$name];
128
        }
129
130
        throw new RuntimeException(
131
            sprintf('Undefined property "%s" in the table %s', $name, $this->table)
132
        );
133
    }
134
135
    /**
136
     * Change the value of
137
     * - a field
138
     * - a localized field
139
     * @param mixed $value
140
     */
141
    public function __set(string $name, $value)
142
    {
143
        if ($name === 'id') {
144
            if (!is_null($this->values['id']) && !is_null($value)) {
145
                throw new RuntimeException('The field "id" cannot be overrided');
146
            }
147
148
            $this->values['id'] = $value;
149
150
            return $value;
151
        }
152
153
        //It's a value
154
        if ($valueName = $this->getValueName($name)) {
155
            if ($this->values[$valueName] === $value) {
156
                unset($this->changes[$valueName]);
157
            } else {
158
                $this->changes[$valueName] = $value;
159
            }
160
161
            return $value;
162
        }
163
164
        throw new RuntimeException(
165
            sprintf('The field %s does not exists', $name)
166
        );
167
    }
168
169
    /**
170
     * Check whether a value is set or not
171
     */
172
    public function __isset(string $name): bool
173
    {
174
        $valueName = $this->getValueName($name);
175
176
        return ($valueName && !is_null($this->getValue($valueName))) || isset($this->data[$name]);
0 ignored issues
show
Bug Best Practice introduced by
The expression $valueName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
177
    }
178
179
    /**
180
     * Removes the value of a field
181
     */
182
    public function __unset(string $name)
183
    {
184
        unset($this->data[$name]);
185
186
        $this->__set($name, null);
187
    }
188
189
    /**
190
     * Returns an array with all fields of the row
191
     */
192
    public function toArray(): array
193
    {
194
        return $this->changes + $this->values;
195
    }
196
197
    /**
198
     * Edit the values using an array
199
     */
200
    public function edit(array $values): self
201
    {
202
        foreach ($values as $name => $value) {
203
            $this->__set($name, $value);
204
        }
205
206
        return $this;
207
    }
208
209
    /**
210
     * Insert/update the row in the database
211
     */
212
    public function save(): self
213
    {
214
        if (!empty($this->changes)) {
215
            $eventDispatcher = $this->table->getEventDispatcher();
216
217
            if ($eventDispatcher) {
218
                $eventDispatcher->dispatch(new BeforeSaveRow($this));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\BeforeSaveRow($this) is of type object<SimpleCrud\Events\BeforeSaveRow>, but the function expects a object<Psr\EventDispatcher\object>.

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...
219
            }
220
221
            if (empty($this->id)) {
222
                $this->id = $this->table->insert($this->changes)->run();
0 ignored issues
show
Documentation Bug introduced by
The method insert does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
223
            } else {
224
                $this->table->update($this->changes)
0 ignored issues
show
Documentation Bug introduced by
The method update does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
225
                    ->where('id = ', $this->id)
226
                    ->run();
227
            }
228
229
            $this->values = $this->toArray();
230
            $this->changes = [];
231
            $this->table->cache($this);
232
        }
233
234
        return $this;
235
    }
236
237
    /**
238
     * Delete the row in the database
239
     */
240
    public function delete(): self
241
    {
242
        $id = $this->id;
243
244
        if (!empty($id)) {
245
            $this->table->delete()
0 ignored issues
show
Documentation Bug introduced by
The method delete does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
246
                ->where('id = ', $id)
247
                ->run();
248
249
            $this->values['id'] = null;
250
        }
251
252
        return $this;
253
    }
254
255
    /**
256
     * Relate this row with other rows
257
     */
258
    public function relate(Row ...$rows): self
259
    {
260
        $table1 = $this->table;
261
262
        foreach ($rows as $row) {
263
            $table2 = $row->getTable();
264
265
            //Has one
266
            if ($field = $table1->getJoinField($table2)) {
267
                $this->{$field->getName()} = $row->id;
268
                continue;
269
            }
270
271
            //Has many
272
            if ($field = $table2->getJoinField($table1)) {
273
                $row->{$field->getName()} = $this->id;
274
                $row->save();
275
                continue;
276
            }
277
278
            //Has many to many
279
            if ($joinTable = $table1->getJoinTable($table2)) {
280
                $joinTable->insert([
281
                    $joinTable->getJoinField($table1)->getName() => $this->id,
282
                    $joinTable->getJoinField($table2)->getName() => $row->id,
283
                ])
284
                ->run();
285
286
                continue;
287
            }
288
289
            throw new RuntimeException(
290
                sprintf('The tables %s and %s are not related', $table1, $table2)
291
            );
292
        }
293
294
        return $this->save();
295
    }
296
297
    /**
298
     * Unrelate this row with other rows
299
     */
300
    public function unrelate(Row ...$rows): self
301
    {
302
        $table1 = $this->table;
303
304
        foreach ($rows as $row) {
305
            $table2 = $row->getTable();
306
307
            //Has one
308
            if ($field = $table1->getJoinField($table2)) {
309
                $this->{$field->getName()} = null;
310
                continue;
311
            }
312
313
            //Has many
314
            if ($field = $table2->getJoinField($table1)) {
315
                $row->{$field->getName()} = null;
316
                $row->save();
317
                continue;
318
            }
319
320
            //Has many to many
321
            if ($joinTable = $table1->getJoinTable($table2)) {
322
                $joinTable->delete()
323
                    ->where("{$joinTable->getJoinField($table1)} = ", $this->id)
324
                    ->where("{$joinTable->getJoinField($table2)} = ", $row->id)
325
                    ->run();
326
327
                continue;
328
            }
329
330
            throw new RuntimeException(
331
                sprintf('The tables %s and %s are not related', $table1, $table2)
332
            );
333
        }
334
335
        return $this->save();
336
    }
337
338
    /**
339
     * Unrelate this row with all rows of other tables
340
     */
341
    public function unrelateAll(Table ...$tables): self
342
    {
343
        $table1 = $this->table;
344
345
        foreach ($tables as $table2) {
346
            //Has one
347
            if ($field = $table1->getJoinField($table2)) {
348
                $this->{$field->getName()} = null;
349
                continue;
350
            }
351
352
            //Has many
353
            if ($field = $table2->getJoinField($table1)) {
354
                $table2->update([
0 ignored issues
show
Documentation Bug introduced by
The method update does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
355
                    $field->getName() => null,
356
                ])
357
                ->relatedWith($table1)
358
                ->run();
359
                continue;
360
            }
361
362
            //Has many to many
363
            if ($joinTable = $table1->getJoinTable($table2)) {
364
                $joinTable->delete()
365
                    ->where("{$joinTable->getJoinField($table1)} = ", $this->id)
366
                    ->where("{$joinTable->getJoinField($table2)} IS NOT NULL")
367
                    ->run();
368
369
                continue;
370
            }
371
372
            throw new RuntimeException(
373
                sprintf('The tables %s and %s are not related', $table1, $table2)
374
            );
375
        }
376
377
        return $this->save();
378
    }
379
380
    /**
381
     * Creates a select query of a table related with this row
382
     */
383
    public function select(Table $table): Select
384
    {
385
        //Has one
386
        if ($this->table->getJoinField($table)) {
387
            return $table->select()->one()->relatedWith($this);
0 ignored issues
show
Documentation Bug introduced by
The method select does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
388
        }
389
390
        return $table->select()->relatedWith($this);
0 ignored issues
show
Documentation Bug introduced by
The method select does not exist on object<SimpleCrud\Table>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
391
    }
392
393
    /**
394
     * Return the real field name
395
     */
396
    private function getValueName(string $name): ?string
397
    {
398
        if (array_key_exists($name, $this->values)) {
399
            return $name;
400
        }
401
402
        //It's a localizable field
403
        $language = $this->table->getDatabase()->getConfig(Database::CONFIG_LOCALE);
404
405
        if (!is_null($language)) {
406
            $name .= "_{$language}";
407
408
            if (array_key_exists($name, $this->values)) {
409
                return $name;
410
            }
411
        }
412
413
        return null;
414
    }
415
416
    private function getValue(string $name)
417
    {
418
        return array_key_exists($name, $this->changes) ? $this->changes[$name] : $this->values[$name];
419
    }
420
}
421