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())) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
77 | foreach ($this->utils->findParents($e->getClass()) as $parent) { |
||||
0 ignored issues
–
show
It seems like
$e->getClass() can also be of type null ; however, parameter $class of Cycle\Annotated\Utils\EntityUtils::findParents() 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
![]() |
|||||
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()); |
||||
0 ignored issues
–
show
It seems like
$e->getTableName() can also be of type null ; however, parameter $table of Cycle\Schema\Registry::linkTable() 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
![]() |
|||||
93 | 1032 | } |
|||
94 | 1032 | ||||
95 | 712 | foreach ($children as $e) { |
|||
96 | $registry->registerChildWithoutMerge($registry->getEntity($this->utils->findParent($e->getClass())), $e); |
||||
0 ignored issues
–
show
It seems like
$this->utils->findParent($e->getClass()) can also be of type null ; however, parameter $role of Cycle\Schema\Registry::getEntity() 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
![]() |
|||||
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 |