1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Cycle\Annotated; |
||
6 | |||
7 | use Cycle\Annotated\Annotation\Entity; |
||
8 | use Cycle\Annotated\Exception\AnnotationException; |
||
9 | use Cycle\Annotated\Utils\EntityUtils; |
||
10 | use Cycle\Schema\Definition\Entity as EntitySchema; |
||
11 | use Cycle\Schema\Exception\RegistryException; |
||
12 | use Cycle\Schema\Exception\RelationException; |
||
13 | use Cycle\Schema\GeneratorInterface; |
||
14 | use Cycle\Schema\Registry; |
||
15 | use Doctrine\Common\Annotations\Reader as DoctrineReader; |
||
16 | use Spiral\Attributes\ReaderInterface; |
||
17 | use Spiral\Tokenizer\ClassesInterface; |
||
18 | |||
19 | /** |
||
20 | * Generates ORM schema based on annotated classes. |
||
21 | */ |
||
22 | final class Entities implements GeneratorInterface |
||
23 | { |
||
24 | // table name generation |
||
25 | public const TABLE_NAMING_PLURAL = 1; |
||
26 | public const TABLE_NAMING_SINGULAR = 2; |
||
27 | public const TABLE_NAMING_NONE = 3; |
||
28 | |||
29 | private ReaderInterface $reader; |
||
30 | private Configurator $generator; |
||
31 | private EntityUtils $utils; |
||
32 | |||
33 | 1056 | public function __construct( |
|
34 | private ClassesInterface $locator, |
||
35 | DoctrineReader|ReaderInterface $reader = null, |
||
36 | int $tableNamingStrategy = self::TABLE_NAMING_PLURAL, |
||
37 | ) { |
||
38 | 1056 | $this->reader = ReaderFactory::create($reader); |
|
39 | 1056 | $this->utils = new EntityUtils($this->reader); |
|
40 | 1056 | $this->generator = new Configurator($this->reader, $tableNamingStrategy); |
|
41 | 1056 | } |
|
42 | |||
43 | 1056 | public function run(Registry $registry): Registry |
|
44 | { |
||
45 | /** @var EntitySchema[] $children */ |
||
46 | 1056 | $children = []; |
|
47 | 1056 | foreach ($this->locator->getClasses() as $class) { |
|
48 | try { |
||
49 | /** @var Entity $ann */ |
||
50 | 1056 | $ann = $this->reader->firstClassMetadata($class, Entity::class); |
|
51 | } catch (\Exception $e) { |
||
52 | throw new AnnotationException($e->getMessage(), $e->getCode(), $e); |
||
53 | } |
||
54 | |||
55 | 1056 | if ($ann === null) { |
|
56 | 736 | continue; |
|
57 | } |
||
58 | |||
59 | 1056 | $e = $this->generator->initEntity($ann, $class); |
|
60 | |||
61 | // columns |
||
62 | 1056 | $this->generator->initFields($e, $class); |
|
63 | |||
64 | // relations |
||
65 | 1032 | $this->generator->initRelations($e, $class); |
|
66 | |||
67 | // schema modifiers |
||
68 | 1032 | $this->generator->initModifiers($e, $class); |
|
69 | |||
70 | // foreign keys |
||
71 | 1032 | $this->generator->initForeignKeys($ann, $e, $class); |
|
72 | |||
73 | 1032 | // additional columns (mapped to local fields automatically) |
|
74 | 688 | $this->generator->initColumns($e, $ann->getColumns(), $class); |
|
75 | 688 | ||
76 | if ($this->utils->hasParent($e->getClass())) { |
||
77 | foreach ($this->utils->findParents($e->getClass()) as $parent) { |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
78 | // additional columns from parent class |
||
79 | 1032 | $ann = $this->reader->firstClassMetadata($parent, Entity::class); |
|
80 | 1032 | $this->generator->initColumns($e, $ann->getColumns(), $parent); |
|
81 | } |
||
82 | |||
83 | 1032 | $children[] = $e; |
|
84 | 688 | continue; |
|
85 | } |
||
86 | |||
87 | 1032 | // generated fields |
|
88 | $this->generator->initGeneratedFields($e, $class); |
||
89 | |||
90 | 1032 | // register entity (OR find parent) |
|
91 | $registry->register($e); |
||
92 | $registry->linkTable($e, $e->getDatabase(), $e->getTableName()); |
||
93 | 1032 | } |
|
94 | 1032 | ||
95 | 712 | foreach ($children as $e) { |
|
96 | $registry->registerChildWithoutMerge($registry->getEntity($this->utils->findParent($e->getClass())), $e); |
||
97 | } |
||
98 | 1032 | ||
99 | return $this->normalizeNames($registry); |
||
100 | } |
||
101 | 1032 | ||
102 | private function normalizeNames(Registry $registry): Registry |
||
103 | 808 | { |
|
104 | foreach ($this->locator->getClasses() as $class) { |
||
105 | 784 | if (!$registry->hasEntity($class->getName())) { |
|
106 | 568 | continue; |
|
107 | 568 | } |
|
108 | 496 | ||
109 | 496 | $e = $registry->getEntity($class->getName()); |
|
110 | 496 | ||
111 | // resolve all the relation target names into roles |
||
112 | foreach ($e->getRelations() as $name => $r) { |
||
113 | try { |
||
114 | $r->setTarget($this->resolveTarget($registry, $r->getTarget())); |
||
115 | 784 | ||
116 | 568 | if ($r->getOptions()->has('though')) { |
|
117 | 568 | $though = $r->getOptions()->get('though'); |
|
118 | 568 | if ($though !== null) { |
|
119 | 568 | $r->getOptions()->set( |
|
120 | 568 | 'though', |
|
121 | $this->resolveTarget($registry, $though) |
||
122 | ); |
||
123 | } |
||
124 | } |
||
125 | 784 | ||
126 | 568 | if ($r->getOptions()->has('through')) { |
|
127 | 568 | $through = $r->getOptions()->get('through'); |
|
128 | if ($through !== null) { |
||
129 | $r->getOptions()->set( |
||
130 | 'through', |
||
131 | 784 | $this->resolveTarget($registry, $through) |
|
132 | 568 | ); |
|
133 | 784 | } |
|
134 | } |
||
135 | |||
136 | 24 | if ($r->getOptions()->has('throughInnerKey')) { |
|
137 | 24 | if ($throughInnerKey = (array)$r->getOptions()->get('throughInnerKey')) { |
|
138 | 24 | $r->getOptions()->set('throughInnerKey', $throughInnerKey); |
|
139 | 24 | } |
|
140 | 24 | } |
|
141 | |||
142 | if ($r->getOptions()->has('throughOuterKey')) { |
||
143 | 24 | if ($throughOuterKey = (array)$r->getOptions()->get('throughOuterKey')) { |
|
144 | $r->getOptions()->set('throughOuterKey', $throughOuterKey); |
||
145 | } |
||
146 | } |
||
147 | } catch (RegistryException $ex) { |
||
148 | throw new RelationException( |
||
149 | sprintf( |
||
150 | 1008 | 'Unable to resolve `%s`.`%s` relation target (not found or invalid)', |
|
151 | $e->getRole(), |
||
152 | $name |
||
153 | 808 | ), |
|
154 | $ex->getCode(), |
||
155 | 808 | $ex |
|
156 | ); |
||
157 | 592 | } |
|
158 | } |
||
159 | |||
160 | 808 | // resolve foreign key target and column names |
|
161 | foreach ($e->getForeignKeys() as $foreignKey) { |
||
162 | 72 | $target = $this->resolveTarget($registry, $foreignKey->getTarget()); |
|
163 | 72 | \assert(!empty($target), 'Unable to resolve foreign key target entity.'); |
|
164 | 48 | $targetEntity = $registry->getEntity($target); |
|
165 | 48 | ||
166 | $foreignKey->setTarget($target); |
||
167 | $foreignKey->setInnerColumns($this->getColumnNames($e, $foreignKey->getInnerColumns())); |
||
168 | |||
169 | $foreignKey->setOuterColumns(empty($foreignKey->getOuterColumns()) |
||
170 | ? $targetEntity->getPrimaryFields()->getColumnNames() |
||
171 | 784 | : $this->getColumnNames($targetEntity, $foreignKey->getOuterColumns())); |
|
172 | } |
||
173 | } |
||
174 | |||
175 | return $registry; |
||
176 | } |
||
177 | |||
178 | private function resolveTarget(Registry $registry, string $name): ?string |
||
179 | { |
||
180 | if (\interface_exists($name, true)) { |
||
181 | // do not resolve interfaces |
||
182 | return $name; |
||
183 | } |
||
184 | |||
185 | if (!$registry->hasEntity($name)) { |
||
186 | // point all relations to the parent |
||
187 | foreach ($registry as $entity) { |
||
188 | foreach ($registry->getChildren($entity) as $child) { |
||
189 | if ($child->getClass() === $name || $child->getRole() === $name) { |
||
190 | return $entity->getRole(); |
||
191 | } |
||
192 | } |
||
193 | } |
||
194 | } |
||
195 | |||
196 | return $registry->getEntity($name)->getRole(); |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * @param array<non-empty-string> $columns |
||
0 ignored issues
–
show
|
|||
201 | * |
||
202 | * @throws AnnotationException |
||
203 | * |
||
204 | * @return array<non-empty-string> |
||
0 ignored issues
–
show
|
|||
205 | */ |
||
206 | private function getColumnNames(EntitySchema $entity, array $columns): array |
||
207 | { |
||
208 | $names = []; |
||
209 | foreach ($columns as $name) { |
||
210 | $names[] = match (true) { |
||
211 | $entity->getFields()->has($name) => $entity->getFields()->get($name)->getColumn(), |
||
212 | $entity->getFields()->hasColumn($name) => $name, |
||
213 | default => throw new AnnotationException('Unable to resolve column name.'), |
||
214 | }; |
||
215 | } |
||
216 | |||
217 | return $names; |
||
218 | } |
||
219 | } |
||
220 |