1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Cycle\Annotated; |
||
6 | |||
7 | use Cycle\Annotated\Annotation\Inheritance; |
||
8 | use Cycle\Annotated\Exception\AnnotationException; |
||
9 | use Cycle\Annotated\Utils\EntityUtils; |
||
10 | use Cycle\Schema\Definition\Entity as EntitySchema; |
||
11 | use Cycle\Schema\Definition\Inheritance\JoinedTable as JoinedTableInheritanceSchema; |
||
12 | use Cycle\Schema\Definition\Inheritance\SingleTable as SingleTableInheritanceSchema; |
||
13 | use Cycle\Schema\Definition\Map\FieldMap; |
||
14 | use Cycle\Schema\Exception\TableInheritance\WrongParentKeyColumnException; |
||
15 | use Cycle\Schema\GeneratorInterface; |
||
16 | use Cycle\Schema\Registry; |
||
17 | use Doctrine\Common\Annotations\Reader as DoctrineReader; |
||
18 | use Spiral\Attributes\ReaderInterface; |
||
19 | |||
20 | class TableInheritance implements GeneratorInterface |
||
21 | { |
||
22 | private ReaderInterface $reader; |
||
23 | private EntityUtils $utils; |
||
24 | |||
25 | 144 | public function __construct( |
|
26 | DoctrineReader|ReaderInterface $reader = null |
||
27 | ) { |
||
28 | 144 | $this->reader = ReaderFactory::create($reader); |
|
29 | 144 | $this->utils = new EntityUtils($this->reader); |
|
30 | 144 | } |
|
31 | |||
32 | 144 | public function run(Registry $registry): Registry |
|
33 | { |
||
34 | /** @var EntitySchema[] $found */ |
||
35 | 144 | $found = []; |
|
36 | |||
37 | 144 | foreach ($registry as $entity) { |
|
38 | // Only child entities can have table inheritance annotation |
||
39 | 144 | $children = $registry->getChildren($entity); |
|
40 | |||
41 | 144 | foreach ($children as $child) { |
|
42 | /** @var Inheritance $annotation */ |
||
43 | 144 | if ($annotation = $this->parseMetadata($child, Inheritance::class)) { |
|
44 | 144 | $childClass = $child->getClass(); |
|
45 | |||
46 | // Child entities always have parent entity |
||
47 | do { |
||
48 | 144 | $parent = $this->findParent( |
|
49 | $registry, |
||
50 | 144 | $this->utils->findParent($childClass, false) |
|
51 | ); |
||
52 | |||
53 | 144 | if ($annotation instanceof Inheritance\JoinedTable) { |
|
54 | 144 | break; |
|
55 | } |
||
56 | |||
57 | 144 | $childClass = $parent->getClass(); |
|
58 | 144 | } while ($this->parseMetadata($parent, Inheritance::class) !== null); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
59 | |||
60 | 144 | if ($inheritanceEntity = $this->initInheritance($annotation, $child, $parent)) { |
|
61 | 144 | $found[] = $inheritanceEntity; |
|
62 | } |
||
63 | } |
||
64 | |||
65 | // All child should be presented in a schema as separated entity |
||
66 | // Every child will be handled according its table inheritance type |
||
67 | 144 | if (!$registry->hasEntity($child->getRole())) { |
|
68 | 144 | $registry->register($child); |
|
69 | |||
70 | 144 | $registry->linkTable( |
|
71 | $child, |
||
72 | 144 | $this->getDatabase($child, $registry), |
|
73 | 144 | $this->getTableName($child, $registry) |
|
74 | ); |
||
75 | } |
||
76 | } |
||
77 | } |
||
78 | |||
79 | 144 | foreach ($found as $entity) { |
|
80 | 144 | if ($entity->getInheritance() instanceof SingleTableInheritanceSchema) { |
|
81 | 144 | $allowedEntities = \array_map( |
|
82 | 144 | static fn (string $role) => $registry->getEntity($role)->getClass(), |
|
83 | 144 | $entity->getInheritance()->getChildren() |
|
84 | ); |
||
85 | 144 | $this->removeStiExtraFields($entity, $allowedEntities); |
|
86 | 144 | } elseif ($entity->getInheritance() instanceof JoinedTableInheritanceSchema) { |
|
87 | 144 | $this->removeJtiExtraFields($entity); |
|
88 | 144 | $this->addForeignKey($this->parseMetadata($entity, Inheritance::class), $entity, $registry); |
|
89 | } |
||
90 | } |
||
91 | |||
92 | 144 | return $registry; |
|
93 | } |
||
94 | |||
95 | 144 | private function findParent(Registry $registry, string $role): ?EntitySchema |
|
96 | { |
||
97 | 144 | foreach ($registry as $entity) { |
|
98 | 144 | if ($entity->getRole() === $role || $entity->getClass() === $role) { |
|
99 | 144 | return $entity; |
|
100 | } |
||
101 | |||
102 | 144 | $children = $registry->getChildren($entity); |
|
103 | 144 | foreach ($children as $child) { |
|
104 | 144 | if ($child->getRole() === $role || $child->getClass() === $role) { |
|
105 | 144 | return $child; |
|
106 | } |
||
107 | } |
||
108 | } |
||
109 | |||
110 | return null; |
||
111 | } |
||
112 | |||
113 | 144 | private function initInheritance( |
|
114 | Inheritance $inheritance, |
||
115 | EntitySchema $entity, |
||
116 | EntitySchema $parent |
||
117 | ): ?EntitySchema { |
||
118 | 144 | if ($inheritance instanceof Inheritance\SingleTable) { |
|
119 | 144 | if (!$parent->getInheritance() instanceof SingleTableInheritanceSchema) { |
|
120 | 144 | $parent->setInheritance(new SingleTableInheritanceSchema()); |
|
121 | } |
||
122 | |||
123 | 144 | $parent->getInheritance()->addChild( |
|
124 | 144 | $inheritance->getValue() ?? $entity->getRole(), |
|
125 | 144 | $entity->getClass() |
|
126 | ); |
||
127 | |||
128 | 144 | $entity->markAsChildOfSingleTableInheritance($parent->getClass()); |
|
129 | |||
130 | // Root STI may have a discriminator annotation |
||
131 | /** @var Inheritance\DiscriminatorColumn $annotation */ |
||
132 | 144 | if ($annotation = $this->parseMetadata($parent, Inheritance\DiscriminatorColumn::class)) { |
|
133 | 144 | $parent->getInheritance()->setDiscriminator($annotation->getName()); |
|
134 | } |
||
135 | |||
136 | 144 | $parent->merge($entity); |
|
137 | |||
138 | 144 | return $parent; |
|
139 | 144 | } |
|
140 | 144 | if ($inheritance instanceof Inheritance\JoinedTable) { |
|
141 | $entity->setInheritance( |
||
142 | 144 | new JoinedTableInheritanceSchema( |
|
143 | $parent, |
||
144 | $inheritance->getOuterKey() |
||
145 | ) |
||
146 | 144 | ); |
|
147 | |||
148 | return $entity; |
||
149 | } |
||
150 | |||
151 | // Custom table inheritance types developers can handle in their own generators |
||
152 | return null; |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * @template T |
||
157 | * |
||
158 | * @param class-string<T> $name |
||
159 | * |
||
160 | 144 | * @return T|null |
|
161 | */ |
||
162 | private function parseMetadata(EntitySchema $entity, string $name): ?object |
||
163 | 144 | { |
|
164 | try { |
||
165 | 144 | $class = $entity->getClass(); |
|
166 | assert($class !== null); |
||
167 | return $this->reader->firstClassMetadata(new \ReflectionClass($class), $name); |
||
168 | } catch (\Exception $e) { |
||
169 | throw new AnnotationException($e->getMessage(), $e->getCode(), $e); |
||
170 | } |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | 144 | * Removes parent entity fields from given entity except Primary key. |
|
175 | */ |
||
176 | 144 | private function removeJtiExtraFields(EntitySchema $entity): void |
|
177 | 144 | { |
|
178 | 144 | foreach ($entity->getFields() as $name => $field) { |
|
179 | if ($field->getEntityClass() === $entity->getClass()) { |
||
180 | continue; |
||
181 | 144 | } |
|
182 | 144 | ||
183 | if (!$field->isPrimary()) { |
||
184 | 144 | $entity->getFields()->remove($name); |
|
185 | 144 | } else { |
|
186 | if ($field->getType() === 'primary') { |
||
187 | $field->setType('integer')->setPrimary(true); |
||
188 | } elseif ($field->getType() === 'bigPrimary') { |
||
189 | $field->setType('bigInteger')->setPrimary(true); |
||
190 | } |
||
191 | 144 | } |
|
192 | } |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | 144 | * Removes non STI child entity fields from given entity. |
|
197 | */ |
||
198 | 144 | private function removeStiExtraFields(EntitySchema $entity, array $allowedEntities): void |
|
199 | { |
||
200 | 144 | $allowedEntities[] = $entity->getClass(); |
|
201 | 144 | ||
202 | 144 | foreach ($entity->getFields() as $name => $field) { |
|
203 | if (\in_array($field->getEntityClass(), $allowedEntities, true)) { |
||
204 | continue; |
||
205 | 144 | } |
|
206 | |||
207 | 144 | $entity->getFields()->remove($name); |
|
208 | } |
||
209 | 144 | } |
|
210 | |||
211 | private function addForeignKey( |
||
212 | Inheritance\JoinedTable $annotation, |
||
213 | EntitySchema $entity, |
||
214 | 144 | Registry $registry |
|
215 | 144 | ): void { |
|
216 | if (!$annotation->isCreateFk()) { |
||
217 | return; |
||
218 | 144 | } |
|
219 | 144 | ||
220 | $parent = $this->getParentForForeignKey($entity, $registry); |
||
221 | 144 | $outerFields = $this->getOuterFields($entity, $parent, $annotation); |
|
222 | 144 | ||
223 | 144 | foreach ($outerFields->getColumnNames() as $column) { |
|
224 | if (!$registry->getTableSchema($parent)->hasColumn($column)) { |
||
225 | return; |
||
226 | } |
||
227 | 120 | } |
|
228 | 120 | ||
229 | 24 | foreach ($entity->getPrimaryFields()->getColumnNames() as $column) { |
|
230 | if (!$registry->getTableSchema($entity)->hasColumn($column)) { |
||
231 | return; |
||
232 | } |
||
233 | 96 | } |
|
234 | |||
235 | 96 | $registry->getTableSchema($parent)->index($outerFields->getColumnNames())->unique(); |
|
236 | 96 | ||
237 | 96 | $registry->getTableSchema($entity) |
|
238 | 96 | ->foreignKey($entity->getPrimaryFields()->getColumnNames()) |
|
239 | 96 | ->references($registry->getTable($parent), $outerFields->getColumnNames()) |
|
240 | 96 | ->onUpdate($annotation->getFkAction()) |
|
241 | ->onDelete($annotation->getFkAction()); |
||
242 | 144 | } |
|
243 | |||
244 | 144 | private function getParentForForeignKey(EntitySchema $schema, Registry $registry): EntitySchema |
|
245 | { |
||
246 | 144 | $parentSchema = $schema->getInheritance(); |
|
247 | |||
248 | 144 | if ($parentSchema instanceof JoinedTableInheritanceSchema) { |
|
249 | 144 | // entity is STI child |
|
250 | 144 | $parent = $parentSchema->getParent(); |
|
251 | 144 | if ($parent->isChildOfSingleTableInheritance()) { |
|
252 | 144 | return $this->getParentForForeignKey($this->findParent( |
|
253 | 144 | $registry, |
|
254 | $this->utils->findParent($parent->getClass(), false) |
||
255 | ), $registry); |
||
256 | 144 | } |
|
257 | |||
258 | return $parent; |
||
259 | 144 | } |
|
260 | |||
261 | return $schema; |
||
262 | 144 | } |
|
263 | |||
264 | private function getOuterFields( |
||
265 | EntitySchema $entity, |
||
266 | EntitySchema $parent, |
||
267 | 144 | Inheritance\JoinedTable $annotation |
|
268 | ): FieldMap { |
||
269 | 144 | $outerKey = $annotation->getOuterKey(); |
|
270 | 144 | ||
271 | if ($outerKey) { |
||
272 | if (!$parent->getFields()->has($outerKey)) { |
||
273 | throw new WrongParentKeyColumnException($entity, $outerKey); |
||
274 | 144 | } |
|
275 | |||
276 | return (new FieldMap())->set($outerKey, $parent->getFields()->get($outerKey)); |
||
277 | 144 | } |
|
278 | |||
279 | return $parent->getPrimaryFields(); |
||
280 | } |
||
281 | |||
282 | private function getTableName(EntitySchema $child, Registry $registry): string |
||
283 | { |
||
284 | $parent = $this->findParent($registry, $this->utils->findParent($child->getClass(), false)); |
||
285 | |||
286 | $inheritance = $parent->getInheritance(); |
||
287 | if (!$inheritance instanceof SingleTableInheritanceSchema) { |
||
288 | return $child->getTableName(); |
||
289 | } |
||
290 | $entities = \array_map( |
||
291 | static fn (string $role) => $registry->getEntity($role)->getClass(), |
||
292 | $inheritance->getChildren() |
||
293 | ); |
||
294 | |||
295 | return \in_array($child->getClass(), $entities, true) ? $parent->getTableName() : $child->getTableName(); |
||
296 | } |
||
297 | |||
298 | private function getDatabase(EntitySchema $child, Registry $registry): ?string |
||
299 | { |
||
300 | $parent = $this->findParent($registry, $this->utils->findParent($child->getClass(), false)); |
||
301 | |||
302 | $inheritance = $parent->getInheritance(); |
||
303 | if (!$inheritance instanceof SingleTableInheritanceSchema) { |
||
304 | return $child->getDatabase(); |
||
305 | } |
||
306 | $entities = \array_map( |
||
307 | static fn (string $role) => $registry->getEntity($role)->getClass(), |
||
308 | $inheritance->getChildren() |
||
309 | ); |
||
310 | |||
311 | return \in_array($child->getClass(), $entities, true) ? $parent->getDatabase() : $child->getDatabase(); |
||
312 | } |
||
313 | } |
||
314 |