Issues (590)

src/Relations/AbstractRelation.php (8 issues)

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
Documentation Bug introduced by
It seems like $constraints can also be of type Closure. However, the property $constraints is declared as type array. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

249
        /** @scrutinizer ignore-deprecated */ $this->relationInfo->clear($entity);

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.

Loading history...
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. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 ignore-call  annotation

269
            $query->/** @scrutinizer ignore-call */ 
270
                    where($this->applyContext($context, $constraints + $this->constraints));
Loading history...
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
Bug Best Practice introduced by
The expression return $query returns the type Bdf\Prime\Query\ReadCommandInterface which is incompatible with the documented return type Bdf\Prime\Relations\Q.
Loading history...
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. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

360
            /** @scrutinizer ignore-deprecated */ $this->relationInfo->clear($entity);

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.

Loading history...
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 ignore-call  annotation

403
        if ($this->local === $query->/** @scrutinizer ignore-call */ repository()) {
Loading history...
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