Passed
Pull Request — 2.1 (#65)
by Vincent
11:10 queued 05:05
created

OneOrMany::fillDistantEntityConstraints()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 19
ccs 12
cts 12
cp 1
rs 9.6111
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 5
1
<?php
2
3
namespace Bdf\Prime\Relations;
4
5
use Bdf\Prime\Query\Contract\EntityJoinable;
6
use Bdf\Prime\Query\Contract\ReadOperation;
7
use Bdf\Prime\Query\Contract\WriteOperation;
8
use Bdf\Prime\Query\ReadCommandInterface;
9
use Bdf\Prime\Repository\RepositoryInterface;
10
use InvalidArgumentException;
11
12
use function array_intersect_key;
13
14
/**
15
 * OneOrMany
16
 *
17
 * @package Bdf\Prime\Relations
18
 *
19
 * @todo possibilité de désactiver les constraints globales
20
 *
21
 * @template L as object
22
 * @template R as object
23
 *
24
 * @extends Relation<L, R>
25
 */
26
abstract class OneOrMany extends Relation
27
{
28
    /**
29
     * Default attributes to fill when creating a new relation entity
30
     *
31
     * @var array<string, mixed>|null
32
     */
33
    private ?array $defaultValues = null;
34
35
    /**
36
     * {@inheritdoc}
37
     */
38 84
    public function relationRepository(): RepositoryInterface
39
    {
40 84
        return $this->distant;
41
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 240
    protected function applyConstraints(ReadCommandInterface $query, $constraints = [], $context = null): ReadCommandInterface
47
    {
48 240
        parent::applyConstraints($query, $constraints, $context);
49
50
        // Si le dépot distant possède la foreign key, on estime qu'il possède le discriminator
51
        // On applique donc la contrainte de relation sur le discriminitor
52 240
        if ($this->isPolymorphic() && $this->isForeignKeyBarrier($this->distant->entityClass())) {
53 9
            $query->where(
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

53
            $query->/** @scrutinizer ignore-call */ 
54
                    where(
Loading history...
54 9
                $this->applyContext($context, [$this->discriminator => $this->discriminatorValue])
55 9
            );
56
        }
57
58 240
        return $query;
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 77
    public function join(EntityJoinable $query, string $alias): void
65
    {
66
        // @fixme ?
67
//        if ($alias === null) {
68
//            $alias = $this->attributeAim;
69
//        }
70
71 77
        $query->joinEntity($this->distant->entityName(), $this->distantKey, $this->getLocalAlias($query).$this->localKey, $alias);
72
73
        // apply relation constraints
74 77
        $this->applyConstraints($query, [], '$'.$alias);
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 63
    public function joinRepositories(EntityJoinable $query, string $alias, $discriminator = null): array
81
    {
82 63
        return [
83 63
            $alias => $this->relationRepository()
84 63
        ];
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    #[ReadOperation]
91 216
    protected function relations($keys, $with, $constraints, $without): array
92
    {
93
        /** @var R[] */
94 216
        return $this->relationQuery($keys, $constraints)
95 216
            ->with($with)
0 ignored issues
show
Bug introduced by
The method with() 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

95
            ->/** @scrutinizer ignore-call */ with($with)
Loading history...
96 216
            ->without($without)
97 216
            ->all();
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103 213
    protected function match($collection, $relations): void
104
    {
105 213
        foreach ($relations as $key => $distant) {
106 203
            foreach ($collection[$key] as $local) {
107 203
                $this->setRelation($local, $distant);
108
            }
109
        }
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 28
    public function link($owner): ReadCommandInterface
116
    {
117 28
        return $this->query($this->getLocalKeyValue($owner));
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 13
    public function associate($owner, $entity)
124
    {
125 13
        if (!$this->isForeignKeyBarrier($owner)) {
126 6
            throw new InvalidArgumentException('The local entity is not the foreign key barrier.');
127
        }
128
129 7
        if ($this->isPolymorphic()) {
130 4
            $this->discriminatorValue = $this->discriminator(get_class($entity));
131
        }
132
133 7
        $this->setForeignKeyValue($owner, $this->getDistantKeyValue($entity));
134 7
        $this->setRelation($owner, $entity);
135
136 7
        return $owner;
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 13
    public function dissociate($owner)
143
    {
144 13
        if (!$this->isForeignKeyBarrier($owner)) {
145 6
            throw new InvalidArgumentException('The local entity is not the foreign key barrier.');
146
        }
147
148 7
        if ($this->isPolymorphic()) {
149 4
            $this->discriminatorValue = null;
150
        }
151
152
        // TODO Dont update key if it is embedded in the relation object
153 7
        $this->setForeignKeyValue($owner, null);
154 7
        $this->setRelation($owner, null);
155
156 7
        return $owner;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 17
    public function create($owner, array $data = [])
163
    {
164 17
        if ($this->isForeignKeyBarrier($owner)) {
165 6
            throw new InvalidArgumentException('The local entity is not the primary key barrier.');
166
        }
167
168 11
        $entity = $this->distant->entity($data);
169
170 11
        $this->fillDistantEntityConstraints($entity, $this->getLocalKeyValue($owner));
171
172 11
        return $entity;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    #[WriteOperation]
179
    public function add($owner, $related): int
180
    {
181
        if ($this->isForeignKeyBarrier($owner)) {
182
            throw new InvalidArgumentException('The local entity is not the primary key barrier.');
183
        }
184
185
        $this->fillDistantEntityConstraints($related, $this->getLocalKeyValue($owner));
186
187
        return $this->distant->save($related);
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    #[WriteOperation]
194 31
    public function saveAll($owner, array $relations = []): int
195
    {
196 31
        $entities = $this->getRelation($owner);
197
198 31
        if (empty($entities)) {
199 4
            return 0;
200
        }
201
202 27
        $id = $this->getLocalKeyValue($owner);
203
204
        //Detach all relations
205 27
        if ($this->saveStrategy === self::SAVE_STRATEGY_REPLACE) {
206 4
            $this->query($id)->delete();
207
        }
208
209 27
        if (!is_array($entities)) {
210 23
            $entities = [$entities];
211
        }
212
213
        // Save new relations
214 27
        $nb = 0;
215
216 27
        foreach ($entities as $entity) {
217 27
            $this->fillDistantEntityConstraints($entity, $id);
218 27
            $nb += $this->distant->saveAll($entity, $relations);
219
        }
220
221 27
        return (int) $nb;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    #[WriteOperation]
228 25
    public function deleteAll($owner, array $relations = []): int
229
    {
230 25
        $entities = $this->getRelation($owner);
231
232 25
        if (empty($entities)) {
233 2
            return 0;
234
        }
235
236 24
        if (!is_array($entities)) {
237 19
            $entities = [$entities];
238
        }
239
240 24
        $nb = 0;
241
242 24
        foreach ($entities as $entity) {
243 24
            $nb += $this->distant->deleteAll($entity, $relations);
244
        }
245
246 24
        return (int) $nb;
247
    }
248
249
    /**
250
     * Get the repository that owns the foreign key and the key name
251
     *
252
     * @return array{0:RepositoryInterface,1:string}
253
     */
254
    abstract protected function getForeignInfos(): array;
255
256
    /**
257
     * Get the query used to load relations
258
     *
259
     * @param array $keys The owner keys
260
     * @param array $constraints Constraints to apply on the query
261
     *
262
     * @return ReadCommandInterface
263
     */
264
    abstract protected function relationQuery($keys, $constraints): ReadCommandInterface;
265
266
    /**
267
     * Check if the entity is the foreign key barrier
268
     *
269
     * @param string|object $entity
270
     *
271
     * @return bool
272
     */
273 163
    private function isForeignKeyBarrier($entity): bool
274
    {
275 163
        list($repository) = $this->getForeignInfos();
276
277 163
        if (!is_string($entity)) {
278 43
            $entity = get_class($entity);
279
        }
280
281 163
        return $repository->entityClass() === $entity;
282
    }
283
284
    /**
285
     * Set the foreign key value on an entity
286
     *
287
     * @param object $entity
288
     * @param mixed  $id
289
     */
290 52
    private function setForeignKeyValue($entity, $id): void
291
    {
292
        /**
293
         * @var RepositoryInterface $repository
294
         * @var string $key
295
         */
296 52
        list($repository, $key) = $this->getForeignInfos();
297
298 52
        if ($repository->entityClass() === get_class($entity)) {
299 33
            $repository->mapper()->hydrateOne($entity, $key, $id);
300
301 33
            if ($this->isPolymorphic()) {
302 11
                $repository->mapper()->hydrateOne($entity, $this->discriminator, $this->discriminatorValue);
303
            }
304
        }
305
    }
306
307
    /**
308
     * Fill the distant entity with default values and foreign key
309
     *
310
     * @param object $entity The distant entity
311
     * @param mixed $id The foreign key value
312
     *
313
     * @return void
314
     */
315 38
    private function fillDistantEntityConstraints(object $entity, $id): void
316
    {
317 38
        $this->setForeignKeyValue($entity, $id);
318
319 38
        if (!$this->constraints || !$this->distant) {
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...
320 34
            return;
321
        }
322
323 4
        if (($values = $this->defaultValues) === null) {
324 3
            $values = $this->defaultValues = array_intersect_key(
325 3
                $this->constraints,
326 3
                $this->distant->metadata()->attributes
327 3
            );
328
        }
329
330 4
        $mapper = $this->distant->mapper();
331
332 4
        foreach ($values as $key => $value) {
333 4
            $mapper->hydrateOne($entity, $key, $value);
334
        }
335
    }
336
}
337