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
|
|
|
|