BelongsToMany::relationQuery()   A
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.0493

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 19
ccs 8
cts 9
cp 0.8889
rs 9.2222
c 0
b 0
f 0
cc 6
nc 4
nop 2
crap 6.0493
1
<?php
2
3
namespace Bdf\Prime\Relations;
4
5
use Bdf\Prime\Exception\PrimeException;
6
use Bdf\Prime\Query\Contract\Deletable;
7
use Bdf\Prime\Query\Contract\EntityJoinable;
8
use Bdf\Prime\Query\Contract\ReadOperation;
9
use Bdf\Prime\Query\Contract\WriteOperation;
10
use Bdf\Prime\Query\Custom\KeyValue\KeyValueQuery;
11
use Bdf\Prime\Query\QueryInterface;
12
use Bdf\Prime\Query\ReadCommandInterface;
13
use Bdf\Prime\Repository\EntityRepository;
14
use Bdf\Prime\Repository\RepositoryInterface;
15
16
/**
17
 * BelongsToMany
18
 *
19
 * For a relation named 'relation' use the prefix 'relationThrough.' for adding constraints on through table.
20
 * ex:
21
 *
22
 * <code>
23
 * $query->with([
24
 *     'relation' => [
25
 *         'name :like'             => '...',
26
 *         'relationThrough.status' => '...'  // through constraint
27
 *     ]
28
 * ]);
29
 * </code>
30
 *
31
 * @package Bdf\Prime\Relations
32
 *
33
 * @todo Voir pour gérer la table de through dynamiquement. Si cette relation est une HasManyThrough, elle doit etre en readonly
34
 *
35
 * @template L as object
36
 * @template R as object
37
 *
38
 * @extends Relation<L, R>
39
 */
40
class BelongsToMany extends Relation
41
{
42
    /**
43
     * Through repository
44
     *
45
     * @var EntityRepository
46
     */
47
    protected $through;
48
49
    /**
50
     * Through local key
51
     *
52
     * @var string
53
     */
54
    protected $throughLocal;
55
56
    /**
57
     * Through distant key
58
     *
59
     * @var string
60
     */
61
    protected $throughDistant;
62
63
    /**
64
     * The through global constraints
65
     *
66
     * @var array
67
     */
68
    protected $throughConstraints = [];
69
70
    /**
71
     * Merge of all constraints
72
     *
73
     * @var array
74
     */
75
    protected $allConstraints = [];
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    protected $saveStrategy = self::SAVE_STRATEGY_REPLACE;
81
82
    //===============================
83
    // Save queries for optimisation
84
    //===============================
85
86
    /**
87
     * @var KeyValueQuery
88
     */
89
    private $throughQuery;
90
91
    /**
92
     * @var KeyValueQuery
93
     */
94
    private $relationQuery;
95
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 22
    public function relationRepository(): RepositoryInterface
101
    {
102 22
        return $this->distant;
103
    }
104
105
    /**
106
     * Set the though infos
107
     *
108
     * @param RepositoryInterface $through
109
     * @param string $throughLocal
110
     * @param string $throughDistant
111
     *
112
     * @return void
113
     */
114 32
    public function setThrough(RepositoryInterface $through, string $throughLocal, string $throughDistant): void
115
    {
116 32
        $this->through        = $through;
0 ignored issues
show
Documentation Bug introduced by
$through is of type Bdf\Prime\Repository\RepositoryInterface, but the property $through was declared to be of type Bdf\Prime\Repository\EntityRepository. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
117 32
        $this->throughLocal   = $throughLocal;
118 32
        $this->throughDistant = $throughDistant;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function setConstraints($constraints)
125
    {
126
        $this->allConstraints = $constraints;
127
128
        list($this->constraints, $this->throughConstraints) = $this->extractConstraints($constraints);
129
130
        return $this;
131
    }
132
133
    /**
134
     * Extract constraints design for through queries
135
     *
136
     * @param array|\Closure $constraints
137
     *
138
     * @return array
139
     */
140 23
    protected function extractConstraints($constraints)
141
    {
142 23
        if (!is_array($constraints)) {
143
            return [$constraints, []];
144
        }
145
146 23
        $through = [];
147 23
        $global = [];
148 23
        $prefix = $this->attributeAim.'Through.';
149 23
        $length = strlen($prefix);
150
151 23
        foreach ($constraints as $column => $value) {
152 6
            if (strpos($column, $prefix) === 0) {
153 3
                $through[substr($column, $length)] = $value;
154
            } else {
155 3
                $global[$column] = $value;
156
            }
157
        }
158
159 23
        return [$global, $through];
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 3
    public function join(EntityJoinable $query, string $alias): void
166
    {
167
        // @fixme ??
168
//        if ($alias === null) {
169
//            $alias = $this->attributeAim;
170
//        }
171
172
        // TODO rechercher l'alias de through dans les tables alias du query builder
173
174 3
        $query->joinEntity($this->through->entityName(), $this->throughLocal, $this->getLocalAlias($query).$this->localKey, $this->attributeAim.'Through');
175 3
        $query->joinEntity($this->distant->entityName(), $this->distantKey, $this->attributeAim.'Through>'.$this->throughDistant, $alias);
176
177 3
        $this->applyConstraints($query, [], $alias);
178 3
        $this->applyThroughConstraints($query, [], $this->attributeAim.'Through');
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 3
    public function joinRepositories(EntityJoinable $query, string $alias, $discriminator = null): array
185
    {
186 3
        return [
187 3
            $this->attributeAim.'Through' => $this->through,
188 3
            $alias => $this->distant,
189 3
        ];
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 19
    public function link($owner): ReadCommandInterface
196
    {
197
        /** @var QueryInterface<\Bdf\Prime\Connection\ConnectionInterface, R>&EntityJoinable $query */
198 19
        $query = $this->distant->queries()->builder();
199
200 19
        return $query
201 19
            ->joinEntity($this->through->entityName(), $this->throughDistant, $this->distantKey, $this->attributeAim.'Through')
0 ignored issues
show
Bug introduced by
The method joinEntity() does not exist on Bdf\Prime\Query\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\QueryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

201
            ->/** @scrutinizer ignore-call */ joinEntity($this->through->entityName(), $this->throughDistant, $this->distantKey, $this->attributeAim.'Through')
Loading history...
202 19
            ->where($this->attributeAim.'Through.'.$this->throughLocal, $this->getLocalKeyValue($owner))
203 19
            ->where($this->allConstraints)
204 19
        ;
205
    }
206
207
    /**
208
     * Get a query from through entity repository
209
     *
210
     * @param string|array  $key
211
     * @param array $constraints
212
     *
213
     * @return ReadCommandInterface&Deletable
214
     */
215 28
    protected function throughQuery($key, $constraints = []): ReadCommandInterface
216
    {
217 28
        if (is_array($key)) {
218 23
            if (count($key) !== 1 || $constraints || $this->throughConstraints) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->throughConstraints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
219 3
                return $this->applyThroughConstraints(
220 3
                    $this->through->where($this->throughLocal, $key),
221 3
                    $constraints
222 3
                );
223
            }
224
225 20
            $key = $key[0];
226
        }
227
228 25
        if ($this->throughQuery) {
229 10
            return $this->throughQuery->where($this->throughLocal, $key);
230
        }
231
232 15
        $this->throughQuery = $this->through->queries()->keyValue($this->throughLocal, $key);
233
234 15
        if ($this->throughQuery) {
235 15
            return $this->throughQuery;
236
        }
237
238
        return $this->applyThroughConstraints(
239
            $this->through->where($this->throughLocal, $key),
240
            $constraints
241
        );
242
    }
243
244
    /**
245
     * Build the query for find related entities
246
     */
247 23
    protected function relationQuery(array $keys, $constraints): ReadCommandInterface
248
    {
249
        // Constraints can be on relation attributes : builder must be used
250
        // @todo Handle "bulk select"
251 23
        if (count($keys) !== 1 || $constraints || $this->constraints) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->constraints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
252 14
            return $this->query($keys, $constraints)->by($this->distantKey);
0 ignored issues
show
Bug introduced by
The method by() does not exist on Bdf\Prime\Query\QueryInterface. It seems like you code against a sub-type of said class. However, the method does not exist in 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

252
            return $this->query($keys, $constraints)->/** @scrutinizer ignore-call */ by($this->distantKey);
Loading history...
253
        }
254
255 9
        if ($this->relationQuery) {
256 1
            return $this->relationQuery->where($this->distantKey, reset($keys));
257
        }
258
259 8
        $query = $this->distant->queries()->keyValue($this->distantKey, reset($keys));
260
261 8
        if (!$query) {
0 ignored issues
show
introduced by
$query is of type Bdf\Prime\Query\Contract...\KeyValueQueryInterface, thus it always evaluated to true.
Loading history...
262
            return $this->query($keys, $constraints)->by($this->distantKey);
263
        }
264
265 8
        return $this->relationQuery = $query->by($this->distantKey);
0 ignored issues
show
Bug introduced by
The method by() does not exist on Bdf\Prime\Query\Contract...\KeyValueQueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\Contract...\KeyValueQueryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

265
        return $this->relationQuery = $query->/** @scrutinizer ignore-call */ by($this->distantKey);
Loading history...
266
    }
267
268
    /**
269
     * Apply the through constraints
270
     *
271
     * @param Q $query
0 ignored issues
show
Bug introduced by
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...
272
     * @param array $constraints
273
     * @param string|null $context
274
     *
275
     * @return Q
276
     *
277
     * @template Q as ReadCommandInterface<\Bdf\Prime\Connection\ConnectionInterface, object>&\Bdf\Prime\Query\Contract\Whereable
278
     */
279 6
    protected function applyThroughConstraints(ReadCommandInterface $query, $constraints = [], ?string $context = null): ReadCommandInterface
280
    {
281 6
        return $query->where($this->applyContext($context, $constraints + $this->throughConstraints));
0 ignored issues
show
Bug introduced by
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

281
        return $query->/** @scrutinizer ignore-call */ where($this->applyContext($context, $constraints + $this->throughConstraints));
Loading history...
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287
    #[ReadOperation]
288 23
    protected function relations($keys, $with, $constraints, $without): array
289
    {
290 23
        list($constraints, $throughConstraints) = $this->extractConstraints($constraints);
291
292 23
        $throughEntities = [];
293 23
        $throughDistants = [];
294
295 23
        $collection = $this->throughQuery($keys, $throughConstraints)->execute([
296 23
            $this->throughLocal   => $this->throughLocal,
297 23
            $this->throughDistant => $this->throughDistant,
298 23
        ]);
299
300 23
        foreach ($collection as $entity) {
301 23
            $throughLocal   = $entity[$this->throughLocal];
302 23
            $throughDistant = $entity[$this->throughDistant];
303
304 23
            $throughDistants[$throughDistant] = $throughDistant;
305 23
            $throughEntities[$throughLocal][$throughDistant] = $throughDistant;
306
        }
307
308 23
        $relations = $this->relationQuery($throughDistants, $constraints)
309 23
            ->with($with)
0 ignored issues
show
Bug introduced by
The method with() does not exist on Bdf\Prime\Query\Custom\KeyValue\KeyValueQuery. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

309
            ->/** @scrutinizer ignore-call */ with($with)
Loading history...
310 23
            ->without($without)
311 23
            ->all();
312
313 23
        return [
314 23
            'throughEntities' => $throughEntities,
315 23
            'entities'        => $relations,
316 23
        ];
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322 23
    protected function match($collection, $relations): void
323
    {
324 23
        foreach ($relations['throughEntities'] as $key => $throughDistants) {
325 23
            $entities = [];
326
327 23
            foreach ($throughDistants as $throughDistant) {
328 23
                if (isset($relations['entities'][$throughDistant])) {
329 23
                    $entities[] = $relations['entities'][$throughDistant];
330
                }
331
            }
332
333 23
            if (empty($entities)) {
334
                continue;
335
            }
336
337 23
            foreach ($collection[$key] as $local) {
338 23
                $this->setRelation($local, $entities);
339
            }
340
        }
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     *
346
     * @throws PrimeException
347
     */
348
    #[WriteOperation]
349 3
    public function associate($owner, $entity)
350
    {
351 3
        $this->attach($owner, $entity);
352
353 3
        return $owner;
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     *
359
     * @throws PrimeException
360
     */
361
    #[WriteOperation]
362 4
    public function dissociate($owner)
363
    {
364 4
        $this->detach($owner, $this->getRelation($owner));
365
366 4
        return $owner;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     *
372
     * @throws PrimeException
373
     */
374 3
    public function create($owner, array $data = [])
375
    {
376 3
        $entity = $this->distant->entity($data);
377
378 3
        $this->distant->save($entity);
379
380 3
        $this->add($owner, $entity);
381
382 3
        return $entity;
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388
    #[WriteOperation]
389 3
    public function add($owner, $related): int
390
    {
391 3
        return $this->attach($owner, $related);
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397
    #[WriteOperation]
398 5
    public function saveAll($owner, array $relations = []): int
399
    {
400
        //Detach all relations
401 5
        if ($this->saveStrategy === self::SAVE_STRATEGY_REPLACE) {
402 5
            $this->throughQuery($this->getLocalKeyValue($owner))->delete();
403
        }
404
405
        // Attach new relations
406 5
        return $this->attach($owner, $this->getRelation($owner));
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412
    #[WriteOperation]
413 4
    public function deleteAll($owner, array $relations = []): int
414
    {
415 4
        return $this->detach($owner, $this->getRelation($owner));
416
    }
417
418
    /**
419
     * Check whether the owner has a distant entity relation
420
     *
421
     * @param L $owner
0 ignored issues
show
Bug introduced by
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...
422
     * @param string|R $entity
423
     *
424
     * @return boolean
425
     * @throws PrimeException
426
     */
427
    #[ReadOperation]
428 3
    public function has($owner, $entity): bool
429
    {
430 3
        $data = [$this->throughLocal => $this->getLocalKeyValue($owner)];
431
432 3
        if (!is_object($entity)) {
433
            $data[$this->throughDistant] = $entity;
434
        } else {
435 3
            $data[$this->throughDistant] = $this->getDistantKeyValue($entity);
436
        }
437
438 3
        return $this->through->exists($this->through->entity($data));
439
    }
440
441
    /**
442
     * Attach a distant entity to an entity
443
     *
444
     * @param L $owner
445
     * @param string|R[]|R $entities
446
     *
447
     * @return int
448
     * @throws PrimeException
449
     */
450
    #[WriteOperation]
451 14
    public function attach($owner, $entities): int
452
    {
453 14
        if (empty($entities)) {
454
            return 0;
455
        }
456
457 14
        $ownerId = $this->getLocalKeyValue($owner);
458
459 14
        if (!is_array($entities)) {
460 9
            $entities = [$entities];
461
        }
462
463 14
        $nb = 0;
464
465 14
        foreach ($entities as $entity) {
466
            // distant could be a object or the distant id
467 14
            $data = [$this->throughLocal => $ownerId];
468
469 14
            if (!is_object($entity)) {
470
                $data[$this->throughDistant] = $entity;
471
            } else {
472 14
                $data[$this->throughDistant] = $this->getDistantKeyValue($entity);
473
            }
474
475 14
            $nb += $this->through->save($this->through->entity($data));
476
        }
477
478 14
        return $nb;
479
    }
480
481
    /**
482
     * Detach a distant entity of an entity
483
     *
484
     * @param L $owner
485
     * @param string|R[]|R $entities
486
     *
487
     * @return int
488
     * @throws PrimeException
489
     */
490
    #[WriteOperation]
491 11
    public function detach($owner, $entities): int
492
    {
493 11
        if (empty($entities)) {
494
            return 0;
495
        }
496
497 11
        $ownerId = $this->getLocalKeyValue($owner);
498
499 11
        if (!is_array($entities)) {
500 3
            $entities = [$entities];
501
        }
502
503 11
        $nb = 0;
504
505 11
        foreach ($entities as $entity) {
506
            // distant could be a object or the distant id
507 11
            $data = [$this->throughLocal => $ownerId];
508
509 11
            if (!is_object($entity)) {
510
                $data[$this->throughDistant] = $entity;
511
            } else {
512 11
                $data[$this->throughDistant] = $this->getDistantKeyValue($entity);
513
            }
514
515 11
            $nb += $this->through->delete($this->through->entity($data));
516
        }
517
518 11
        return $nb;
519
    }
520
}
521