1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Cycle\Schema\Definition; |
||
6 | |||
7 | use Cycle\ORM\MapperInterface; |
||
8 | use Cycle\ORM\RepositoryInterface; |
||
9 | use Cycle\ORM\Select\ScopeInterface; |
||
10 | use Cycle\ORM\Select\SourceInterface; |
||
11 | use Cycle\Schema\Definition\Map\FieldMap; |
||
12 | use Cycle\Schema\Definition\Map\ForeignKeyMap; |
||
13 | use Cycle\Schema\Definition\Map\OptionMap; |
||
14 | use Cycle\Schema\Definition\Map\RelationMap; |
||
15 | use Cycle\Schema\Exception\EntityException; |
||
16 | use Cycle\Schema\SchemaModifierInterface; |
||
17 | |||
18 | /** |
||
19 | * Contains information about specific entity definition. |
||
20 | * |
||
21 | * @template TEntity of object |
||
22 | */ |
||
23 | final class Entity |
||
24 | { |
||
25 | private OptionMap $options; |
||
26 | |||
27 | /** |
||
28 | * @var non-empty-string|null |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
29 | */ |
||
30 | private ?string $role = null; |
||
31 | |||
32 | /** |
||
33 | * @var class-string<TEntity>|null |
||
0 ignored issues
–
show
|
|||
34 | */ |
||
35 | private ?string $class = null; |
||
36 | |||
37 | /** |
||
38 | * @var non-empty-string|null |
||
0 ignored issues
–
show
|
|||
39 | */ |
||
40 | private ?string $database = null; |
||
41 | |||
42 | /** |
||
43 | * @var non-empty-string|null |
||
0 ignored issues
–
show
|
|||
44 | */ |
||
45 | private ?string $tableName = null; |
||
46 | |||
47 | /** |
||
48 | * @var class-string<MapperInterface>|null |
||
0 ignored issues
–
show
|
|||
49 | 1438 | */ |
|
50 | private ?string $mapper = null; |
||
51 | 1438 | ||
52 | 1438 | /** |
|
53 | 1438 | * @var class-string<SourceInterface>|null |
|
0 ignored issues
–
show
|
|||
54 | 1438 | */ |
|
55 | 1438 | private ?string $source = null; |
|
56 | |||
57 | /** |
||
58 | * @var class-string<ScopeInterface>|null |
||
0 ignored issues
–
show
|
|||
59 | */ |
||
60 | 56 | private ?string $scope = null; |
|
61 | |||
62 | 56 | /** |
|
63 | 56 | * @var class-string<RepositoryInterface<TEntity>>|null |
|
0 ignored issues
–
show
|
|||
64 | 56 | */ |
|
65 | 56 | private ?string $repository = null; |
|
66 | 56 | ||
67 | /** |
||
68 | 16 | * @var class-string|class-string[]|non-empty-string|non-empty-string[]|null |
|
0 ignored issues
–
show
|
|||
69 | */ |
||
70 | 16 | private array|string|null $typecast = null; |
|
71 | |||
72 | private array $schema = []; |
||
73 | 1356 | private FieldMap $fields; |
|
74 | private RelationMap $relations; |
||
75 | 1356 | private FieldMap $primaryFields; |
|
76 | private array $schemaModifiers = []; |
||
77 | 1356 | private ?Inheritance $inheritance = null; |
|
78 | |||
79 | /** @var class-string|null */ |
||
0 ignored issues
–
show
|
|||
80 | 1364 | private ?string $stiParent = null; |
|
81 | |||
82 | 1364 | private ForeignKeyMap $foreignKeys; |
|
83 | |||
84 | public function __construct() |
||
85 | 1318 | { |
|
86 | $this->options = new OptionMap(); |
||
87 | 1318 | $this->fields = new FieldMap(); |
|
88 | $this->primaryFields = new FieldMap(); |
||
89 | 1318 | $this->relations = new RelationMap(); |
|
90 | $this->foreignKeys = new ForeignKeyMap(); |
||
91 | } |
||
92 | 1250 | ||
93 | public function getOptions(): OptionMap |
||
94 | 1250 | { |
|
95 | return $this->options; |
||
96 | } |
||
97 | 2 | ||
98 | /** |
||
99 | 2 | * @param non-empty-string $role |
|
0 ignored issues
–
show
|
|||
100 | */ |
||
101 | 2 | public function setRole(string $role): self |
|
102 | { |
||
103 | $this->role = $role; |
||
104 | 790 | ||
105 | return $this; |
||
106 | 790 | } |
|
107 | |||
108 | /** |
||
109 | 2 | * @return non-empty-string|null |
|
0 ignored issues
–
show
|
|||
110 | */ |
||
111 | 2 | public function getRole(): ?string |
|
112 | { |
||
113 | 2 | return $this->role; |
|
114 | } |
||
115 | |||
116 | 790 | /** |
|
117 | * @param class-string<TEntity> $class |
||
0 ignored issues
–
show
|
|||
118 | 790 | */ |
|
119 | public function setClass(string $class): self |
||
120 | { |
||
121 | 2 | $this->class = $class; |
|
122 | |||
123 | 2 | return $this; |
|
124 | } |
||
125 | 2 | ||
126 | /** |
||
127 | * @return class-string<TEntity>|null |
||
0 ignored issues
–
show
|
|||
128 | 790 | */ |
|
129 | public function getClass(): ?string |
||
130 | 790 | { |
|
131 | return $this->class; |
||
132 | } |
||
133 | 2 | ||
134 | /** |
||
135 | 2 | * @param class-string<MapperInterface>|null $mapper |
|
0 ignored issues
–
show
|
|||
136 | */ |
||
137 | 2 | public function setMapper(?string $mapper): self |
|
138 | { |
||
139 | $this->mapper = $mapper; |
||
140 | 790 | ||
141 | return $this; |
||
142 | 790 | } |
|
143 | |||
144 | /** |
||
145 | * @return class-string<MapperInterface>|null |
||
0 ignored issues
–
show
|
|||
146 | */ |
||
147 | public function getMapper(): ?string |
||
148 | { |
||
149 | return $this->normalizeClass($this->mapper); |
||
150 | 598 | } |
|
151 | |||
152 | 598 | /** |
|
153 | * @param class-string<SourceInterface>|null $source |
||
0 ignored issues
–
show
|
|||
154 | 598 | */ |
|
155 | public function setSource(?string $source): self |
||
156 | { |
||
157 | $this->source = $source; |
||
158 | |||
159 | return $this; |
||
160 | 794 | } |
|
161 | |||
162 | 794 | /** |
|
163 | * @return class-string<SourceInterface>|null |
||
0 ignored issues
–
show
|
|||
164 | */ |
||
165 | 1328 | public function getSource(): ?string |
|
166 | { |
||
167 | 1328 | return $this->normalizeClass($this->source); |
|
168 | } |
||
169 | |||
170 | 1252 | /** |
|
171 | * @param class-string<ScopeInterface>|null $scope |
||
0 ignored issues
–
show
|
|||
172 | 1252 | */ |
|
173 | public function setScope(?string $scope): self |
||
174 | { |
||
175 | 16 | $this->scope = $scope; |
|
176 | |||
177 | 16 | return $this; |
|
178 | 16 | } |
|
179 | |||
180 | /** |
||
181 | * @return class-string<ScopeInterface>|null |
||
0 ignored issues
–
show
|
|||
182 | */ |
||
183 | public function getScope(): ?string |
||
184 | 794 | { |
|
185 | return $this->normalizeClass($this->scope); |
||
186 | } |
||
187 | 794 | ||
188 | 784 | /** |
|
189 | * @param class-string<RepositoryInterface<TEntity>>|null $repository |
||
0 ignored issues
–
show
|
|||
190 | 2 | */ |
|
191 | public function setRepository(?string $repository): self |
||
192 | 2 | { |
|
193 | $this->repository = $repository; |
||
194 | 2 | ||
195 | return $this; |
||
196 | } |
||
197 | 790 | ||
198 | /** |
||
199 | 790 | * @return class-string<RepositoryInterface<TEntity>>|null |
|
0 ignored issues
–
show
|
|||
200 | */ |
||
201 | public function getRepository(): ?string |
||
202 | { |
||
203 | return $this->normalizeClass($this->repository); |
||
204 | } |
||
205 | 10 | ||
206 | /** |
||
207 | 10 | * @param class-string|class-string[]|non-empty-string|non-empty-string[]|null $typecast |
|
0 ignored issues
–
show
|
|||
208 | 2 | * |
|
209 | 2 | * @return $this |
|
210 | */ |
||
211 | public function setTypecast(array|string|null $typecast): self |
||
212 | { |
||
213 | 10 | $this->typecast = $typecast; |
|
214 | 10 | ||
215 | 10 | return $this; |
|
216 | } |
||
217 | |||
218 | 10 | /** |
|
219 | * @return class-string|class-string[]|non-empty-string|non-empty-string[]|null |
||
0 ignored issues
–
show
|
|||
220 | */ |
||
221 | public function getTypecast(): array|string|null |
||
222 | { |
||
223 | 794 | return $this->typecast; |
|
224 | } |
||
225 | 794 | ||
226 | 2 | public function getFields(): FieldMap |
|
227 | { |
||
228 | return $this->fields; |
||
229 | 792 | } |
|
230 | 792 | ||
231 | 790 | public function getRelations(): RelationMap |
|
232 | { |
||
233 | return $this->relations; |
||
234 | } |
||
235 | 36 | ||
236 | public function getForeignKeys(): ForeignKeyMap |
||
237 | { |
||
238 | return $this->foreignKeys; |
||
239 | } |
||
240 | |||
241 | public function addSchemaModifier(SchemaModifierInterface $modifier): self |
||
242 | { |
||
243 | 6 | $this->schemaModifiers[] = $modifier->withRole($this->role ?? throw new EntityException( |
|
244 | 'Entity must have a `role` to be able to add a modifier.', |
||
245 | 6 | )); |
|
246 | |||
247 | 6 | return $this; |
|
248 | 6 | } |
|
249 | 4 | ||
250 | /** |
||
251 | 4 | * @return \Traversable<array-key, SchemaModifierInterface> |
|
252 | */ |
||
253 | public function getSchemaModifiers(): \Traversable |
||
254 | { |
||
255 | yield from $this->schemaModifiers; |
||
256 | 1236 | } |
|
257 | |||
258 | 1236 | public function setSchema(array $schema): self |
|
259 | { |
||
260 | 1236 | $this->schema = $schema; |
|
261 | 1236 | ||
262 | 1184 | return $this; |
|
263 | } |
||
264 | |||
265 | public function getSchema(): array |
||
266 | 1236 | { |
|
267 | 1184 | return $this->schema; |
|
268 | } |
||
269 | |||
270 | /** |
||
271 | 94 | * Merge entity relations and fields. |
|
272 | 94 | */ |
|
273 | public function merge(self $entity): void |
||
274 | { |
||
275 | 2 | foreach ($entity->getRelations() as $name => $relation) { |
|
276 | if (!$this->relations->has($name)) { |
||
277 | $this->relations->set($name, $relation); |
||
278 | 92 | } |
|
279 | } |
||
280 | |||
281 | 796 | foreach ($entity->getFields() as $name => $field) { |
|
282 | if (!$this->fields->has($name)) { |
||
283 | 796 | $this->fields->set($name, $field); |
|
284 | 788 | } |
|
285 | } |
||
286 | |||
287 | 8 | foreach ($entity->getForeignKeys() as $foreignKey) { |
|
288 | if (!$this->foreignKeys->has($foreignKey)) { |
||
289 | $this->foreignKeys->set($foreignKey); |
||
290 | 18 | } |
|
291 | } |
||
292 | 18 | } |
|
293 | 18 | ||
294 | /** |
||
295 | 792 | * Check if entity has primary key |
|
296 | */ |
||
297 | 792 | public function hasPrimaryKey(): bool |
|
298 | { |
||
299 | if ($this->primaryFields->count() > 0) { |
||
300 | return true; |
||
301 | } |
||
302 | |||
303 | 782 | foreach ($this->getFields() as $field) { |
|
304 | if ($field->isPrimary()) { |
||
305 | 782 | return true; |
|
306 | } |
||
307 | } |
||
308 | |||
309 | return false; |
||
310 | } |
||
311 | 4 | ||
312 | /** |
||
313 | 4 | * Set primary key using column list |
|
314 | 4 | * |
|
315 | * @param string[] $columns |
||
316 | 2 | */ |
|
317 | public function setPrimaryColumns(array $columns): void |
||
318 | 2 | { |
|
319 | $this->primaryFields = new FieldMap(); |
||
320 | |||
321 | 2 | foreach ($columns as $column) { |
|
322 | $name = $this->fields->getKeyByColumnName($column); |
||
323 | 2 | $this->primaryFields->set($name, $this->fields->get($name)); |
|
324 | 2 | } |
|
325 | } |
||
326 | 2 | ||
327 | /** |
||
328 | 2 | * Get entity primary key property names |
|
329 | */ |
||
330 | public function getPrimaryFields(): FieldMap |
||
331 | 2 | { |
|
332 | $map = new FieldMap(); |
||
333 | 2 | ||
334 | 2 | foreach ($this->getFields() as $name => $field) { |
|
335 | if ($field->isPrimary()) { |
||
336 | $map->set($name, $field); |
||
337 | } |
||
338 | } |
||
339 | |||
340 | if ($this->primaryFields->count() === 0 xor $map->count() === 0) { |
||
341 | return $map->count() === 0 ? $this->primaryFields : $map; |
||
342 | } |
||
343 | |||
344 | if ( |
||
345 | $this->primaryFields->count() !== $map->count() |
||
346 | || array_diff($map->getColumnNames(), $this->primaryFields->getColumnNames()) !== [] |
||
347 | ) { |
||
348 | // todo make friendly exception |
||
349 | throw new EntityException("Ambiguous primary key definition for `{$this->getRole()}`."); |
||
350 | } |
||
351 | |||
352 | return $this->primaryFields; |
||
353 | } |
||
354 | |||
355 | public function setInheritance(Inheritance $inheritance): void |
||
356 | { |
||
357 | $this->inheritance = $inheritance; |
||
358 | } |
||
359 | |||
360 | public function getInheritance(): ?Inheritance |
||
361 | { |
||
362 | return $this->inheritance; |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * Check if entity is a child of STI |
||
367 | */ |
||
368 | public function isChildOfSingleTableInheritance(): bool |
||
369 | { |
||
370 | return $this->stiParent !== null; |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * @param class-string|null $parentClass |
||
0 ignored issues
–
show
|
|||
375 | */ |
||
376 | public function markAsChildOfSingleTableInheritance(?string $parentClass): void |
||
377 | { |
||
378 | $this->stiParent = $parentClass; |
||
379 | } |
||
380 | |||
381 | public function getDatabase(): ?string |
||
382 | { |
||
383 | return $this->database; |
||
384 | } |
||
385 | |||
386 | /** |
||
387 | * @param non-empty-string|null $database |
||
0 ignored issues
–
show
|
|||
388 | */ |
||
389 | public function setDatabase(?string $database): void |
||
390 | { |
||
391 | $this->database = $database; |
||
392 | } |
||
393 | |||
394 | public function getTableName(): ?string |
||
395 | { |
||
396 | return $this->tableName; |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * @param non-empty-string $tableName |
||
0 ignored issues
–
show
|
|||
401 | */ |
||
402 | public function setTableName(string $tableName): void |
||
403 | { |
||
404 | $this->tableName = $tableName; |
||
405 | } |
||
406 | |||
407 | /** |
||
408 | * Full entity copy. |
||
409 | */ |
||
410 | public function __clone() |
||
411 | { |
||
412 | $this->options = clone $this->options; |
||
413 | $this->fields = clone $this->fields; |
||
414 | $this->primaryFields = clone $this->primaryFields; |
||
415 | $this->relations = clone $this->relations; |
||
416 | $this->foreignKeys = clone $this->foreignKeys; |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * @template T of object |
||
421 | * |
||
422 | * @param class-string<T>|null $class |
||
0 ignored issues
–
show
|
|||
423 | * |
||
424 | * @return ($class is class-string<T> ? class-string<T> : null) |
||
0 ignored issues
–
show
|
|||
425 | */ |
||
426 | private function normalizeClass(?string $class = null): ?string |
||
427 | { |
||
428 | if ($class === null) { |
||
429 | return null; |
||
430 | } |
||
431 | |||
432 | /** @var class-string<T> $class */ |
||
433 | $class = \ltrim($class, '\\'); |
||
434 | |||
435 | return $class; |
||
436 | } |
||
437 | } |
||
438 |