1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Cycle\Schema\Relation; |
||
6 | |||
7 | use Cycle\Database\Schema\AbstractTable; |
||
8 | use Cycle\ORM\Relation; |
||
9 | use Cycle\ORM\SchemaInterface; |
||
10 | use Cycle\Schema\Definition\Entity; |
||
11 | use Cycle\Schema\Exception\RegistryException; |
||
12 | use Cycle\Schema\Registry; |
||
13 | use Cycle\Schema\RelationInterface; |
||
14 | |||
15 | /** |
||
16 | * Defines relation options, renders needed columns and other options. |
||
17 | */ |
||
18 | abstract class RelationSchema implements RelationInterface |
||
19 | { |
||
20 | // relation rendering options |
||
21 | public const INDEX_CREATE = 1001; |
||
22 | public const FK_CREATE = 1002; |
||
23 | public const FK_ACTION = 1003; |
||
24 | public const FK_ON_DELETE = 1004; |
||
25 | public const INVERSE = 1005; |
||
26 | public const MORPH_KEY_LENGTH = 1009; |
||
27 | public const EMBEDDED_PREFIX = 1010; |
||
28 | |||
29 | // options to be excluded from generated schema (helpers) |
||
30 | protected const EXCLUDE = [ |
||
31 | self::FK_CREATE, |
||
32 | self::FK_ACTION, |
||
33 | self::FK_ON_DELETE, |
||
34 | self::INDEX_CREATE, |
||
35 | self::EMBEDDED_PREFIX, |
||
36 | ]; |
||
37 | |||
38 | // exported relation type |
||
39 | protected const RELATION_TYPE = null; |
||
40 | |||
41 | // name of all required relation options |
||
42 | protected const RELATION_SCHEMA = []; |
||
43 | |||
44 | /** |
||
45 | * Relation container name in the entity |
||
46 | */ |
||
47 | protected string $name; |
||
48 | |||
49 | /** |
||
50 | * @var non-empty-string |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
51 | */ |
||
52 | protected string $source; |
||
53 | |||
54 | /** |
||
55 | * @var non-empty-string |
||
0 ignored issues
–
show
|
|||
56 | */ |
||
57 | protected string $target; |
||
58 | |||
59 | protected OptionSchema $options; |
||
60 | |||
61 | /** |
||
62 | 720 | * @param non-empty-string $role |
|
0 ignored issues
–
show
|
|||
63 | */ |
||
64 | 720 | public function withRole(string $role): static |
|
65 | 720 | { |
|
66 | $relation = clone $this; |
||
67 | $relation->source = $role; |
||
68 | return $relation; |
||
69 | } |
||
70 | 1168 | ||
71 | public function modifySchema(array &$schema): void |
||
72 | 1168 | { |
|
73 | 1168 | $schema[SchemaInterface::RELATIONS][$this->name] = $this->packSchema(); |
|
74 | 1168 | } |
|
75 | 1168 | ||
76 | /** |
||
77 | 1168 | * @param non-empty-string $source |
|
0 ignored issues
–
show
|
|||
78 | 1168 | * @param non-empty-string $target |
|
79 | 1168 | */ |
|
80 | 1168 | public function withContext(string $name, string $source, string $target, OptionSchema $options): RelationInterface |
|
81 | { |
||
82 | $relation = clone $this; |
||
83 | 1168 | $relation->source = $source; |
|
84 | $relation->target = $target; |
||
85 | $relation->name = $name; |
||
86 | 1048 | ||
87 | $relation->options = $options->withTemplate(static::RELATION_SCHEMA)->withContext([ |
||
88 | 1048 | 'relation' => $name, |
|
89 | 1048 | 'source:role' => $source, |
|
90 | 'target:role' => $target, |
||
91 | ]); |
||
92 | 1000 | ||
93 | 1000 | return $relation; |
|
94 | 1000 | } |
|
95 | |||
96 | public function compute(Registry $registry): void |
||
97 | 960 | { |
|
98 | $this->options = $this->options->withContext([ |
||
99 | 720 | 'source:primaryKey' => $this->getPrimaryColumns($registry->getEntity($this->source)), |
|
100 | ]); |
||
101 | 720 | ||
102 | if ($registry->hasEntity($this->target)) { |
||
103 | $this->options = $this->options->withContext([ |
||
104 | 'target:primaryKey' => $this->getPrimaryColumns($registry->getEntity($this->target)), |
||
105 | 720 | ]); |
|
106 | 720 | } |
|
107 | } |
||
108 | 48 | ||
109 | protected function getLoadMethod(): ?int |
||
110 | 688 | { |
|
111 | if (!$this->options->has(Relation::LOAD)) { |
||
112 | return null; |
||
113 | } |
||
114 | 952 | ||
115 | switch ($this->options->get(Relation::LOAD)) { |
||
116 | 952 | case 'eager': |
|
117 | case Relation::LOAD_EAGER: |
||
118 | return Relation::LOAD_EAGER; |
||
119 | default: |
||
120 | return Relation::LOAD_PROMISE; |
||
121 | } |
||
122 | 1160 | } |
|
123 | |||
124 | 1160 | protected function getOptions(): OptionSchema |
|
125 | { |
||
126 | 1160 | return $this->options; |
|
127 | 88 | } |
|
128 | |||
129 | /** |
||
130 | 1112 | * @throws RegistryException |
|
131 | */ |
||
132 | protected function getPrimaryColumns(Entity $entity): array |
||
133 | { |
||
134 | $columns = $entity->getPrimaryFields()->getNames(); |
||
135 | |||
136 | if ($columns === []) { |
||
137 | throw new RegistryException("Entity `{$entity->getRole()}` must have defined primary key"); |
||
138 | } |
||
139 | 24 | ||
140 | return $columns; |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * @param array<string> $columns |
||
145 | * @param bool $strictOrder True means that fields order in the {@see $columns} argument is matter |
||
146 | 24 | * @param bool $withSorting True means that fields will be compared taking into account the column values sorting |
|
147 | 24 | * @param bool|null $unique Unique index or not. Null means both |
|
148 | */ |
||
149 | 24 | protected function hasIndex( |
|
150 | AbstractTable $table, |
||
151 | 24 | array $columns, |
|
152 | 24 | bool $strictOrder = true, |
|
153 | 24 | bool $withSorting = true, |
|
154 | ?bool $unique = null, |
||
155 | 8 | ): bool { |
|
156 | if ($strictOrder && $withSorting && $unique === null) { |
||
157 | 8 | return $table->hasIndex($columns); |
|
158 | } |
||
159 | $indexes = $table->getIndexes(); |
||
160 | |||
161 | 8 | foreach ($indexes as $index) { |
|
162 | 8 | if ($unique !== null && $index->isUnique() !== $unique) { |
|
163 | continue; |
||
164 | } |
||
165 | 24 | $tableColumns = $withSorting ? $index->getColumnsWithSort() : $index->getColumns(); |
|
166 | |||
167 | if (count($columns) !== count($tableColumns)) { |
||
168 | 720 | continue; |
|
169 | } |
||
170 | 720 | ||
171 | if ($strictOrder ? $columns === $tableColumns : array_diff($columns, $tableColumns) === []) { |
||
172 | 720 | return true; |
|
173 | 720 | } |
|
174 | 720 | } |
|
175 | return false; |
||
176 | } |
||
177 | 720 | ||
178 | private function packSchema(): array |
||
179 | { |
||
180 | $schema = []; |
||
181 | 720 | ||
182 | foreach (static::RELATION_SCHEMA as $option => $template) { |
||
183 | if (in_array($option, static::EXCLUDE, true)) { |
||
184 | 720 | continue; |
|
185 | 720 | } |
|
186 | 720 | ||
187 | 720 | $schema[$option] = $this->options->get($option); |
|
188 | } |
||
189 | |||
190 | // load option is not required in schema |
||
191 | unset($schema[Relation::LOAD]); |
||
192 | |||
193 | return [ |
||
194 | Relation::TYPE => static::RELATION_TYPE, |
||
195 | Relation::TARGET => $this->target, |
||
196 | Relation::LOAD => $this->getLoadMethod(), |
||
197 | Relation::SCHEMA => $schema, |
||
198 | ]; |
||
199 | } |
||
200 | } |
||
201 |