Record::offsetUnset()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Simply\Database;
4
5
use Simply\Database\Exception\InvalidRelationshipException;
6
7
/**
8
 * Represents data loaded from a database.
9
 * @author Riikka Kalliomäki <[email protected]>
10
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
11
 * @license http://opensource.org/licenses/mit-license.php MIT License
12
 */
13
class Record implements \ArrayAccess
14
{
15
    /** A record state when new record is being inserted to database */
16
    public const STATE_INSERT = 1;
17
18
    /** A record state when existing record is being updated in the database */
19
    public const STATE_UPDATE = 2;
20
21
    /** A record state when the record no longer exists in the database */
22
    public const STATE_DELETE = 3;
23
24
    /** @var Schema The schema for the record data */
25
    private $schema;
26
27
    /** @var array The primary key for the record at the time of retrieving */
28
    private $primaryKey;
29
30
    /** @var array Values for the record fields */
31
    private $values;
32
33
    /** @var bool[] Associative list of fields for the record that have been modified */
34
    private $changed;
35
36
    /** @var int The current state of the record */
37
    private $state;
38
39
    /** @var Record[][] Lists of referenced records for each loaded relationship */
40
    private $referencedRecords;
41
42
    /** @var Model|null The model associated with the record */
43
    private $model;
44
45
    /**
46
     * Record constructor.
47
     * @param Schema $schema The schema for the record data
48
     * @param Model|null $model The model associated with the record or null if it has not been initialized
49
     */
50 47
    public function __construct(Schema $schema, Model $model = null)
51
    {
52 47
        $this->primaryKey = [];
53 47
        $this->schema = $schema;
54 47
        $this->values = array_fill_keys($schema->getFields(), null);
55 47
        $this->state = self::STATE_INSERT;
56 47
        $this->changed = [];
57 47
        $this->referencedRecords = [];
58 47
        $this->model = $model;
59 47
    }
60
61
    /**
62
     * Returns the primary key for the record as it was at the time the record was loaded.
63
     * @return array Associative array of primary key fields and their values
64
     */
65 15
    public function getPrimaryKey(): array
66
    {
67 15
        if (empty($this->primaryKey)) {
68 1
            throw new \RuntimeException('Cannot refer to the record via primary key, if it is not defined');
69
        }
70
71 14
        return $this->primaryKey;
72
    }
73
74
    /**
75
     * Tells if the record is empty, i.e. none of the fields have any values.
76
     * @return bool True if all the fields values are null, false otherwise
77
     */
78 3
    public function isEmpty(): bool
79
    {
80 3
        foreach ($this->values as $value) {
81 3
            if ($value !== null) {
82 3
                return false;
83
            }
84
        }
85
86 1
        return true;
87
    }
88
89
    /**
90
     * Tells if the record is new and not yet inserted into the database.
91
     * @return bool True if the record has not been inserted into database, false otherwise
92
     */
93 15
    public function isNew(): bool
94
    {
95 15
        return $this->state === self::STATE_INSERT;
96
    }
97
98
    /**
99
     * Tells if the record has been deleted from the database.
100
     * @return bool True if the record no longer exists in the database, false otherwise
101
     */
102 15
    public function isDeleted(): bool
103
    {
104 15
        return $this->state === self::STATE_DELETE;
105
    }
106
107
    /**
108
     * Updates the state of the record after the appropriate database operation.
109
     * @param int $state The appropriate state depending on the performed database operation
110
     */
111 19
    public function updateState(int $state): void
112
    {
113 19
        $this->state = $state === self::STATE_DELETE ? self::STATE_DELETE : self::STATE_UPDATE;
114 19
        $this->changed = [];
115
116 19
        $this->updatePrimaryKey();
117 19
    }
118
119
    /**
120
     * Updates the stored primary key based on the field values.
121
     */
122 23
    private function updatePrimaryKey(): void
123
    {
124 23
        $this->primaryKey = [];
125
126 23
        foreach ($this->schema->getPrimaryKey() as $key) {
127 23
            $this->primaryKey[$key] = $this->values[$key];
128
        }
129 23
    }
130
131
    /**
132
     * Returns the schema for the record data.
133
     * @return Schema The schema for the record data
134
     */
135 39
    public function getSchema(): Schema
136
    {
137 39
        return $this->schema;
138
    }
139
140
    /**
141
     * Returns the model associated with the record and initializes it if has not been initialized yet.
142
     * @return Model The model associated with the record data
143
     */
144 20
    public function getModel(): Model
145
    {
146 20
        if ($this->model === null) {
147 20
            $this->model = $this->schema->createModel($this);
148
        }
149
150 20
        return $this->model;
151
    }
152
153
    /**
154
     * Tells if the referenced records for the given relationship has been loaded.
155
     * @param string $name Name of the relationship
156
     * @return bool True if the referenced records have been loaded, false if not
157
     */
158 5
    public function hasReferencedRecords(string $name): bool
159
    {
160 5
        $name = $this->getSchema()->getRelationship($name)->getName();
161
162 5
        return isset($this->referencedRecords[$name]);
163
    }
164
165
    /**
166
     * Loads the referenced records for the given relationship.
167
     * @param string $name Name of the relationship
168
     * @param Record[] $records List of records referenced by this record
169
     */
170 9
    public function setReferencedRecords(string $name, array $records): void
171
    {
172 9
        $name = $this->getSchema()->getRelationship($name)->getName();
173
174
        (function (self ... $records) use ($name): void {
175 9
            $this->referencedRecords[$name] = $records;
176 9
        })(... $records);
177 9
    }
178
179
    /**
180
     * Returns the list of referenced records for the given relationship.
181
     * @param string $name Name of the relationship
182
     * @return Record[] List of records referenced by this record
183
     */
184 9
    public function getReferencedRecords(string $name): array
185
    {
186 9
        $name = $this->getSchema()->getRelationship($name)->getName();
187
188 9
        if (!isset($this->referencedRecords[$name])) {
189 1
            throw new \RuntimeException('The referenced records have not been provided');
190
        }
191
192 8
        return $this->referencedRecords[$name];
193
    }
194
195
    /**
196
     * Sets the referenced fields in this record to reference the record of the given model.
197
     * @param string $name Name of the relationship
198
     * @param Model $model The model that this record should be reference
199
     */
200 7
    public function associate(string $name, Model $model): void
201
    {
202 7
        $relationship = $this->getSchema()->getRelationship($name);
203
204 7
        if (!$relationship->isUniqueRelationship()) {
205 1
            throw new InvalidRelationshipException('A single model can only be associated to an unique relationships');
206
        }
207
208 6
        $keys = $relationship->getFields();
209 6
        $fields = $relationship->getReferencedFields();
210 6
        $record = $model->getDatabaseRecord();
211
212 6
        if ($record->getSchema() !== $relationship->getReferencedSchema()) {
213 1
            throw new \InvalidArgumentException('The associated record belongs to incorrect schema');
214
        }
215
216 5
        while ($keys !== []) {
217 5
            $value = $record[array_pop($fields)];
218
219 5
            if ($value === null) {
220 1
                throw new \RuntimeException('Cannot associate with models with nulls in referenced fields');
221
            }
222
223 4
            $this[array_pop($keys)] = $value;
224
        }
225
226 4
        $this->referencedRecords[$relationship->getName()] = [$record];
227 4
        $reverse = $relationship->getReverseRelationship();
228
229 4
        if ($reverse->isUniqueRelationship()) {
230 3
            $record->referencedRecords[$reverse->getName()] = [$this];
231 4
        } elseif ($record->hasReferencedRecords($reverse->getName())) {
232 1
            $record->referencedRecords[$reverse->getName()][] = $this;
233
        }
234 4
    }
235
236
    /**
237
     * Sets the referencing fields in the record of the given model to reference this record.
238
     * @param string $name Name of the relationship
239
     * @param Model $model The model that should reference this record
240
     */
241 4
    public function addAssociation(string $name, Model $model): void
242
    {
243 4
        $relationship = $this->getSchema()->getRelationship($name);
244
245 4
        if ($relationship->isUniqueRelationship()) {
246 1
            throw new InvalidRelationshipException('Cannot add a new model to an unique relationship');
247
        }
248
249 3
        $model->getDatabaseRecord()->associate($relationship->getReverseRelationship()->getName(), $this->getModel());
250 3
    }
251
252
    /**
253
     * Returns the model of the referenced record in a unique relationship.
254
     * @param string $name Name of the relationship
255
     * @return Model|null The referenced model, or null if no model is referenced by this record
256
     */
257 5
    public function getRelatedModel(string $name): ?Model
258
    {
259 5
        $relationship = $this->getSchema()->getRelationship($name);
260
261 5
        if (!$relationship->isUniqueRelationship()) {
262 1
            throw new InvalidRelationshipException('A single model can only be fetched for an unique relationship');
263
        }
264
265 4
        $records = $this->getReferencedRecords($name);
266
267 4
        if (empty($records)) {
268 2
            return null;
269
        }
270
271 3
        return $this->getReferencedRecords($name)[0]->getModel();
272
    }
273
274
    /**
275
     * Returns list of models referenced by this record via the given relationship.
276
     * @param string $name Name of the relationship
277
     * @return array List of models referenced by this record
278
     */
279 2
    public function getRelatedModels(string $name): array
280
    {
281 2
        $relationship = $this->getSchema()->getRelationship($name);
282
283 2
        if ($relationship->isUniqueRelationship()) {
284 1
            throw new InvalidRelationshipException('Cannot fetch multiple models for an unique relationship');
285
        }
286
287 1
        $models = [];
288
289 1
        foreach ($this->getReferencedRecords($name) as $record) {
290 1
            $models[] = $record->getModel();
291
        }
292
293 1
        return $models;
294
    }
295
296
    /**
297
     * Gets list of models that are referenced by records that this record references.
298
     * @param string $proxy Name of the relationship in this record
299
     * @param string $name Name of the relationship in the proxy record
300
     * @return array List of models referenced by the records referenced by this record
301
     */
302 5
    public function getRelatedModelsByProxy(string $proxy, string $name): array
303
    {
304 5
        $proxyRelationship = $this->getSchema()->getRelationship($proxy);
305 5
        $relationship = $proxyRelationship->getReferencedSchema()->getRelationship($name);
306
307 5
        if ($proxyRelationship->isUniqueRelationship()) {
308 1
            throw new InvalidRelationshipException('Cannot fetch models via an unique proxy relationship');
309
        }
310
311 4
        if (!$relationship->isUniqueRelationship()) {
312 1
            throw new InvalidRelationshipException('Cannot fetch models via proxy without a unique relationship');
313
        }
314
315 3
        $models = [];
316
317 3
        foreach ($this->getReferencedRecords($proxy) as $record) {
318 3
            $records = $record->getReferencedRecords($name);
319
320 3
            if (empty($records)) {
321 2
                continue;
322
            }
323
324 2
            $models[] = $records[0]->getModel();
325
        }
326
327 3
        return $models;
328
    }
329
330
    /**
331
     * Returns list of records recursively referenced by this record or any referenced record.
332
     * @return Record[] List of all referenced records and any record they recursively reference
333
     */
334 5
    public function getAllReferencedRecords(): array
335
    {
336
        /** @var Record[] $records */
337 5
        $records = [spl_object_id($this) => $this];
338
339
        do {
340 5
            foreach (current($records)->referencedRecords as $recordList) {
341 1
                foreach ($recordList as $record) {
342 1
                    $id = spl_object_id($record);
343
344 1
                    if (!isset($records[$id])) {
345 1
                        $records[$id] = $record;
346
                    }
347
                }
348
            }
349 5
        } while (next($records) !== false);
350
351 5
        return array_values($records);
352
    }
353
354
    /**
355
     * Sets the values for the fields in the record loaded from the database.
356
     * @param array $row Value for the fields in this record
357
     */
358 18
    public function setDatabaseValues(array $row): void
359
    {
360 18
        if (array_keys($row) !== array_keys($this->values)) {
361 2
            if (array_diff_key($row, $this->values) !== [] || \count($row) !== \count($this->values)) {
362 1
                throw new \InvalidArgumentException('Invalid set of record database values provided');
363
            }
364
365 1
            $row = array_replace($this->values, $row);
366
        }
367
368 17
        $this->values = $row;
369 17
        $this->state = self::STATE_UPDATE;
370 17
        $this->changed = [];
371 17
        $this->updatePrimaryKey();
372 17
    }
373
374
    /**
375
     * Returns the values for the fields in this record for storing in database.
376
     * @return array Associative list of fields and their values
377
     */
378 16
    public function getDatabaseValues(): array
379
    {
380 16
        return $this->values;
381
    }
382
383
    /**
384
     * Returns list of all fields that have been modified since the records state was last updated.
385
     * @return string[] List of fields updated since last time the state was updated
386
     */
387 16
    public function getChangedFields(): array
388
    {
389 16
        return array_keys($this->changed);
390
    }
391
392
    /**
393
     * Tells if the value in the given field is other than null.
394
     * @param string $offset Name of the field
395
     * @return bool True if the value is something else than null, false otherwise
396
     */
397 1
    public function offsetExists($offset)
398
    {
399 1
        return $this->offsetGet($offset) !== null;
400
    }
401
402
    /**
403
     * Returns the value for the given field.
404
     * @param string $offset Name of the field
405
     * @return mixed Value for the given field
406
     */
407 18
    public function offsetGet($offset)
408
    {
409 18
        $offset = (string) $offset;
410
411 18
        if (!array_key_exists($offset, $this->values)) {
412 1
            throw new \InvalidArgumentException("Invalid record field '$offset'");
413
        }
414
415 17
        return $this->values[$offset];
416
    }
417
418
    /**
419
     * Sets the value for the given field.
420
     * @param string $offset Name of the field
421
     * @param mixed $value Value for the given field
422
     */
423 27
    public function offsetSet($offset, $value)
424
    {
425 27
        $offset = (string) $offset;
426
427 27
        if (!array_key_exists($offset, $this->values)) {
428 1
            throw new \InvalidArgumentException("Invalid record field '$offset'");
429
        }
430
431 26
        $this->values[$offset] = $value;
432 26
        $this->changed[$offset] = true;
433 26
    }
434
435
    /**
436
     * Sets the value of the given field to null and marks it unchanged, if the record has not yet been inserted.
437
     * @param string $offset The name of the field
438
     */
439 1
    public function offsetUnset($offset)
440
    {
441 1
        $this->offsetSet($offset, null);
442
443 1
        if ($this->state === self::STATE_INSERT) {
444 1
            unset($this->changed[$offset]);
445
        }
446 1
    }
447
}
448