Issues (590)

src/Collection/EntityCollection.php (2 issues)

1
<?php
2
3
namespace Bdf\Prime\Collection;
4
5
use Bdf\Prime\Collection\Indexer\EntityIndexer;
6
use Bdf\Prime\Connection\ConnectionInterface;
7
use Bdf\Prime\Entity\ImportableInterface;
8
use Bdf\Prime\Exception\PrimeException;
9
use Bdf\Prime\Query\Contract\ReadOperation;
10
use Bdf\Prime\Query\Contract\WriteOperation;
11
use Bdf\Prime\Query\QueryInterface;
12
use Bdf\Prime\Relations\Relation;
13
use Bdf\Prime\Repository\EntityRepository;
14
use Bdf\Prime\Repository\RepositoryEventsSubscriberInterface;
15
use Bdf\Prime\Repository\RepositoryInterface;
16
use Bdf\Prime\Repository\Write\BufferedWriter;
17
use IteratorAggregate;
18
19
/**
20
 * Collection of entities
21
 *
22
 * Can process optimized bulk operations, like delete, update, or load
23
 * Provide helpers method for bulk entities handling, like save, refresh, link, query
24
 *
25
 * <code>
26
 * $collection = Entity::collection(); // Create the collection
27
 * $collection->push($entity); // Add an entity
28
 * $collection->save(); // Save all entities
29
 * $collection->update(['value' => 42]); // Update attribute "value" to 42
30
 * </code>
31
 *
32
 * @todo Optimize bulk insert query
33
 *
34
 * @template E as object
35
 *
36
 * @implements CollectionInterface<E>
37
 * @implements IteratorAggregate<array-key, E>
38
 */
39
class EntityCollection implements IteratorAggregate, CollectionInterface, ImportableInterface
40
{
41
    /**
42
     * @var RepositoryInterface<E>
43
     */
44
    private $repository;
45
46
    /**
47
     * @var CollectionInterface<E>
48
     */
49
    private $storage;
50
51
52
    /**
53
     * RelationCollection constructor.
54
     *
55
     * @param RepositoryInterface<E> $repository The entity repository
56
     * @param CollectionInterface<E>|E[]|null $storage
57
     *
58
     * @internal Should not be created manually
59
     */
60 58
    public function __construct(RepositoryInterface $repository, $storage = null)
61
    {
62 58
        $this->repository = $repository;
63
64 58
        $this->storage = $storage instanceof CollectionInterface ? $storage : new ArrayCollection($storage);
65
    }
66
67
    /**
68
     * Load relations on entities
69
     *
70
     * @param array|string $relations The relations to load. Can be a string for load only one relation
71
     *
72
     * @return $this
73
     * @throws PrimeException
74
     *
75
     * @todo Faut-il utiliser loadIfNotLoaded ?
76
     */
77
    #[ReadOperation]
78 1
    public function load($relations)
79
    {
80 1
        foreach (Relation::sanitizeRelations((array)$relations) as $relationName => $meta) {
81 1
            $this->repository->relation($relationName)->load(
82 1
                EntityIndexer::fromArray($this->repository->mapper(), $this->storage->all()),
83 1
                $meta['relations'],
84 1
                $meta['constraints']
85 1
            );
86
        }
87
88 1
        return $this;
89
    }
90
91
    /**
92
     * Link a query on each elements
93
     *
94
     * This method will configure query like :
95
     * SELECT * FROM relation WHERE relation.fk IN (entity1.key, entity2.key, ...)
96
     *
97
     * <code>
98
     * // Perform query on customer.customerPack.pack
99
     * $customer->relation('packs')
100
     *     ->wrapAs('collection')
101
     *     ->all()
102
     *     ->link('pack')
103
     *     ->where(...)
104
     *     ->all()
105
     * ;
106
     * </code>
107
     *
108
     * @param string $relation The relation name
109
     *
110
     * @return QueryInterface
111
     * @fixme Works with Polymorph
112
     */
113 2
    public function link($relation)
114
    {
115 2
        return $this->repository
116 2
            ->relation($relation)
117 2
            ->link($this->all())
118 2
        ;
119
    }
120
121
    /**
122
     * Get the query, related to all entities
123
     *
124
     * This method will configure query like :
125
     * SELECT * FROM entity WHERE pk IN (entity1.pk, entity2.pk, ...)
126
     *
127
     * @return QueryInterface<ConnectionInterface, E>
128
     */
129 5
    public function query()
130
    {
131 5
        return $this->repository->queries()->entities($this->all());
132
    }
133
134
    /**
135
     * Delete all entities in the collection
136
     *
137
     * /!\ The collection will not be cleared. The deleted entities can still be used
138
     *
139
     * @return $this
140
     * @throws PrimeException
141
     */
142
    #[WriteOperation]
143 3
    public function delete()
144
    {
145 3
        $this->repository->transaction(function (RepositoryInterface $repository) {
146
            /** @var RepositoryInterface&RepositoryEventsSubscriberInterface $repository */
147 3
            $writer = new BufferedWriter($repository);
148
149 3
            foreach ($this as $entity) {
150 3
                $writer->delete($entity);
151
            }
152
153 3
            $writer->flush();
154 3
        });
155
156 3
        return $this;
157
    }
158
159
    /**
160
     * Save all entities in the collection
161
     *
162
     * @return $this
163
     * @throws PrimeException
164
     */
165
    #[WriteOperation]
166 1
    public function save()
167
    {
168 1
        $this->repository->transaction(function (RepositoryInterface $repository) {
169 1
            foreach ($this as $entity) {
170 1
                $repository->save($entity);
171
            }
172 1
        });
173
174 1
        return $this;
175
    }
176
177
    /**
178
     * Save entities and its relations
179
     * /!\ Not optimized
180
     *
181
     * @param string|array $relations The relations names
182
     *
183
     * @return int
184
     * @throws PrimeException
185
     */
186
    #[WriteOperation]
187 1
    public function saveAll($relations)
188
    {
189 1
        $relations = Relation::sanitizeRelations((array)$relations);
190
191 1
        return $this->repository->transaction(function (RepositoryInterface $repository) use ($relations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->repository...ion(...) { /* ... */ }) returns the type Bdf\Prime\Repository\R which is incompatible with the documented return type integer.
Loading history...
192 1
            $nb = 0;
193
194 1
            foreach ($this as $entity) {
195 1
                $nb += $repository->save($entity);
196
197 1
                foreach ($relations as $relationName => $info) {
198 1
                    $nb += $repository->relation($relationName)->saveAll($entity, $info['relations']);
199
                }
200
            }
201
202 1
            return $nb;
203 1
        });
204
    }
205
206
    /**
207
     * Delete entities and its relations
208
     * /!\ Not optimized
209
     *
210
     * @param string|array $relations The relations names
211
     *
212
     * @return int
213
     * @throws PrimeException
214
     */
215
    #[WriteOperation]
216 1
    public function deleteAll($relations)
217
    {
218 1
        $relations = Relation::sanitizeRelations((array)$relations);
219
220 1
        return $this->repository->transaction(function (RepositoryInterface $repository) use ($relations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->repository...ion(...) { /* ... */ }) returns the type Bdf\Prime\Repository\R which is incompatible with the documented return type integer.
Loading history...
221 1
            $nb = 0;
222
223 1
            foreach ($this as $entity) {
224 1
                $nb += $repository->delete($entity);
225
226 1
                foreach ($relations as $relationName => $info) {
227 1
                    $nb += $repository->relation($relationName)->deleteAll($entity, $info['relations']);
228
                }
229
            }
230
231 1
            return $nb;
232 1
        });
233
    }
234
235
    /**
236
     * Perform an update on all entities
237
     * Update in database AND entities attributes
238
     *
239
     * Example:
240
     * <code>
241
     * $tasks = Task::wrapAs('collection')->limit(10)->all();
242
     * // $tasks = [
243
     * //    new Task(['id' => 1, 'processing' => false]),
244
     * //    new Task(['id' => 2, 'processing' => false]),
245
     * //    new Task(['id' => 3, 'processing' => false]),
246
     * // ];
247
     *
248
     * $tasks->update(['proccessing' => true]);
249
     * // $tasks = [
250
     * //    new Task(['id' => 1, 'processing' => true]),
251
     * //    new Task(['id' => 2, 'processing' => true]),
252
     * //    new Task(['id' => 3, 'processing' => true]),
253
     * // ];
254
     * </code>
255
     *
256
     * @param array $data Data to set (in form [attribute] => [value])
257
     *
258
     * @return $this
259
     * @throws PrimeException
260
     */
261
    #[WriteOperation]
262 1
    public function update(array $data)
263
    {
264 1
        foreach ($this as $entity) {
265 1
            $entity->import($data);
266
        }
267
268 1
        $this->query()->update($data);
269
270 1
        return $this;
271
    }
272
273
    /**
274
     * Refresh all entities into the collection
275
     *
276
     * This method is equivalent to re-select all entities
277
     *
278
     * /!\ Do not refresh each entities, but the entire collection. Do not store references if you want to refresh the collection
279
     *
280
     * @return $this
281
     * @throws PrimeException
282
     */
283
    #[ReadOperation]
284 2
    public function refresh()
285
    {
286 2
        $this->pushAll($this->query()->all());
287
288 2
        return $this;
289
    }
290
291
    /**
292
     * Get the related repository
293
     *
294
     * @return RepositoryInterface<E>
295
     */
296 13
    public function repository()
297
    {
298 13
        return $this->repository;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     *
304
     * Replace all entities of the collection with imported data
305
     * Entities will be instantiated with given data
306
     *
307
     * <code>
308
     * Person::collection()->import([
309
     *     ['name' => 'John'],
310
     *     ['name' => 'Mark'],
311
     * ]);
312
     * // [ new Person(['name' => 'John']),
313
     * //   new Person(['name' => 'Mark']) ]
314
     * </code>
315
     */
316 8
    public function import(array $data): void
317
    {
318 8
        $entities = [];
319
320 8
        foreach ($data as $value) {
321 8
            if (is_array($value)) {
322 7
                $entities[] = $this->repository->entity($value);
323
            } else {
324 1
                $entities[] = $value;
325
            }
326
        }
327
328 8
        $this->pushAll($entities);
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334 4
    public function export(array $attributes = []): array
335
    {
336 4
        $data = [];
337
338 4
        foreach ($this as $entity) {
339 4
            $data[] = $entity->export($attributes);
340
        }
341
342 4
        return $data;
343
    }
344
345
    //===================//
346
    // Delegated methods //
347
    //===================//
348
349
    /**
350
     * {@inheritdoc}
351
     */
352 11
    public function pushAll(array $items)
353
    {
354 11
        $this->storage->pushAll($items);
355
356 11
        return $this;
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362 1
    public function push($item)
363
    {
364 1
        $this->storage->push($item);
365
366 1
        return $this;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     */
372 1
    public function put($key, $item)
373
    {
374 1
        $this->storage->put($key, $item);
375
376 1
        return $this;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382 20
    public function all()
383
    {
384 20
        return $this->storage->all();
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390 6
    public function get($key, $default = null)
391
    {
392 6
        return $this->storage->get($key, $default);
393
    }
394
395
    /**
396
     * {@inheritdoc}
397
     */
398 1
    public function has($key)
399
    {
400 1
        return $this->storage->has($key);
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     */
406 1
    public function remove($key)
407
    {
408 1
        $this->storage->remove($key);
409
410 1
        return $this;
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     */
416 1
    public function clear()
417
    {
418 1
        $this->storage->clear();
419
420 1
        return $this;
421
    }
422
423
    /**
424
     * {@inheritdoc}
425
     */
426 1
    public function keys()
427
    {
428 1
        return $this->storage->keys();
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434 2
    public function isEmpty()
435
    {
436 2
        return $this->storage->isEmpty();
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     *
442
     * @psalm-suppress InvalidReturnType
443
     * @psalm-suppress InvalidArgument
444
     */
445 1
    public function map($callback)
446
    {
447
        // @fixme does return static make sense ?
448
        /** @psalm-suppress InvalidReturnStatement */
449 1
        return new static($this->repository, $this->storage->map($callback));
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455 1
    public function filter($callback = null)
456
    {
457 1
        return new static($this->repository, $this->storage->filter($callback));
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463 1
    public function groupBy($groupBy, $mode = self::GROUPBY)
464
    {
465 1
        return new static($this->repository, $this->storage->groupBy($groupBy, $mode));
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471 1
    public function contains($element)
472
    {
473 1
        return $this->storage->contains($element);
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479 1
    public function indexOf($value, $strict = false)
480
    {
481 1
        return $this->storage->indexOf($value, $strict);
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487 1
    public function merge($items)
488
    {
489 1
        return new static($this->repository, $this->storage->merge($items));
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     */
495 1
    public function sort(callable $callback = null)
496
    {
497 1
        return new static($this->repository, $this->storage->sort($callback));
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503 2
    public function toArray()
504
    {
505 2
        return $this->storage->toArray();
506
    }
507
508
    /**
509
     * {@inheritdoc}
510
     */
511 1
    public function offsetExists($offset): bool
512
    {
513 1
        return $this->storage->offsetExists($offset);
514
    }
515
516
    /**
517
     * {@inheritdoc}
518
     */
519
    #[\ReturnTypeWillChange]
520 3
    public function offsetGet($offset)
521
    {
522 3
        return $this->storage->offsetGet($offset);
523
    }
524
525
    /**
526
     * {@inheritdoc}
527
     */
528 1
    public function offsetSet($offset, $value): void
529
    {
530 1
        $this->storage->offsetSet($offset, $value);
531
    }
532
533
    /**
534
     * {@inheritdoc}
535
     */
536 1
    public function offsetUnset($offset): void
537
    {
538 1
        $this->storage->offsetUnset($offset);
539
    }
540
541
    /**
542
     * {@inheritdoc}
543
     */
544 5
    public function count(): int
545
    {
546 5
        return $this->storage->count();
547
    }
548
549
    /**
550
     * {@inheritdoc}
551
     */
552 12
    public function getIterator(): \Iterator
553
    {
554 12
        return new \ArrayIterator($this->storage->all());
555
    }
556
}
557