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)) { |
|||
0 ignored issues
–
show
It seems like
$parent can also be of type null ; however, parameter $parent of Cycle\Annotated\TableInh...ance::initInheritance() does only seem to accept Cycle\Schema\Definition\Entity , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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); |
|||
0 ignored issues
–
show
It seems like
$this->parseMetadata($en...ion\Inheritance::class) can also be of type null ; however, parameter $annotation of Cycle\Annotated\TableInheritance::addForeignKey() does only seem to accept Cycle\Annotated\Annotation\Inheritance\JoinedTable , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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( |
|||
0 ignored issues
–
show
The method
addChild() does not exist on Cycle\Schema\Definition\Inheritance . It seems like you code against a sub-type of Cycle\Schema\Definition\Inheritance such as Cycle\Schema\Definition\Inheritance\SingleTable .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
124 | 144 | $inheritance->getValue() ?? $entity->getRole(), |
|||
0 ignored issues
–
show
It seems like
$inheritance->getValue() ?? $entity->getRole() can also be of type null ; however, parameter $discriminatorValue of Cycle\Schema\Definition\...SingleTable::addChild() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
125 | 144 | $entity->getClass() |
|||
0 ignored issues
–
show
It seems like
$entity->getClass() can also be of type null ; however, parameter $class of Cycle\Schema\Definition\...SingleTable::addChild() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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()); |
|||
0 ignored issues
–
show
The method
setDiscriminator() does not exist on Cycle\Schema\Definition\Inheritance . It seems like you code against a sub-type of Cycle\Schema\Definition\Inheritance such as Cycle\Schema\Definition\Inheritance\SingleTable .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 |
||||
0 ignored issues
–
show
|
|||||
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( |
|||
0 ignored issues
–
show
It seems like
$this->findParent($regis...nt->getClass(), false)) can also be of type null ; however, parameter $schema of Cycle\Annotated\TableInh...etParentForForeignKey() does only seem to accept Cycle\Schema\Definition\Entity , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
253 | 144 | $registry, |
|||
254 | $this->utils->findParent($parent->getClass(), false) |
||||
0 ignored issues
–
show
It seems like
$parent->getClass() can also be of type null ; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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)); |
||||
0 ignored issues
–
show
It seems like
$child->getClass() can also be of type null ; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
285 | |||||
286 | $inheritance = $parent->getInheritance(); |
||||
287 | if (!$inheritance instanceof SingleTableInheritanceSchema) { |
||||
288 | return $child->getTableName(); |
||||
0 ignored issues
–
show
|
|||||
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(); |
||||
0 ignored issues
–
show
|
|||||
296 | } |
||||
297 | |||||
298 | private function getDatabase(EntitySchema $child, Registry $registry): ?string |
||||
299 | { |
||||
300 | $parent = $this->findParent($registry, $this->utils->findParent($child->getClass(), false)); |
||||
0 ignored issues
–
show
It seems like
$child->getClass() can also be of type null ; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParent() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 |