Relationship::isUniqueRelationship()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 a relationship between two schemas.
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 Relationship
14
{
15
    /** @var string Name of the relationship */
16
    private $name;
17
18
    /** @var Schema The referring schema */
19
    private $schema;
20
21
    /** @var string[] The referring fields */
22
    private $fields;
23
24
    /** @var Schema The referenced schema */
25
    private $referencedSchema;
26
27
    /** @var string[] The referenced fields */
28
    private $referencedFields;
29
30
    /** @var bool Whether the relationship is unique or not */
31
    private $unique;
32
33
    /** @var Relationship|null The reverse relationship or null if not initialized yet */
34
    private $reverse;
35
36
    /**
37
     * Relationship constructor.
38
     * @param string $name Name of the relationship
39
     * @param Schema $schema The referring schema
40
     * @param string[] $fields The referring fields
41
     * @param Schema $referencedSchema The referenced schema
42
     * @param string[] $referencedFields The referenced fields
43
     * @param bool $unique Whether the relationship can only reference a single record or not
44
     */
45 37
    public function __construct(
46
        string $name,
47
        Schema $schema,
48
        array $fields,
49
        Schema $referencedSchema,
50
        array $referencedFields,
51
        bool $unique
52
    ) {
53
        $formatStrings = function (string ... $strings): array {
54 37
            return $strings;
55 37
        };
56
57 37
        $this->name = $name;
58 37
        $this->schema = $schema;
59 37
        $this->fields = $formatStrings(... $fields);
60 37
        $this->referencedSchema = $referencedSchema;
61 37
        $this->referencedFields = $formatStrings(... $referencedFields);
62 37
        $this->unique = $unique || array_diff($this->referencedSchema->getPrimaryKey(), $this->referencedFields) === [];
63
64 37
        if (empty($this->fields) || \count($this->fields) !== \count($this->referencedFields)) {
65 1
            throw new \InvalidArgumentException('Unexpected list of fields in relationship');
66
        }
67
68 36
        if (array_diff($this->fields, $this->schema->getFields()) !== []) {
69 1
            throw new \InvalidArgumentException('The referencing fields must be defined in the referencing schema');
70
        }
71
72 35
        if (array_diff($this->referencedFields, $this->referencedSchema->getFields()) !== []) {
73 1
            throw new \InvalidArgumentException('The referenced fields must be defined in the referenced schema');
74
        }
75 34
    }
76
77
    /**
78
     * Returns the name of the relationship.
79
     * @return string Name of the relationship
80
     */
81 11
    public function getName(): string
82
    {
83 11
        return $this->name;
84
    }
85
86
    /**
87
     * Returns the referring schema.
88
     * @return Schema The referring schema
89
     */
90 10
    public function getSchema(): Schema
91
    {
92 10
        return $this->schema;
93
    }
94
95
    /**
96
     * Returns the referring fields.
97
     * @return string[] The referring fields
98
     */
99 17
    public function getFields(): array
100
    {
101 17
        return $this->fields;
102
    }
103
104
    /**
105
     * Returns the referenced schema.
106
     * @return Schema The referenced schema
107
     */
108 27
    public function getReferencedSchema(): Schema
109
    {
110 27
        return $this->referencedSchema;
111
    }
112
113
    /**
114
     * Returns referenced fields.
115
     * @return string[] The referenced fields
116
     */
117 16
    public function getReferencedFields(): array
118
    {
119 16
        return $this->referencedFields;
120
    }
121
122
    /**
123
     * Tells if the relationship is unique or not.
124
     * @return bool True if the relationship can only refer to a single record, false otherwise
125
     */
126 23
    public function isUniqueRelationship(): bool
127
    {
128 23
        return $this->unique;
129
    }
130
131
    /**
132
     * Returns the reverse relationship.
133
     * @return Relationship The reverse relationship
134
     */
135 9
    public function getReverseRelationship(): self
136
    {
137 9
        if ($this->reverse === null) {
138 9
            $this->reverse = $this->detectReverseRelationship();
139
        }
140
141 7
        return $this->reverse;
142
    }
143
144
    /**
145
     * Detects the reverse relationship in the referenced schema.
146
     * @return Relationship The reverse relationship in the referenced schema
147
     */
148 9
    private function detectReverseRelationship(): self
149
    {
150 9
        $reverse = array_filter(
151 9
            $this->referencedSchema->getRelationships(),
152
            function (self $relationship): bool {
153 9
                return $this->isReverseRelationship($relationship);
154 9
            }
155
        );
156
157 9
        if (\count($reverse) !== 1) {
158 2
            throw new InvalidRelationshipException('Could not find a valid reverse relationship');
159
        }
160
161 7
        return array_pop($reverse);
162
    }
163
164
    /**
165
     * Tells if the given relationship is a reverse relationship to this relationship.
166
     * @param Relationship $relationship The relationship to test
167
     * @return bool True if the given relationship is a reverse relationship, false if not
168
     */
169 9
    private function isReverseRelationship(self $relationship): bool
170
    {
171 9
        return $relationship->getSchema() === $this->getReferencedSchema()
172 9
            && $relationship->getReferencedSchema() === $this->getSchema()
173 9
            && $relationship->getFields() === $this->getReferencedFields()
174 9
            && $relationship->getReferencedFields() === $this->getFields();
175
    }
176
177
    /**
178
     * Fills this relationship for the given records from the list of given records.
179
     * @param Record[] $records The records to fill
180
     * @param Record[] $referencedRecords All the records referenced by the list of records to fill
181
     */
182 8
    public function fillRelationship(array $records, array $referencedRecords): void
183
    {
184 8
        if (\count($this->getFields()) !== 1) {
185 1
            throw new InvalidRelationshipException('Relationship fill is not supported for composite foreign keys');
186
        }
187
188 7
        if (empty($records)) {
189 1
            return;
190
        }
191
192 7
        $this->assignSortedRecords($records, $this->getSortedRecords($referencedRecords));
193 3
    }
194
195
    /**
196
     * Returns list of records sorted by the value of the referenced field.
197
     * @param Record[] $records The list of records to sort
198
     * @return Record[][] Lists of records sorted by value of the referenced field
199
     */
200 7
    private function getSortedRecords(array $records): array
201
    {
202 7
        $schema = $this->getReferencedSchema();
203 7
        $field = $this->getReferencedFields()[0];
204 7
        $unique = $this->isUniqueRelationship();
205 7
        $sorted = [];
206
207 7
        foreach ($records as $record) {
208 6
            if ($record->getSchema() !== $schema) {
209 1
                throw new \InvalidArgumentException('The referenced records must all belong to the referenced schema');
210
            }
211
212 5
            $value = $record[$field];
213
214 5
            if ($value === null) {
215 1
                continue;
216
            }
217
218 5
            if ($unique && isset($sorted[$value])) {
219 2
                throw new InvalidRelationshipException('Multiple records detected for unique relationship');
220
            }
221
222 5
            $sorted[$value][] = $record;
223
        }
224
225 4
        return $sorted;
226
    }
227
228
    /**
229
     * Fills the relationships in given records from the sorted list of records.
230
     * @param Record[] $records List of records to fill
231
     * @param Record[][] $sorted List of records sorted by the value of the referenced field
232
     */
233 4
    private function assignSortedRecords(array $records, array $sorted): void
234
    {
235 4
        $schema = $this->getSchema();
236 4
        $name = $this->getName();
237 4
        $field = $this->getFields()[0];
238
239 4
        $fillReverse = $this->getReverseRelationship()->isUniqueRelationship();
240 4
        $reverse = $this->getReverseRelationship()->getName();
241
242 4
        foreach ($records as $record) {
243 4
            if ($record->getSchema() !== $schema) {
244 1
                throw new \InvalidArgumentException('The filled records must all belong to the referencing schema');
245
            }
246
247 3
            $value = $record[$field];
248 3
            $sortedRecords = $value === null || empty($sorted[$value]) ? [] : $sorted[$value];
249 3
            $record->setReferencedRecords($name, $sortedRecords);
250
251 3
            if ($fillReverse) {
252 3
                foreach ($sortedRecords as $reverseRecord) {
253 3
                    $reverseRecord->setReferencedRecords($reverse, [$record]);
254
                }
255
            }
256
        }
257 3
    }
258
259
    /**
260
     * Fills this unique relationship for a single record.
261
     * @param Record $record The record to fill
262
     * @param Record $referencedRecord The referenced record
263
     */
264 4
    public function fillSingleRecord(Record $record, Record $referencedRecord): void
265
    {
266 4
        if (!$this->isUniqueRelationship()) {
267 1
            throw new InvalidRelationshipException('Only unique relationships can be filled with single records');
268
        }
269
270 3
        if ($referencedRecord->isEmpty()) {
271 1
            $record->setReferencedRecords($this->getName(), []);
272 1
            return;
273
        }
274
275 3
        $keys = $this->getFields();
276 3
        $fields = $this->getReferencedFields();
277
278 3
        while ($keys !== []) {
279 3
            if ((string) $record[array_pop($keys)] !== (string) $referencedRecord[array_pop($fields)]) {
280 1
                throw new \InvalidArgumentException('The provided records are not related');
281
            }
282
        }
283
284 2
        $record->setReferencedRecords($this->getName(), [$referencedRecord]);
285 2
        $reverse = $this->getReverseRelationship();
286
287 2
        if ($reverse->isUniqueRelationship()) {
288 1
            $referencedRecord->setReferencedRecords($reverse->getName(), [$record]);
289
        }
290 2
    }
291
}
292