1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Cycle\ORM\Select; |
||||
6 | |||||
7 | use Cycle\Database\Query\SelectQuery; |
||||
8 | use Cycle\ORM\Exception\FactoryException; |
||||
9 | use Cycle\ORM\Exception\LoaderException; |
||||
10 | use Cycle\ORM\Exception\SchemaException; |
||||
11 | use Cycle\ORM\FactoryInterface; |
||||
12 | use Cycle\ORM\Parser\AbstractNode; |
||||
13 | use Cycle\ORM\Service\SourceProviderInterface; |
||||
14 | use Cycle\ORM\Relation; |
||||
15 | use Cycle\ORM\SchemaInterface; |
||||
16 | use Cycle\ORM\Select\Loader\ParentLoader; |
||||
17 | use Cycle\ORM\Select\Loader\SubclassLoader; |
||||
18 | use Cycle\ORM\Select\Traits\AliasTrait; |
||||
19 | use Cycle\ORM\Select\Traits\ChainTrait; |
||||
20 | use JetBrains\PhpStorm\Deprecated; |
||||
21 | |||||
22 | /** |
||||
23 | * ORM Loaders used to load an compile data tree based on results fetched from SQL databases, |
||||
24 | * loaders can communicate with SelectQuery by providing it's own set of conditions, columns |
||||
25 | * joins and etc. In some cases loader may create additional selector to load data using information |
||||
26 | * fetched from previous query. |
||||
27 | * |
||||
28 | * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface |
||||
29 | * in order to support external references (MongoDB and etc). |
||||
30 | * |
||||
31 | * Loaders can be used for both - loading and filtering of record data. |
||||
32 | * |
||||
33 | * Reference tree generation logic example: |
||||
34 | * User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user |
||||
35 | * is USER_ID. Post loader must request User data loader to create references based on ID field |
||||
36 | * values. Once Post data were parsed we can mount it under parent user using mount method: |
||||
37 | * |
||||
38 | * @see Select::load() |
||||
39 | * @see Select::with() |
||||
40 | * |
||||
41 | * @internal |
||||
42 | */ |
||||
43 | abstract class AbstractLoader implements LoaderInterface |
||||
44 | { |
||||
45 | use AliasTrait; |
||||
46 | use ChainTrait; |
||||
47 | |||||
48 | // Loading methods for data loaders. |
||||
49 | public const INLOAD = 1; |
||||
50 | public const POSTLOAD = 2; |
||||
51 | public const JOIN = 3; |
||||
52 | public const LEFT_JOIN = 4; |
||||
53 | protected const SUBQUERY = 5; |
||||
54 | |||||
55 | protected array $options = [ |
||||
56 | 'load' => false, |
||||
57 | 'scope' => true, |
||||
58 | ]; |
||||
59 | |||||
60 | /** @var LoaderInterface[] */ |
||||
61 | protected array $load = []; |
||||
62 | |||||
63 | /** @var AbstractLoader[] */ |
||||
64 | protected array $join = []; |
||||
65 | |||||
66 | /** |
||||
67 | * Parent in class inheritance hierarchy |
||||
68 | */ |
||||
69 | protected ?AbstractLoader $inherit = null; |
||||
70 | |||||
71 | /** @var SubclassLoader[] */ |
||||
72 | protected array $subclasses = []; |
||||
73 | |||||
74 | protected bool $loadSubclasses = true; |
||||
75 | |||||
76 | /** |
||||
77 | * Loader that contains current loader |
||||
78 | */ |
||||
79 | protected ?LoaderInterface $parent = null; |
||||
80 | |||||
81 | /** |
||||
82 | * Children roles for Joined Table Inheritance |
||||
83 | * |
||||
84 | * @var array<string, array> |
||||
85 | */ |
||||
86 | protected array $children; |
||||
87 | |||||
88 | 6834 | protected SourceInterface $source; |
|||
89 | |||||
90 | public function __construct( |
||||
91 | protected SchemaInterface $ormSchema, |
||||
92 | protected SourceProviderInterface $sourceProvider, |
||||
93 | protected FactoryInterface $factory, |
||||
94 | 6834 | protected string $target, |
|||
95 | 6834 | ) { |
|||
96 | $this->children = $this->ormSchema->getInheritedRoles($target); |
||||
97 | $this->source = $this->sourceProvider->getSource($target); |
||||
98 | 6834 | } |
|||
99 | |||||
100 | 6834 | public function isHierarchical(): bool |
|||
101 | { |
||||
102 | return $this->inherit !== null || ($this->loadSubclasses && $this->children !== []); |
||||
103 | } |
||||
104 | |||||
105 | public function setSubclassesLoading(bool $enabled): void |
||||
106 | 6064 | { |
|||
107 | $this->loadSubclasses = $enabled; |
||||
108 | 6064 | } |
|||
109 | |||||
110 | 6064 | public function getTarget(): string |
|||
111 | 1386 | { |
|||
112 | return $this->target; |
||||
113 | } |
||||
114 | 6064 | ||||
115 | 72 | public function withContext(LoaderInterface $parent, array $options = []): static |
|||
116 | { |
||||
117 | // check that given options are known |
||||
118 | 6064 | if (!empty($wrong = \array_diff(\array_keys($options), \array_keys($this->options)))) { |
|||
119 | throw new LoaderException( |
||||
120 | 6064 | \sprintf( |
|||
121 | 176 | 'Relation %s does not support option: %s', |
|||
122 | $this::class, |
||||
123 | \implode(',', $wrong), |
||||
124 | ), |
||||
125 | 1146 | ); |
|||
126 | } |
||||
127 | 1146 | ||||
128 | $loader = clone $this; |
||||
129 | $loader->parent = $parent; |
||||
130 | 264 | $loader->options = $options + $this->options; |
|||
131 | |||||
132 | 264 | return $loader; |
|||
133 | } |
||||
134 | |||||
135 | 6358 | /** |
|||
136 | * Load the relation. |
||||
137 | 6358 | * |
|||
138 | * @param LoaderInterface|string $relation Relation name, or chain of relations separated by. If you need to set |
||||
139 | * inheritance then pass LoaderInterface object |
||||
140 | 4458 | * @param array $options Loader options (to be applied to last chain element only). |
|||
141 | * @param bool $join When set to true loaders will be forced into JOIN mode. |
||||
142 | * @param bool $load Load relation data. |
||||
143 | 4458 | * |
|||
144 | * @return LoaderInterface Must return loader for a requested relation. |
||||
145 | * @throws LoaderException |
||||
146 | */ |
||||
147 | public function loadRelation( |
||||
148 | string|LoaderInterface $relation, |
||||
149 | array $options, |
||||
150 | bool $join = false, |
||||
151 | bool $load = false, |
||||
152 | ): LoaderInterface { |
||||
153 | 4458 | if ($relation instanceof ParentLoader) { |
|||
154 | 4458 | return $this->inherit = $relation->withContext($this); |
|||
155 | 4458 | } |
|||
156 | |||||
157 | 4458 | if ($relation instanceof SubclassLoader) { |
|||
158 | $loader = $relation->withContext($this); |
||||
159 | $this->subclasses[] = $loader; |
||||
160 | return $loader; |
||||
161 | } |
||||
162 | |||||
163 | $relation = $this->resolvePath($relation); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
164 | $alias ??= $options['alias'] ?? $relation; |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
165 | unset($options['alias']); |
||||
166 | if (!empty($options['as'])) { |
||||
167 | // ?? |
||||
168 | $this->registerPath($options['as'], $alias); |
||||
169 | } |
||||
170 | |||||
171 | // Check if relation contain dot, i.e. relation chain |
||||
172 | if ($this->isChain($relation)) { |
||||
173 | 4554 | return $this->loadChain($relation, $options, $join, $load, $alias); |
|||
0 ignored issues
–
show
The call to
Cycle\ORM\Select\AbstractLoader::loadChain() has too many arguments starting with $alias .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||
174 | } |
||||
175 | |||||
176 | /* |
||||
177 | * Joined loaders must be isolated from normal loaders due they would not load any data |
||||
178 | * and will only modify SelectQuery. |
||||
179 | 4554 | */ |
|||
180 | 744 | if (!$join || $load) { |
|||
181 | $loaders = &$this->load; |
||||
182 | 4394 | } else { |
|||
183 | 616 | $loaders = &$this->join; |
|||
184 | 616 | } |
|||
185 | 616 | ||||
186 | if ($load) { |
||||
187 | 4082 | $options['load'] ??= true; |
|||
188 | 4082 | } |
|||
189 | 160 | ||||
190 | if (isset($loaders[$relation])) { |
||||
191 | // Overwrite existing loader options |
||||
192 | return $loaders[$alias] = $loaders[$alias]->withContext($this, $options); |
||||
193 | 4082 | } |
|||
194 | 362 | ||||
195 | if ($join) { |
||||
196 | if (empty($options['method']) || !\in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) { |
||||
197 | // let's tell our loaded that it's method is JOIN (forced) |
||||
198 | $options['method'] = self::JOIN; |
||||
199 | } |
||||
200 | } |
||||
201 | 4082 | ||||
202 | 3642 | try { |
|||
203 | // Creating new loader. |
||||
204 | 552 | $loader = $this->factory->loader( |
|||
205 | $this->ormSchema, |
||||
206 | $this->sourceProvider, |
||||
207 | 4082 | $this->target, |
|||
208 | 3642 | $relation, |
|||
209 | ); |
||||
210 | } catch (SchemaException | FactoryException $e) { |
||||
211 | 4082 | if ($this->inherit instanceof self) { |
|||
212 | return $this->inherit->loadRelation($relation, $options, $join, $load, $alias); |
||||
0 ignored issues
–
show
The call to
Cycle\ORM\Select\AbstractLoader::loadRelation() has too many arguments starting with $alias .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||
213 | 416 | } |
|||
214 | throw new LoaderException( |
||||
215 | \sprintf('Unable to create loader: %s', $e->getMessage()), |
||||
216 | 4082 | $e->getCode(), |
|||
217 | 552 | $e, |
|||
218 | ); |
||||
219 | 544 | } |
|||
220 | |||||
221 | return $loaders[$alias] = $loader->withContext($this, $options); |
||||
222 | } |
||||
223 | |||||
224 | public function createNode(): AbstractNode |
||||
225 | 4082 | { |
|||
226 | 4082 | $node = $this->initNode(); |
|||
227 | 4082 | ||||
228 | 4082 | if ($this->inherit !== null) { |
|||
229 | $node->joinNode(null, $this->inherit->createNode()); |
||||
230 | } |
||||
231 | 64 | ||||
232 | 64 | foreach ($this->load as $relation => $loader) { |
|||
233 | 56 | if ($loader instanceof JoinableInterface && $loader->isJoined()) { |
|||
234 | $node->joinNode($relation, $loader->createNode()); |
||||
235 | 8 | continue; |
|||
236 | 8 | } |
|||
237 | 8 | ||||
238 | $node->linkNode($relation, $loader->createNode()); |
||||
239 | } |
||||
240 | |||||
241 | if ($this->loadSubclasses) { |
||||
242 | 4082 | foreach ($this->subclasses as $loader) { |
|||
243 | $node->joinNode(null, $loader->createNode()); |
||||
244 | } |
||||
245 | 6382 | } |
|||
246 | |||||
247 | 6382 | return $node; |
|||
248 | } |
||||
249 | 6382 | ||||
250 | 744 | public function loadData(AbstractNode $node, bool $includeRole = false): void |
|||
251 | { |
||||
252 | $this->loadChild($node, $includeRole); |
||||
253 | 6382 | } |
|||
254 | 3554 | ||||
255 | 1370 | public function getSource(): SourceInterface |
|||
256 | 1370 | { |
|||
257 | return $this->source; |
||||
258 | } |
||||
259 | 2378 | ||||
260 | /** |
||||
261 | * Returns inheritance parent loader. |
||||
262 | 6382 | */ |
|||
263 | 6334 | public function getParentLoader(): ?LoaderInterface |
|||
264 | 336 | { |
|||
265 | return $this->inherit; |
||||
266 | } |
||||
267 | |||||
268 | 6382 | /** |
|||
269 | * Indicates that loader loads data. |
||||
270 | */ |
||||
271 | 3842 | abstract public function isLoaded(): bool; |
|||
272 | |||||
273 | 3842 | /** |
|||
274 | * Ensure state of every nested loader. |
||||
275 | */ |
||||
276 | public function __clone() |
||||
277 | { |
||||
278 | $this->parent = null; |
||||
279 | |||||
280 | foreach ($this->load as $name => $loader) { |
||||
281 | $this->load[$name] = $loader->withContext($this); |
||||
282 | } |
||||
283 | |||||
284 | foreach ($this->join as $name => $loader) { |
||||
285 | $this->join[$name] = $loader->withContext($this); |
||||
286 | 3842 | } |
|||
287 | |||||
288 | 3842 | $this->inherit = $this->inherit?->withContext($this); |
|||
289 | 730 | ||||
290 | foreach ($this->subclasses as $i => $loader) { |
||||
291 | 3842 | $this->subclasses[$i] = $loader->withContext($this); |
|||
292 | } |
||||
293 | } |
||||
294 | 6334 | ||||
295 | final public function __destruct() |
||||
296 | 6334 | { |
|||
297 | 168 | unset($this->parent, $this->inherit, $this->subclasses, $this->load, $this->join); |
|||
298 | } |
||||
299 | |||||
300 | protected function loadChild(AbstractNode $node, bool $includeRole = false): void |
||||
301 | 6286 | { |
|||
302 | 744 | foreach ($this->load as $relation => $loader) { |
|||
303 | 744 | $loader->loadData($node->getNode($relation), $includeRole); |
|||
304 | } |
||||
305 | $this->loadHierarchy($node, $includeRole); |
||||
306 | } |
||||
307 | 6286 | ||||
308 | 6286 | /** |
|||
309 | 6286 | * @deprecated |
|||
310 | 328 | * |
|||
311 | 328 | * @codeCoverageIgnore |
|||
312 | */ |
||||
313 | #[Deprecated('2.3', '$this->loadHierarchy(%parameter0%, %parameter1%)')] |
||||
314 | protected function loadIerarchy(AbstractNode $node, bool $includeRole = false): void |
||||
315 | 6286 | { |
|||
316 | $this->loadHierarchy($node, $includeRole); |
||||
317 | } |
||||
318 | |||||
319 | protected function loadHierarchy(AbstractNode $node, bool $includeRole = false): void |
||||
320 | { |
||||
321 | if ($this->inherit === null && !$this->loadSubclasses) { |
||||
322 | return; |
||||
323 | 6494 | } |
|||
324 | |||||
325 | 6494 | // Merge parent nodes |
|||
326 | if ($this->inherit !== null) { |
||||
327 | 6494 | $inheritNode = $node->getParentMergeNode(); |
|||
328 | 744 | $this->inherit->loadData($inheritNode, $includeRole); |
|||
329 | } |
||||
330 | |||||
331 | 6494 | // Merge subclass nodes |
|||
332 | 552 | if ($this->loadSubclasses) { |
|||
333 | $subclassNodes = $node->getSubclassMergeNodes(); |
||||
334 | foreach ($this->subclasses as $i => $loader) { |
||||
335 | 6494 | $inheritNode = $subclassNodes[$i]; |
|||
336 | 3562 | $loader->loadData($inheritNode, $includeRole); |
|||
337 | 1378 | } |
|||
338 | 24 | } |
|||
339 | 1354 | ||||
340 | $node->mergeInheritanceNodes($includeRole); |
||||
341 | } |
||||
342 | |||||
343 | 6470 | /** |
|||
344 | 6422 | * Create input node for the loader. |
|||
345 | 328 | */ |
|||
346 | abstract protected function initNode(): AbstractNode; |
||||
347 | |||||
348 | protected function configureQuery(SelectQuery $query): SelectQuery |
||||
349 | 6470 | { |
|||
350 | $query = $this->applyScope($query); |
||||
351 | |||||
352 | if ($this->inherit !== null) { |
||||
353 | $query = $this->inherit->configureQuery($query); |
||||
354 | } |
||||
355 | |||||
356 | foreach ($this->join as $loader) { |
||||
357 | $query = $loader->configureQuery($query); |
||||
358 | } |
||||
359 | 6834 | ||||
360 | foreach ($this->load as $loader) { |
||||
361 | 6834 | if ($loader instanceof JoinableInterface && $loader->isJoined()) { |
|||
362 | $query = $loader->isHierarchical() |
||||
363 | ? $loader->configureSubQuery($query) |
||||
0 ignored issues
–
show
The method
configureSubQuery() does not exist on Cycle\ORM\Select\JoinableInterface . Did you maybe mean configureQuery() ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||
364 | : $loader->configureQuery($query); |
||||
365 | } |
||||
366 | } |
||||
367 | 6834 | ||||
368 | if ($this->loadSubclasses) { |
||||
369 | 6834 | foreach ($this->subclasses as $loader) { |
|||
370 | 6834 | $query = $loader->configureQuery($query); |
|||
371 | 6834 | } |
|||
372 | 744 | } |
|||
373 | |||||
374 | 6834 | return $query; |
|||
375 | 6834 | } |
|||
376 | |||||
377 | abstract protected function applyScope(SelectQuery $query): SelectQuery; |
||||
378 | 6834 | ||||
379 | /** |
||||
380 | 6834 | * Define schema option associated with the entity. |
|||
381 | 6834 | * |
|||
382 | 6834 | * @return mixed |
|||
383 | 6834 | */ |
|||
384 | protected function define(int $property) |
||||
385 | { |
||||
386 | 6834 | return $this->ormSchema->define($this->target, $property); |
|||
387 | } |
||||
388 | 6834 | ||||
389 | 616 | /** |
|||
390 | 616 | * Returns list of relations to be automatically joined with parent object. |
|||
391 | 616 | */ |
|||
392 | protected function getEagerLoaders(?string $role = null): \Generator |
||||
393 | { |
||||
394 | $role ??= $this->target; |
||||
395 | $parentLoader = $this->generateParentLoader($role); |
||||
396 | 6834 | if ($parentLoader !== null) { |
|||
397 | yield $parentLoader; |
||||
398 | 6834 | } |
|||
399 | 6834 | yield from $this->generateSublassLoaders(); |
|||
400 | 5278 | yield from $this->generateEagerRelationLoaders($role); |
|||
401 | 840 | } |
|||
402 | |||||
403 | protected function generateParentLoader(string $role): ?LoaderInterface |
||||
404 | { |
||||
405 | $parent = $this->ormSchema->define($role, SchemaInterface::PARENT); |
||||
406 | return $parent === null |
||||
407 | ? null |
||||
408 | : $this->factory->loader($this->ormSchema, $this->sourceProvider, $role, FactoryInterface::PARENT_LOADER); |
||||
409 | } |
||||
410 | |||||
411 | protected function generateSublassLoaders(): iterable |
||||
412 | { |
||||
413 | if ($this->children !== []) { |
||||
414 | foreach ($this->children as $subRole => $children) { |
||||
415 | yield $this->factory |
||||
416 | ->loader($this->ormSchema, $this->sourceProvider, $subRole, FactoryInterface::CHILD_LOADER); |
||||
417 | } |
||||
418 | } |
||||
419 | } |
||||
420 | |||||
421 | protected function generateEagerRelationLoaders(string $target): \Generator |
||||
422 | { |
||||
423 | $relations = $this->ormSchema->define($target, SchemaInterface::RELATIONS) ?? []; |
||||
424 | foreach ($relations as $relation => $schema) { |
||||
425 | if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) { |
||||
426 | yield $relation; |
||||
427 | } |
||||
428 | } |
||||
429 | } |
||||
430 | } |
||||
431 |