1 | <?php |
||||||
2 | |||||||
3 | namespace Bdf\Prime\Relations; |
||||||
4 | |||||||
5 | use BadMethodCallException; |
||||||
6 | use Bdf\Prime\Collection\CollectionInterface; |
||||||
7 | use Bdf\Prime\Collection\Indexer\EntityIndexerInterface; |
||||||
8 | use Bdf\Prime\Collection\Indexer\EntitySetIndexer; |
||||||
9 | use Bdf\Prime\Locatorizable; |
||||||
10 | use Bdf\Prime\Query\Contract\Deletable; |
||||||
11 | use Bdf\Prime\Query\Contract\Whereable; |
||||||
12 | use Bdf\Prime\Query\QueryInterface; |
||||||
13 | use Bdf\Prime\Query\ReadCommandInterface; |
||||||
14 | use Bdf\Prime\Relations\Info\LocalHashTableRelationInfo; |
||||||
15 | use Bdf\Prime\Relations\Info\NullRelationInfo; |
||||||
16 | use Bdf\Prime\Relations\Info\RelationInfoInterface; |
||||||
17 | use Bdf\Prime\Repository\RepositoryInterface; |
||||||
18 | |||||||
19 | /** |
||||||
20 | * Base class for define common methods for relations |
||||||
21 | * |
||||||
22 | * @template L as object |
||||||
23 | * @template R as object |
||||||
24 | * |
||||||
25 | * @implements RelationInterface<L, R> |
||||||
26 | */ |
||||||
27 | abstract class AbstractRelation implements RelationInterface |
||||||
28 | { |
||||||
29 | /** |
||||||
30 | * Relation target attribute |
||||||
31 | * |
||||||
32 | * @var string |
||||||
33 | */ |
||||||
34 | protected $attributeAim; |
||||||
35 | |||||||
36 | /** |
||||||
37 | * The local repository of this relation |
||||||
38 | * |
||||||
39 | * @var RepositoryInterface<L> |
||||||
40 | */ |
||||||
41 | protected $local; |
||||||
42 | |||||||
43 | /** |
||||||
44 | * The local alias |
||||||
45 | * |
||||||
46 | * @var string|null |
||||||
47 | */ |
||||||
48 | protected $localAlias; |
||||||
49 | |||||||
50 | /** |
||||||
51 | * The distant repository |
||||||
52 | * |
||||||
53 | * @var RepositoryInterface<R> |
||||||
54 | */ |
||||||
55 | protected $distant; |
||||||
56 | |||||||
57 | /** |
||||||
58 | * Global constraints for this relation |
||||||
59 | * |
||||||
60 | * @var array |
||||||
61 | */ |
||||||
62 | protected $constraints = []; |
||||||
63 | |||||||
64 | /** |
||||||
65 | * Is the relation not embedded in entity |
||||||
66 | * |
||||||
67 | * @var bool |
||||||
68 | */ |
||||||
69 | protected $isDetached = false; |
||||||
70 | |||||||
71 | /** |
||||||
72 | * The query's result wrapper |
||||||
73 | * |
||||||
74 | * @var null|string|callable |
||||||
75 | * |
||||||
76 | * @see Query::wrapAs() |
||||||
77 | * @see RelationBuilder::wrapAs() |
||||||
78 | */ |
||||||
79 | protected $wrapper; |
||||||
80 | |||||||
81 | /** |
||||||
82 | * @var RelationInfoInterface |
||||||
83 | */ |
||||||
84 | protected $relationInfo; |
||||||
85 | |||||||
86 | |||||||
87 | /** |
||||||
88 | * Set the relation info |
||||||
89 | * |
||||||
90 | * @param string $attributeAim The property name that hold the relation |
||||||
91 | * @param RepositoryInterface<L> $local |
||||||
92 | * @param RepositoryInterface<R>|null $distant |
||||||
93 | */ |
||||||
94 | 263 | public function __construct($attributeAim, RepositoryInterface $local, ?RepositoryInterface $distant = null) |
|||||
95 | { |
||||||
96 | 263 | $this->attributeAim = $attributeAim; |
|||||
97 | 263 | $this->local = $local; |
|||||
98 | 263 | $this->distant = $distant; |
|||||
99 | |||||||
100 | 263 | $this->relationInfo = Locatorizable::isActiveRecordEnabled() |
|||||
101 | 263 | ? new LocalHashTableRelationInfo() |
|||||
102 | : NullRelationInfo::instance() |
||||||
103 | 263 | ; |
|||||
104 | } |
||||||
105 | |||||||
106 | /** |
||||||
107 | * {@inheritdoc} |
||||||
108 | */ |
||||||
109 | 82 | public function setLocalAlias(?string $localAlias) |
|||||
110 | { |
||||||
111 | 82 | $this->localAlias = $localAlias; |
|||||
112 | |||||||
113 | 82 | return $this; |
|||||
114 | } |
||||||
115 | |||||||
116 | /** |
||||||
117 | * {@inheritdoc} |
||||||
118 | */ |
||||||
119 | 127 | public function localRepository(): RepositoryInterface |
|||||
120 | { |
||||||
121 | 127 | return $this->local; |
|||||
122 | } |
||||||
123 | |||||||
124 | // |
||||||
125 | //----------- options |
||||||
126 | // |
||||||
127 | |||||||
128 | /** |
||||||
129 | * {@inheritdoc} |
||||||
130 | */ |
||||||
131 | 261 | public function setOptions(array $options) |
|||||
132 | { |
||||||
133 | 261 | if (isset($options['constraints'])) { |
|||||
134 | 29 | $this->setConstraints($options['constraints']); |
|||||
135 | } |
||||||
136 | |||||||
137 | 261 | if (!empty($options['detached'])) { |
|||||
138 | 13 | $this->setDetached(true); |
|||||
139 | } |
||||||
140 | |||||||
141 | 261 | if (isset($options['wrapper'])) { |
|||||
142 | 3 | $this->setWrapper($options['wrapper']); |
|||||
143 | } |
||||||
144 | |||||||
145 | 261 | return $this; |
|||||
146 | } |
||||||
147 | |||||||
148 | /** |
||||||
149 | * Get the array of options |
||||||
150 | * |
||||||
151 | * @return array |
||||||
152 | */ |
||||||
153 | 1 | public function getOptions(): array |
|||||
154 | { |
||||||
155 | 1 | return [ |
|||||
156 | 1 | 'constraints' => $this->constraints, |
|||||
157 | 1 | 'detached' => $this->isDetached, |
|||||
158 | 1 | 'wrapper' => $this->wrapper, |
|||||
159 | 1 | ]; |
|||||
160 | } |
||||||
161 | |||||||
162 | /** |
||||||
163 | * Set the embedded status |
||||||
164 | * |
||||||
165 | * @param bool $flag |
||||||
166 | * |
||||||
167 | * @return $this |
||||||
168 | */ |
||||||
169 | 14 | public function setDetached(bool $flag) |
|||||
170 | { |
||||||
171 | 14 | $this->isDetached = $flag; |
|||||
172 | |||||||
173 | 14 | return $this; |
|||||
174 | } |
||||||
175 | |||||||
176 | /** |
||||||
177 | * Is the relation embedded |
||||||
178 | * |
||||||
179 | * @return bool |
||||||
180 | */ |
||||||
181 | 1 | public function isDetached(): bool |
|||||
182 | { |
||||||
183 | 1 | return $this->isDetached; |
|||||
184 | } |
||||||
185 | |||||||
186 | /** |
||||||
187 | * Get the query's result wrapper |
||||||
188 | * |
||||||
189 | * @return string|callable|null |
||||||
190 | */ |
||||||
191 | 1 | public function getWrapper() |
|||||
192 | { |
||||||
193 | 1 | return $this->wrapper; |
|||||
194 | } |
||||||
195 | |||||||
196 | /** |
||||||
197 | * @param string|callable $wrapper |
||||||
198 | * |
||||||
199 | * @return $this |
||||||
200 | */ |
||||||
201 | 4 | public function setWrapper($wrapper) |
|||||
202 | { |
||||||
203 | 4 | $this->wrapper = $wrapper; |
|||||
204 | |||||||
205 | 4 | return $this; |
|||||
206 | } |
||||||
207 | |||||||
208 | // |
||||||
209 | //--------- constraints and query |
||||||
210 | // |
||||||
211 | |||||||
212 | /** |
||||||
213 | * Set the global constraints for this relation |
||||||
214 | * |
||||||
215 | * @param array|\Closure $constraints |
||||||
216 | * |
||||||
217 | * @return $this |
||||||
218 | */ |
||||||
219 | 139 | public function setConstraints($constraints) |
|||||
220 | { |
||||||
221 | 139 | $this->constraints = $constraints; |
|||||
0 ignored issues
–
show
|
|||||||
222 | |||||||
223 | 139 | return $this; |
|||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * Get the global constraints of this relation |
||||||
228 | * |
||||||
229 | * @return array|\Closure |
||||||
230 | */ |
||||||
231 | 1 | public function getConstraints() |
|||||
232 | { |
||||||
233 | 1 | return $this->constraints; |
|||||
234 | } |
||||||
235 | |||||||
236 | /** |
||||||
237 | * {@inheritdoc} |
||||||
238 | */ |
||||||
239 | 174 | public function isLoaded($entity): bool |
|||||
240 | { |
||||||
241 | 174 | return $this->relationInfo->isLoaded($entity); |
|||||
242 | } |
||||||
243 | |||||||
244 | /** |
||||||
245 | * {@inheritdoc} |
||||||
246 | */ |
||||||
247 | public function clearInfo($entity): void |
||||||
248 | { |
||||||
249 | $this->relationInfo->clear($entity); |
||||||
0 ignored issues
–
show
The function
Bdf\Prime\Relations\Info...nInfoInterface::clear() has been deprecated: Will be removed in 3.0
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||||
250 | } |
||||||
251 | |||||||
252 | /** |
||||||
253 | * Apply the constraints on query builder |
||||||
254 | * Allows overload of global constraints if both constraints are arrays |
||||||
255 | * |
||||||
256 | * Use prefix on keys if set |
||||||
257 | * |
||||||
258 | * @param Q $query |
||||||
0 ignored issues
–
show
The type
Bdf\Prime\Relations\Q was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
259 | * @param mixed $constraints |
||||||
260 | * @param string $context The context is the prefix used by the query to refer to the related repository |
||||||
261 | * |
||||||
262 | * @return Q |
||||||
263 | * |
||||||
264 | * @template Q as \Bdf\Prime\Query\Contract\Whereable&ReadCommandInterface |
||||||
265 | */ |
||||||
266 | 263 | protected function applyConstraints(ReadCommandInterface $query, $constraints = [], $context = null): ReadCommandInterface |
|||||
267 | { |
||||||
268 | 263 | if (is_array($constraints) && is_array($this->constraints)) { |
|||||
269 | 262 | $query->where($this->applyContext($context, $constraints + $this->constraints)); |
|||||
0 ignored issues
–
show
The method
where() does not exist on Bdf\Prime\Query\ReadCommandInterface . Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\ReadCommandInterface .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
270 | } else { |
||||||
271 | 1 | $query->where($this->applyContext($context, $this->constraints)); |
|||||
272 | 1 | $query->where($this->applyContext($context, $constraints)); |
|||||
273 | } |
||||||
274 | |||||||
275 | 263 | return $query; |
|||||
0 ignored issues
–
show
|
|||||||
276 | } |
||||||
277 | |||||||
278 | /** |
||||||
279 | * Apply the context prefix on each keys of the array of constraints |
||||||
280 | * |
||||||
281 | * @todo algo également présent dans EntityRepository::constraints() |
||||||
282 | * |
||||||
283 | * @param string|null $context |
||||||
284 | * @param mixed|array<string,mixed> $constraints |
||||||
285 | * |
||||||
286 | * @return mixed |
||||||
287 | */ |
||||||
288 | 266 | protected function applyContext(?string $context, $constraints) |
|||||
289 | { |
||||||
290 | 266 | if ($context && is_array($constraints)) { |
|||||
291 | 82 | $context .= '.'; |
|||||
292 | |||||||
293 | /** @var string $key */ |
||||||
294 | 82 | foreach ($constraints as $key => $value) { |
|||||
295 | // Skip commands |
||||||
296 | 26 | if ($key[0] !== ':') { |
|||||
297 | 26 | $constraints[$context.$key] = $value; |
|||||
298 | } |
||||||
299 | |||||||
300 | 26 | unset($constraints[$key]); |
|||||
301 | } |
||||||
302 | } |
||||||
303 | |||||||
304 | 266 | return $constraints; |
|||||
305 | } |
||||||
306 | |||||||
307 | /** |
||||||
308 | * Get a query builder from distant entities |
||||||
309 | * |
||||||
310 | * @param string|array $value |
||||||
311 | * @param mixed $constraints |
||||||
312 | * |
||||||
313 | * @return ReadCommandInterface<\Bdf\Prime\Connection\ConnectionInterface, R>&Deletable |
||||||
314 | */ |
||||||
315 | 183 | protected function query($value, $constraints = []): ReadCommandInterface |
|||||
316 | { |
||||||
317 | 183 | return $this->applyConstraints( |
|||||
318 | 183 | $this->applyWhereKeys($this->distant->queries()->builder(), $value), |
|||||
319 | 183 | $constraints |
|||||
320 | 183 | ); |
|||||
321 | } |
||||||
322 | |||||||
323 | /** |
||||||
324 | * Apply the where constraint on the query |
||||||
325 | * |
||||||
326 | * @param Q $query |
||||||
327 | * @param mixed $value The keys. Can be an array of keys for perform a "IN" query |
||||||
328 | * |
||||||
329 | * @return Q |
||||||
330 | * |
||||||
331 | * @template Q as \Bdf\Prime\Query\Contract\Whereable&ReadCommandInterface<\Bdf\Prime\Connection\ConnectionInterface, R> |
||||||
332 | */ |
||||||
333 | abstract protected function applyWhereKeys(ReadCommandInterface $query, $value): ReadCommandInterface; |
||||||
334 | |||||||
335 | // |
||||||
336 | //---------- util methods to set and get/set relation, foreign and primary key |
||||||
337 | // |
||||||
338 | |||||||
339 | /** |
||||||
340 | * Set the relation value of an entity |
||||||
341 | * |
||||||
342 | * @param L $entity The relation owner |
||||||
0 ignored issues
–
show
The type
Bdf\Prime\Relations\L was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
343 | * @param R|R[]|null $relation The entity to set to the owner. Can be an array of entities |
||||||
344 | */ |
||||||
345 | 235 | protected function setRelation($entity, $relation): void |
|||||
346 | { |
||||||
347 | 235 | if ($this->isDetached) { |
|||||
348 | return; |
||||||
349 | } |
||||||
350 | |||||||
351 | 235 | if ($this->wrapper !== null) { |
|||||
352 | 8 | $relation = $this->distant->collectionFactory()->wrap((array) $relation, $this->wrapper); |
|||||
353 | } |
||||||
354 | |||||||
355 | 235 | $this->local->mapper()->hydrateOne($entity, $this->attributeAim, $relation); |
|||||
356 | |||||||
357 | 235 | if ($relation !== null) { |
|||||
358 | 230 | $this->relationInfo->markAsLoaded($entity); |
|||||
359 | } else { |
||||||
360 | 9 | $this->relationInfo->clear($entity); |
|||||
0 ignored issues
–
show
The function
Bdf\Prime\Relations\Info...nInfoInterface::clear() has been deprecated: Will be removed in 3.0
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||||
361 | } |
||||||
362 | } |
||||||
363 | |||||||
364 | /** |
||||||
365 | * Get the relation value of an entity |
||||||
366 | * |
||||||
367 | * @param L $entity |
||||||
368 | * |
||||||
369 | * @return R|R[]|null The relation object. Can be an array on many relation |
||||||
370 | */ |
||||||
371 | 71 | protected function getRelation($entity) |
|||||
372 | { |
||||||
373 | 71 | if ($this->isDetached) { |
|||||
374 | return null; |
||||||
375 | } |
||||||
376 | |||||||
377 | 71 | $relation = $this->local->mapper()->extractOne($entity, $this->attributeAim); |
|||||
378 | |||||||
379 | 71 | if ($relation === null) { |
|||||
380 | 5 | return null; |
|||||
381 | } |
||||||
382 | |||||||
383 | 67 | if ($relation instanceof CollectionInterface) { |
|||||
384 | 5 | return $relation->all(); |
|||||
385 | } |
||||||
386 | |||||||
387 | 62 | return $relation; |
|||||
388 | } |
||||||
389 | |||||||
390 | /** |
||||||
391 | * Get the referenced alias for this query |
||||||
392 | * |
||||||
393 | * This method returns the local alias in the context of the query |
||||||
394 | * |
||||||
395 | * @param ReadCommandInterface $query |
||||||
396 | * |
||||||
397 | * @return string The alias of the local table |
||||||
398 | */ |
||||||
399 | 82 | protected function getLocalAlias(ReadCommandInterface $query) |
|||||
400 | { |
||||||
401 | // @todo works ? |
||||||
402 | /** @psalm-suppress UndefinedInterfaceMethod */ |
||||||
403 | 82 | if ($this->local === $query->repository()) { |
|||||
0 ignored issues
–
show
The method
repository() does not exist on Bdf\Prime\Query\ReadCommandInterface . It seems like you code against a sub-type of said class. However, the method does not exist in Bdf\Prime\Query\Contract...\KeyValueQueryInterface or Bdf\Prime\Query\QueryInterface or Bdf\Prime\Query\SqlQueryInterface . Are you sure you never get one of those?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
404 | 82 | return ''; |
|||||
405 | } |
||||||
406 | |||||||
407 | 33 | return '$'.$this->localAlias.'>'; |
|||||
408 | } |
||||||
409 | |||||||
410 | // |
||||||
411 | //---------- Relation operations methods |
||||||
412 | // |
||||||
413 | |||||||
414 | /** |
||||||
415 | * {@inheritdoc} |
||||||
416 | */ |
||||||
417 | public function load(EntityIndexerInterface $collection, array $with = [], $constraints = [], array $without = []): void |
||||||
418 | { |
||||||
419 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
420 | } |
||||||
421 | |||||||
422 | /** |
||||||
423 | * {@inheritdoc} |
||||||
424 | */ |
||||||
425 | public function associate($owner, $entity) |
||||||
426 | { |
||||||
427 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
428 | } |
||||||
429 | |||||||
430 | /** |
||||||
431 | * {@inheritdoc} |
||||||
432 | */ |
||||||
433 | public function dissociate($owner) |
||||||
434 | { |
||||||
435 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
436 | } |
||||||
437 | |||||||
438 | /** |
||||||
439 | * {@inheritdoc} |
||||||
440 | */ |
||||||
441 | public function create($owner, array $data = []) |
||||||
442 | { |
||||||
443 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
444 | } |
||||||
445 | |||||||
446 | /** |
||||||
447 | * {@inheritdoc} |
||||||
448 | */ |
||||||
449 | public function add($owner, $related): int |
||||||
450 | { |
||||||
451 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
452 | } |
||||||
453 | |||||||
454 | /** |
||||||
455 | * {@inheritdoc} |
||||||
456 | */ |
||||||
457 | public function saveAll($owner, array $relations = []): int |
||||||
458 | { |
||||||
459 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
460 | } |
||||||
461 | |||||||
462 | /** |
||||||
463 | * {@inheritdoc} |
||||||
464 | */ |
||||||
465 | public function deleteAll($owner, array $relations = []): int |
||||||
466 | { |
||||||
467 | throw new BadMethodCallException('Unsupported operation '.__METHOD__); |
||||||
468 | } |
||||||
469 | |||||||
470 | /** |
||||||
471 | * {@inheritdoc} |
||||||
472 | */ |
||||||
473 | 73 | public function loadIfNotLoaded(EntityIndexerInterface $collection, array $with = [], $constraints = [], array $without = []): void |
|||||
474 | { |
||||||
475 | 73 | if ($collection->empty()) { |
|||||
476 | return; |
||||||
477 | } |
||||||
478 | |||||||
479 | // Constraints are set : force loading |
||||||
480 | // At least one entity is not loaded : perform loading from database |
||||||
481 | 73 | if ($constraints || !$this->isAllLoaded($collection->all())) { |
|||||
482 | 72 | $this->load($collection, $with, $constraints, $without); |
|||||
483 | 69 | return; |
|||||
484 | } |
||||||
485 | |||||||
486 | // Already loaded and no sub-relation to load |
||||||
487 | 11 | if (empty($with)) { |
|||||
488 | 8 | return; |
|||||
489 | } |
||||||
490 | |||||||
491 | 5 | $with = Relation::sanitizeRelations($with); |
|||||
492 | |||||||
493 | 5 | $indexer = new EntitySetIndexer($this->distant->mapper()); |
|||||
494 | |||||||
495 | 5 | foreach ($collection->all() as $owner) { |
|||||
496 | 5 | if (!$relationValue = $this->getRelation($owner)) { |
|||||
497 | continue; // The owner has no relation : skip |
||||||
498 | } |
||||||
499 | |||||||
500 | 5 | if (is_array($relationValue)) { |
|||||
501 | 1 | foreach ($relationValue as $entity) { |
|||||
502 | 1 | $indexer->push($entity); |
|||||
503 | } |
||||||
504 | } else { |
||||||
505 | 4 | $indexer->push($relationValue); |
|||||
506 | } |
||||||
507 | } |
||||||
508 | |||||||
509 | 5 | foreach ($with as $relationName => $options) { |
|||||
510 | 5 | $this->distant->relation($relationName)->loadIfNotLoaded($indexer, $options['relations'], $options['constraints'], $without[$relationName] ?? []); |
|||||
511 | } |
||||||
512 | } |
||||||
513 | |||||||
514 | /** |
||||||
515 | * Check if all entities has loaded the relation |
||||||
516 | * |
||||||
517 | * @param L[] $collection |
||||||
518 | * |
||||||
519 | * @return bool |
||||||
520 | */ |
||||||
521 | 73 | private function isAllLoaded(array $collection): bool |
|||||
522 | { |
||||||
523 | 73 | foreach ($collection as $entity) { |
|||||
524 | 73 | if (!$this->isLoaded($entity)) { |
|||||
525 | 72 | return false; |
|||||
526 | } |
||||||
527 | } |
||||||
528 | |||||||
529 | 11 | return true; |
|||||
530 | } |
||||||
531 | } |
||||||
532 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.