1 | <?php |
||
2 | |||
3 | namespace Bdf\Prime\Relations; |
||
4 | |||
5 | use Bdf\Prime\Collection\Indexer\EntityIndexerInterface; |
||
6 | use Bdf\Prime\Exception\PrimeException; |
||
7 | use Bdf\Prime\Query\Contract\ReadOperation; |
||
8 | use Bdf\Prime\Relations\Util\ForeignKeyRelation; |
||
9 | use Bdf\Prime\Repository\RepositoryInterface; |
||
10 | use RuntimeException; |
||
11 | |||
12 | /** |
||
13 | * Relation |
||
14 | * |
||
15 | * @todo Mettre le saveStrategy en trait + supprimer ou deprécier cette classe |
||
16 | * |
||
17 | * @template L as object |
||
18 | * @template R as object |
||
19 | * |
||
20 | * @extends AbstractRelation<L, R> |
||
21 | */ |
||
22 | abstract class Relation extends AbstractRelation |
||
23 | { |
||
24 | /** @use Polymorph<L> */ |
||
25 | use Polymorph; |
||
26 | /** @use ForeignKeyRelation<L, R> */ |
||
27 | use ForeignKeyRelation; |
||
28 | |||
29 | // save strategies |
||
30 | public const SAVE_STRATEGY_REPLACE = 1; |
||
31 | public const SAVE_STRATEGY_ADD = 2; |
||
32 | |||
33 | /** |
||
34 | * The save cascade strategy. See constance SAVE_STRATEGY_* |
||
35 | * |
||
36 | * @var int |
||
37 | */ |
||
38 | protected $saveStrategy; |
||
39 | |||
40 | |||
41 | /** |
||
42 | * Set the relation info |
||
43 | * |
||
44 | * @param string $attributeAim The property name that hold the relation |
||
45 | * @param RepositoryInterface<L> $local |
||
46 | * @param string $localKey |
||
47 | * @param RepositoryInterface<R>|null $distant |
||
48 | * @param string|null $distantKey |
||
49 | */ |
||
50 | 258 | public function __construct(string $attributeAim, RepositoryInterface $local, string $localKey, ?RepositoryInterface $distant = null, ?string $distantKey = null) |
|
51 | { |
||
52 | 258 | parent::__construct($attributeAim, $local, $distant); |
|
53 | |||
54 | 258 | $this->localKey = $localKey; |
|
55 | 258 | $this->distantKey = $distantKey; |
|
56 | } |
||
57 | |||
58 | // |
||
59 | //----------- options |
||
60 | // |
||
61 | |||
62 | /** |
||
63 | * {@inheritdoc} |
||
64 | */ |
||
65 | 259 | public function setOptions(array $options) |
|
66 | { |
||
67 | 259 | parent::setOptions($options); |
|
68 | |||
69 | 259 | if (!empty($options['saveStrategy'])) { |
|
70 | 1 | $this->setSaveStrategy($options['saveStrategy']); |
|
71 | } |
||
72 | |||
73 | 259 | if (isset($options['discriminator'])) { |
|
74 | 100 | $this->setDiscriminator($options['discriminator']); |
|
75 | } |
||
76 | |||
77 | 259 | if (isset($options['discriminatorValue'])) { |
|
78 | 16 | $this->setDiscriminatorValue($options['discriminatorValue']); |
|
79 | } |
||
80 | |||
81 | 259 | return $this; |
|
82 | } |
||
83 | |||
84 | /** |
||
85 | * {@inheritdoc} |
||
86 | */ |
||
87 | 1 | public function getOptions(): array |
|
88 | { |
||
89 | 1 | return parent::getOptions() + [ |
|
90 | 1 | 'saveStrategy' => $this->saveStrategy, |
|
91 | 1 | 'discriminator' => $this->discriminator, |
|
92 | 1 | 'discriminatorValue' => $this->discriminatorValue, |
|
93 | 1 | ]; |
|
94 | } |
||
95 | |||
96 | /** |
||
97 | * Set the save strategy |
||
98 | * |
||
99 | * @param int $strategy |
||
100 | * |
||
101 | * @return $this |
||
102 | */ |
||
103 | 2 | public function setSaveStrategy(int $strategy) |
|
104 | { |
||
105 | 2 | $this->saveStrategy = $strategy; |
|
106 | |||
107 | 2 | return $this; |
|
108 | } |
||
109 | |||
110 | /** |
||
111 | * Get the save strategy |
||
112 | * |
||
113 | * @return int |
||
114 | */ |
||
115 | 1 | public function getSaveStrategy(): int |
|
116 | { |
||
117 | 1 | return $this->saveStrategy; |
|
118 | } |
||
119 | |||
120 | // |
||
121 | //------------ methods for loading relation |
||
122 | // |
||
123 | |||
124 | /** |
||
125 | * {@inheritdoc} |
||
126 | */ |
||
127 | #[ReadOperation] |
||
128 | 233 | public function load(EntityIndexerInterface $collection, array $with = [], $constraints = [], array $without = []): void |
|
129 | { |
||
130 | 233 | if ($collection->empty()) { |
|
131 | 26 | return; |
|
132 | } |
||
133 | |||
134 | 233 | $indexed = $collection->by($this->localKey); |
|
135 | |||
136 | 233 | $this->match($indexed, $this->relations(array_keys($indexed), $with, $constraints, $without)); |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * Get the entities |
||
141 | * |
||
142 | * @param array $keys |
||
143 | * @param array $with |
||
144 | * @param array $constraints |
||
145 | * @param array $without |
||
146 | * |
||
147 | * @return array Entities |
||
148 | * @throws PrimeException |
||
149 | */ |
||
150 | #[ReadOperation] |
||
151 | abstract protected function relations($keys, $with, $constraints, $without): array; |
||
152 | |||
153 | /** |
||
154 | * Set the relation in a collection of entities |
||
155 | * |
||
156 | * @param L[][] $collection |
||
157 | * @param array $relations |
||
158 | */ |
||
159 | abstract protected function match($collection, $relations): void; |
||
160 | |||
161 | /** |
||
162 | * Get defined relation |
||
163 | * |
||
164 | * Build object relation defined by user |
||
165 | * |
||
166 | * @param RepositoryInterface<T> $repository |
||
167 | * @param string $relationName |
||
168 | * @param array $relationMeta |
||
169 | * |
||
170 | * @return RelationInterface<T, object> |
||
171 | * |
||
172 | * @throws RuntimeException If relation type does not exist |
||
173 | * |
||
174 | * @template T as object |
||
175 | */ |
||
176 | 260 | public static function make(RepositoryInterface $repository, string $relationName, array $relationMeta): RelationInterface |
|
177 | { |
||
178 | 260 | switch ($relationMeta['type']) { |
|
179 | case RelationInterface::BELONGS_TO: |
||
180 | 126 | $relation = new BelongsTo( |
|
181 | 126 | $relationName, |
|
182 | 126 | $repository, |
|
183 | 126 | $relationMeta['localKey'], |
|
184 | 126 | $repository->repository($relationMeta['entity']), |
|
185 | 126 | $relationMeta['distantKey'] |
|
186 | 126 | ); |
|
187 | 126 | break; |
|
188 | |||
189 | case RelationInterface::HAS_ONE: |
||
190 | 38 | $relation = new HasOne( |
|
191 | 38 | $relationName, |
|
192 | 38 | $repository, |
|
193 | 38 | $relationMeta['localKey'], |
|
194 | 38 | $repository->repository($relationMeta['entity']), |
|
195 | 38 | $relationMeta['distantKey'] |
|
196 | 38 | ); |
|
197 | 38 | break; |
|
198 | |||
199 | case RelationInterface::HAS_MANY: |
||
200 | 73 | $relation = new HasMany( |
|
201 | 73 | $relationName, |
|
202 | 73 | $repository, |
|
203 | 73 | $relationMeta['localKey'], |
|
204 | 73 | $repository->repository($relationMeta['entity']), |
|
205 | 73 | $relationMeta['distantKey'] |
|
206 | 73 | ); |
|
207 | 73 | break; |
|
208 | |||
209 | case RelationInterface::BELONGS_TO_MANY: |
||
210 | 32 | $relation = new BelongsToMany( |
|
211 | 32 | $relationName, |
|
212 | 32 | $repository, |
|
213 | 32 | $relationMeta['localKey'], |
|
214 | 32 | $repository->repository($relationMeta['entity']), |
|
215 | 32 | $relationMeta['distantKey'] |
|
216 | 32 | ); |
|
217 | 32 | $relation->setThrough( |
|
218 | 32 | $repository->repository($relationMeta['through']), |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
219 | 32 | $relationMeta['throughLocal'], |
|
220 | 32 | $relationMeta['throughDistant'] |
|
221 | 32 | ); |
|
222 | 32 | break; |
|
223 | |||
224 | case RelationInterface::BY_INHERITANCE: |
||
225 | 31 | $relation = new ByInheritance( |
|
226 | 31 | $relationName, |
|
227 | 31 | $repository, |
|
228 | 31 | $relationMeta['localKey'] |
|
229 | 31 | ); |
|
230 | 31 | break; |
|
231 | |||
232 | case RelationInterface::MORPH_TO: |
||
233 | 84 | $relation = new MorphTo( |
|
234 | 84 | $relationName, |
|
235 | 84 | $repository, |
|
236 | 84 | $relationMeta['localKey'] |
|
237 | 84 | ); |
|
238 | 84 | $relation->setMap($relationMeta['map']); |
|
239 | 84 | break; |
|
240 | |||
241 | case RelationInterface::CUSTOM: |
||
242 | 2 | $relation = $relationMeta['relationClass']::make($repository, $relationName, $relationMeta); |
|
243 | 2 | break; |
|
244 | |||
245 | default: |
||
246 | throw new RuntimeException('Unknown type from relation "' . $relationName . '" in ' . $repository->entityName()); |
||
247 | } |
||
248 | |||
249 | 260 | return $relation->setOptions($relationMeta); |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * Create the array of relation |
||
254 | * |
||
255 | * [relation => constraints] becomes [relation => ['constraints' => constraints, 'relations' => subRelations]] |
||
256 | * |
||
257 | * ex |
||
258 | * <code> |
||
259 | * print_r(Relation::sanitizeRelations([ |
||
260 | * 'customer.packs' => ['enabled' => true], |
||
261 | * )); |
||
262 | * |
||
263 | * // echo an array like [ |
||
264 | * // 'customer' => [ |
||
265 | * // 'constraints' => [], |
||
266 | * // 'relations' => ['packs' => ['enabled' => true]], |
||
267 | * // ] |
||
268 | * // ] |
||
269 | * </code> |
||
270 | * |
||
271 | * @param array $relations |
||
272 | * |
||
273 | * @return array |
||
274 | * |
||
275 | * @todo voir pour intégrer en meta le polymorphism |
||
276 | */ |
||
277 | 610 | public static function sanitizeRelations(array $relations): array |
|
278 | { |
||
279 | 610 | $sanitized = []; |
|
280 | |||
281 | 610 | foreach ($relations as $name => $constraints) { |
|
282 | 265 | if (is_int($name)) { |
|
283 | 156 | $name = $constraints; |
|
284 | 156 | $constraints = []; |
|
285 | } |
||
286 | |||
287 | // relation deja declaré: on ajoute ecrase les constraints |
||
288 | // cas d'appel: ['foo.bar', 'foo' => constraints] |
||
289 | 265 | if (isset($sanitized[$name])) { |
|
290 | 1 | $sanitized[$name]['constraints'] = $constraints; |
|
291 | 1 | continue; |
|
292 | } |
||
293 | |||
294 | 265 | $relations = []; |
|
295 | |||
296 | 265 | list($name, $nested) = self::parseRelationName($name); |
|
297 | |||
298 | // nested relation |
||
299 | 265 | if ($nested) { |
|
300 | // la relation existe deja, on ajoute la nouvelle relation |
||
301 | // cas d'appel ['foo.bar1', 'foo.bar2' => constraints] |
||
302 | 66 | if (isset($sanitized[$name])) { |
|
303 | 17 | $sanitized[$name]['relations'][$nested] = $constraints; |
|
304 | 17 | continue; |
|
305 | } |
||
306 | |||
307 | 65 | $relations[$nested] = $constraints; |
|
308 | 65 | $constraints = []; |
|
309 | } |
||
310 | |||
311 | // declaration d'une relation à charger |
||
312 | 265 | $sanitized[$name] = [ |
|
313 | 265 | 'constraints' => $constraints, |
|
314 | 265 | 'relations' => $relations, |
|
315 | 265 | ]; |
|
316 | } |
||
317 | |||
318 | 610 | return $sanitized; |
|
319 | } |
||
320 | |||
321 | /** |
||
322 | * Create an array of relations and nested relations that must be discarded |
||
323 | * |
||
324 | * @param array $relations |
||
325 | * |
||
326 | * @return array |
||
327 | */ |
||
328 | 240 | public static function sanitizeWithoutRelations(array $relations): array |
|
329 | { |
||
330 | 240 | $sanitized = []; |
|
331 | |||
332 | 240 | foreach ($relations as $name) { |
|
333 | 45 | list($name, $nested) = self::parseRelationName($name); |
|
334 | |||
335 | 45 | if (!isset($sanitized[$name])) { |
|
336 | 45 | $sanitized[$name] = []; |
|
337 | } |
||
338 | |||
339 | 45 | if ($nested) { |
|
340 | 5 | $sanitized[$name][] = $nested; |
|
341 | } |
||
342 | } |
||
343 | |||
344 | 240 | return $sanitized; |
|
345 | } |
||
346 | |||
347 | /** |
||
348 | * Parse the relation name to find whether a nested relation is defined or not |
||
349 | * |
||
350 | * @param string $name |
||
351 | * |
||
352 | * @return array{0:string,1:string|null} |
||
353 | */ |
||
354 | 281 | public static function parseRelationName(string $name): array |
|
355 | { |
||
356 | 281 | if (strpos($name, '.') === false) { |
|
357 | 277 | return [$name, null]; |
|
358 | } |
||
359 | |||
360 | 69 | list($name, $relation) = explode('.', $name, 2); |
|
361 | |||
362 | // gestion du polymorph de relation |
||
363 | 69 | $part = explode('#', $name, 2); |
|
364 | 69 | if (count($part) === 2) { |
|
365 | 37 | $name = $part[0]; |
|
366 | 37 | $relation = $part[1].'#'.$relation; |
|
367 | } |
||
368 | |||
369 | 69 | return [$name, $relation]; |
|
370 | } |
||
371 | |||
372 | /** |
||
373 | * Get entity classname and property from a pattern |
||
374 | * |
||
375 | * @param string $pattern |
||
376 | * |
||
377 | * @return array{0:class-string,1:string} |
||
0 ignored issues
–
show
|
|||
378 | */ |
||
379 | 434 | public static function parseEntity(string $pattern): array |
|
380 | { |
||
381 | 434 | $parts = explode('::', $pattern, 2); |
|
382 | |||
383 | 434 | return [ |
|
384 | 434 | $parts[0], |
|
385 | 434 | isset($parts[1]) ? $parts[1] : 'id', |
|
386 | 434 | ]; |
|
387 | } |
||
388 | } |
||
389 |