Issues (590)

src/Relations/Relation.php (2 issues)

1
<?php
2
3
namespace Bdf\Prime\Relations;
4
5
use Bdf\Prime\Collection\Indexer\EntityIndexerInterface;
6
use Bdf\Prime\Exception\PrimeException;
7
use Bdf\Prime\Query\Contract\ReadOperation;
8
use Bdf\Prime\Relations\Util\ForeignKeyRelation;
9
use Bdf\Prime\Repository\RepositoryInterface;
10
use RuntimeException;
11
12
/**
13
 * Relation
14
 *
15
 * @todo Mettre le saveStrategy en trait + supprimer ou deprécier cette classe
16
 *
17
 * @template L as object
18
 * @template R as object
19
 *
20
 * @extends AbstractRelation<L, R>
21
 */
22
abstract class Relation extends AbstractRelation
23
{
24
    /** @use Polymorph<L> */
25
    use Polymorph;
26
    /** @use ForeignKeyRelation<L, R> */
27
    use ForeignKeyRelation;
28
29
    // save strategies
30
    public const SAVE_STRATEGY_REPLACE = 1;
31
    public const SAVE_STRATEGY_ADD = 2;
32
33
    /**
34
     * The save cascade strategy. See constance SAVE_STRATEGY_*
35
     *
36
     * @var int
37
     */
38
    protected $saveStrategy;
39
40
41
    /**
42
     * Set the relation info
43
     *
44
     * @param string $attributeAim  The property name that hold the relation
45
     * @param RepositoryInterface<L> $local
46
     * @param string $localKey
47
     * @param RepositoryInterface<R>|null $distant
48
     * @param string|null $distantKey
49
     */
50 258
    public function __construct(string $attributeAim, RepositoryInterface $local, string $localKey, ?RepositoryInterface $distant = null, ?string $distantKey = null)
51
    {
52 258
        parent::__construct($attributeAim, $local, $distant);
53
54 258
        $this->localKey = $localKey;
55 258
        $this->distantKey = $distantKey;
56
    }
57
58
    //
59
    //----------- options
60
    //
61
62
    /**
63
     * {@inheritdoc}
64
     */
65 259
    public function setOptions(array $options)
66
    {
67 259
        parent::setOptions($options);
68
69 259
        if (!empty($options['saveStrategy'])) {
70 1
            $this->setSaveStrategy($options['saveStrategy']);
71
        }
72
73 259
        if (isset($options['discriminator'])) {
74 100
            $this->setDiscriminator($options['discriminator']);
75
        }
76
77 259
        if (isset($options['discriminatorValue'])) {
78 16
            $this->setDiscriminatorValue($options['discriminatorValue']);
79
        }
80
81 259
        return $this;
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 1
    public function getOptions(): array
88
    {
89 1
        return parent::getOptions() + [
90 1
            'saveStrategy'          => $this->saveStrategy,
91 1
            'discriminator'         => $this->discriminator,
92 1
            'discriminatorValue'    => $this->discriminatorValue,
93 1
        ];
94
    }
95
96
    /**
97
     * Set the save strategy
98
     *
99
     * @param int $strategy
100
     *
101
     * @return $this
102
     */
103 2
    public function setSaveStrategy(int $strategy)
104
    {
105 2
        $this->saveStrategy = $strategy;
106
107 2
        return $this;
108
    }
109
110
    /**
111
     * Get the save strategy
112
     *
113
     * @return int
114
     */
115 1
    public function getSaveStrategy(): int
116
    {
117 1
        return $this->saveStrategy;
118
    }
119
120
    //
121
    //------------ methods for loading relation
122
    //
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    #[ReadOperation]
128 233
    public function load(EntityIndexerInterface $collection, array $with = [], $constraints = [], array $without = []): void
129
    {
130 233
        if ($collection->empty()) {
131 26
            return;
132
        }
133
134 233
        $indexed = $collection->by($this->localKey);
135
136 233
        $this->match($indexed, $this->relations(array_keys($indexed), $with, $constraints, $without));
137
    }
138
139
    /**
140
     * Get the entities
141
     *
142
     * @param array $keys
143
     * @param array $with
144
     * @param array $constraints
145
     * @param array $without
146
     *
147
     * @return array  Entities
148
     * @throws PrimeException
149
     */
150
    #[ReadOperation]
151
    abstract protected function relations($keys, $with, $constraints, $without): array;
152
153
    /**
154
     * Set the relation in a collection of entities
155
     *
156
     * @param L[][] $collection
157
     * @param array $relations
158
     */
159
    abstract protected function match($collection, $relations): void;
160
161
    /**
162
     * Get defined relation
163
     *
164
     * Build object relation defined by user
165
     *
166
     * @param RepositoryInterface<T> $repository
167
     * @param string              $relationName
168
     * @param array               $relationMeta
169
     *
170
     * @return RelationInterface<T, object>
171
     *
172
     * @throws RuntimeException If relation type does not exist
173
     *
174
     * @template T as object
175
     */
176 260
    public static function make(RepositoryInterface $repository, string $relationName, array $relationMeta): RelationInterface
177
    {
178 260
        switch ($relationMeta['type']) {
179
            case RelationInterface::BELONGS_TO:
180 126
                $relation = new BelongsTo(
181 126
                    $relationName,
182 126
                    $repository,
183 126
                    $relationMeta['localKey'],
184 126
                    $repository->repository($relationMeta['entity']),
185 126
                    $relationMeta['distantKey']
186 126
                );
187 126
                break;
188
189
            case RelationInterface::HAS_ONE:
190 38
                $relation = new HasOne(
191 38
                    $relationName,
192 38
                    $repository,
193 38
                    $relationMeta['localKey'],
194 38
                    $repository->repository($relationMeta['entity']),
195 38
                    $relationMeta['distantKey']
196 38
                );
197 38
                break;
198
199
            case RelationInterface::HAS_MANY:
200 73
                $relation = new HasMany(
201 73
                    $relationName,
202 73
                    $repository,
203 73
                    $relationMeta['localKey'],
204 73
                    $repository->repository($relationMeta['entity']),
205 73
                    $relationMeta['distantKey']
206 73
                );
207 73
                break;
208
209
            case RelationInterface::BELONGS_TO_MANY:
210 32
                $relation = new BelongsToMany(
211 32
                    $relationName,
212 32
                    $repository,
213 32
                    $relationMeta['localKey'],
214 32
                    $repository->repository($relationMeta['entity']),
215 32
                    $relationMeta['distantKey']
216 32
                );
217 32
                $relation->setThrough(
218 32
                    $repository->repository($relationMeta['through']),
0 ignored issues
show
It seems like $repository->repository($relationMeta['through']) can also be of type null; however, parameter $through of Bdf\Prime\Relations\BelongsToMany::setThrough() does only seem to accept Bdf\Prime\Repository\RepositoryInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

218
                    /** @scrutinizer ignore-type */ $repository->repository($relationMeta['through']),
Loading history...
219 32
                    $relationMeta['throughLocal'],
220 32
                    $relationMeta['throughDistant']
221 32
                );
222 32
                break;
223
224
            case RelationInterface::BY_INHERITANCE:
225 31
                $relation = new ByInheritance(
226 31
                    $relationName,
227 31
                    $repository,
228 31
                    $relationMeta['localKey']
229 31
                );
230 31
                break;
231
232
            case RelationInterface::MORPH_TO:
233 84
                $relation = new MorphTo(
234 84
                    $relationName,
235 84
                    $repository,
236 84
                    $relationMeta['localKey']
237 84
                );
238 84
                $relation->setMap($relationMeta['map']);
239 84
                break;
240
241
            case RelationInterface::CUSTOM:
242 2
                $relation = $relationMeta['relationClass']::make($repository, $relationName, $relationMeta);
243 2
                break;
244
245
            default:
246
                throw new RuntimeException('Unknown type from relation "' . $relationName . '" in ' . $repository->entityName());
247
        }
248
249 260
        return $relation->setOptions($relationMeta);
250
    }
251
252
    /**
253
     * Create the array of relation
254
     *
255
     * [relation => constraints] becomes [relation => ['constraints' => constraints, 'relations' => subRelations]]
256
     *
257
     * ex
258
     * <code>
259
     *     print_r(Relation::sanitizeRelations([
260
     *         'customer.packs' => ['enabled' => true],
261
     *     ));
262
     *
263
     *     // echo an array like [
264
     *     //     'customer' => [
265
     *     //         'constraints' => [],
266
     *     //         'relations'   => ['packs' => ['enabled' => true]],
267
     *     //     ]
268
     *     // ]
269
     * </code>
270
     *
271
     * @param array $relations
272
     *
273
     * @return array
274
     *
275
     * @todo voir pour intégrer en meta le polymorphism
276
     */
277 610
    public static function sanitizeRelations(array $relations): array
278
    {
279 610
        $sanitized = [];
280
281 610
        foreach ($relations as $name => $constraints) {
282 265
            if (is_int($name)) {
283 156
                $name = $constraints;
284 156
                $constraints = [];
285
            }
286
287
            // relation deja declaré: on ajoute ecrase les constraints
288
            // cas d'appel: ['foo.bar', 'foo' => constraints]
289 265
            if (isset($sanitized[$name])) {
290 1
                $sanitized[$name]['constraints'] = $constraints;
291 1
                continue;
292
            }
293
294 265
            $relations = [];
295
296 265
            list($name, $nested) = self::parseRelationName($name);
297
298
            // nested relation
299 265
            if ($nested) {
300
                // la relation existe deja, on ajoute la nouvelle relation
301
                // cas d'appel ['foo.bar1', 'foo.bar2' => constraints]
302 66
                if (isset($sanitized[$name])) {
303 17
                    $sanitized[$name]['relations'][$nested] = $constraints;
304 17
                    continue;
305
                }
306
307 65
                $relations[$nested] = $constraints;
308 65
                $constraints = [];
309
            }
310
311
            // declaration d'une relation à charger
312 265
            $sanitized[$name] = [
313 265
                'constraints' => $constraints,
314 265
                'relations'   => $relations,
315 265
            ];
316
        }
317
318 610
        return $sanitized;
319
    }
320
321
    /**
322
     * Create an array of relations and nested relations that must be discarded
323
     *
324
     * @param array $relations
325
     *
326
     * @return array
327
     */
328 240
    public static function sanitizeWithoutRelations(array $relations): array
329
    {
330 240
        $sanitized = [];
331
332 240
        foreach ($relations as $name) {
333 45
            list($name, $nested) = self::parseRelationName($name);
334
335 45
            if (!isset($sanitized[$name])) {
336 45
                $sanitized[$name] = [];
337
            }
338
339 45
            if ($nested) {
340 5
                $sanitized[$name][] = $nested;
341
            }
342
        }
343
344 240
        return $sanitized;
345
    }
346
347
    /**
348
     * Parse the relation name to find whether a nested relation is defined or not
349
     *
350
     * @param string $name
351
     *
352
     * @return array{0:string,1:string|null}
353
     */
354 281
    public static function parseRelationName(string $name): array
355
    {
356 281
        if (strpos($name, '.') === false) {
357 277
            return [$name, null];
358
        }
359
360 69
        list($name, $relation) = explode('.', $name, 2);
361
362
        // gestion du polymorph de relation
363 69
        $part = explode('#', $name, 2);
364 69
        if (count($part) === 2) {
365 37
            $name = $part[0];
366 37
            $relation = $part[1].'#'.$relation;
367
        }
368
369 69
        return [$name, $relation];
370
    }
371
372
    /**
373
     * Get entity classname and property from a pattern
374
     *
375
     * @param string $pattern
376
     *
377
     * @return array{0:class-string,1:string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:class-string,1:string} at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array{0:class-string,1:string}.
Loading history...
378
     */
379 434
    public static function parseEntity(string $pattern): array
380
    {
381 434
        $parts = explode('::', $pattern, 2);
382
383 434
        return [
384 434
            $parts[0],
385 434
            isset($parts[1]) ? $parts[1] : 'id',
386 434
        ];
387
    }
388
}
389