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) { |
|
0 ignored issues
–
show
|
|||
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 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.