Issues (131)

src/RepositoryProxy.php (4 issues)

1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\EntityRepository;
8
use Doctrine\Persistence\ObjectRepository;
9
10
/**
11
 * @mixin EntityRepository<TProxiedObject>
12
 * @template TProxiedObject of object
13
 *
14
 * @author Kevin Bond <[email protected]>
15
 */
16
final class RepositoryProxy implements ObjectRepository, \IteratorAggregate, \Countable
17
{
18
    /** @var ObjectRepository<TProxiedObject>|EntityRepository<TProxiedObject> */
19
    private $repository;
20
21 400
    public function __construct(ObjectRepository $repository)
22
    {
23 400
        $this->repository = $repository;
24 400
    }
25
26 10
    public function __call(string $method, array $arguments)
27
    {
28 10
        return $this->proxyResult($this->repository->{$method}(...$arguments));
29
    }
30
31 140
    public function count(): int
32
    {
33 140
        if ($this->repository instanceof EntityRepository) {
34
            // use query to avoid loading all entities
35 140
            return $this->repository->count([]);
36
        }
37
38
        if ($this->repository instanceof \Countable) {
39
            return \count($this->repository);
40
        }
41
42
        return \count($this->findAll());
43
    }
44
45 10
    public function getIterator(): \Traversable
46
    {
47
        // TODO: $this->repository is set to ObjectRepository, which is not
48
        //       iterable. Can this every be another RepositoryProxy?
49 10
        if (\is_iterable($this->repository)) {
50
            return yield from $this->repository;
51
        }
52
53 10
        yield from $this->findAll();
54 10
    }
55
56
    /**
57
     * @deprecated use RepositoryProxy::count()
58
     */
59 10
    public function getCount(): int
60
    {
61 10
        trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using RepositoryProxy::getCount() is deprecated, use RepositoryProxy::count() (it is now Countable).');
62
63 10
        return $this->count();
64
    }
65
66 40
    public function assert(): RepositoryAssertions
67
    {
68 40
        return new RepositoryAssertions($this);
69
    }
70
71 120
    /**
72
     * @deprecated use RepositoryProxy::assert()->empty()
73 120
     */
74
    public function assertEmpty(string $message = ''): self
75 120
    {
76
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertEmpty() is deprecated, use RepositoryProxy::assert()->empty().');
77
78 10
        $this->assert()->empty($message);
79
80 10
        return $this;
81
    }
82 10
83
    /**
84
     * @deprecated use RepositoryProxy::assert()->count()
85 10
     */
86
    public function assertCount(int $expectedCount, string $message = ''): self
87 10
    {
88
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCount() is deprecated, use RepositoryProxy::assert()->count().');
89 10
90
        $this->assert()->count($expectedCount, $message);
91
92 10
        return $this;
93
    }
94 10
95
    /**
96 10
     * @deprecated use RepositoryProxy::assert()->countGreaterThan()
97
     */
98
    public function assertCountGreaterThan(int $expected, string $message = ''): self
99 10
    {
100
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThan() is deprecated, use RepositoryProxy::assert()->countGreaterThan().');
101 10
102
        $this->assert()->countGreaterThan($expected, $message);
103 10
104
        return $this;
105
    }
106
107
    /**
108
     * @deprecated use RepositoryProxy::assert()->countGreaterThanOrEqual()
109 20
     */
110
    public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self
111 20
    {
112
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThanOrEqual() is deprecated, use RepositoryProxy::assert()->countGreaterThanOrEqual().');
113 20
114
        $this->assert()->countGreaterThanOrEqual($expected, $message);
115
116
        return $this;
117
    }
118
119 10
    /**
120
     * @deprecated use RepositoryProxy::assert()->countLessThan()
121 10
     */
122
    public function assertCountLessThan(int $expected, string $message = ''): self
123 10
    {
124
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThan() is deprecated, use RepositoryProxy::assert()->countLessThan().');
125
126
        $this->assert()->countLessThan($expected, $message);
127
128
        return $this;
129
    }
130
131 20
    /**
132
     * @deprecated use RepositoryProxy::assert()->countLessThanOrEqual()
133 20
     */
134
    public function assertCountLessThanOrEqual(int $expected, string $message = ''): self
135
    {
136
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThanOrEqual() is deprecated, use RepositoryProxy::assert()->countLessThanOrEqual().');
137
138
        $this->assert()->countLessThanOrEqual($expected, $message);
139
140
        return $this;
141 20
    }
142
143 20
    /**
144
     * @deprecated use RepositoryProxy::assert()->exists()
145
     */
146
    public function assertExists($criteria, string $message = ''): self
147
    {
148
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertExists() is deprecated, use RepositoryProxy::assert()->exists().');
149 10
150
        $this->assert()->exists($criteria, $message);
151 10
152
        return $this;
153 10
    }
154 10
155
    /**
156 10
     * @deprecated use RepositoryProxy::assert()->notExists()
157
     */
158
    public function assertNotExists($criteria, string $message = ''): self
159
    {
160
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertNotExists() is deprecated, use RepositoryProxy::assert()->notExists().');
161
162
        $this->assert()->notExists($criteria, $message);
163
164
        return $this;
165
    }
166
167
    /**
168
     * @return Proxy&TProxiedObject|null
169
     *
170
     * @psalm-return Proxy<TProxiedObject>|null
171
     */
172
    public function first(string $sortedField = 'id'): ?Proxy
173
    {
174
        return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null;
175 40
    }
176
177 40
    /**
178
     * @return Proxy&TProxiedObject|null
179
     *
180
     * @psalm-return Proxy<TProxiedObject>|null
181
     */
182
    public function last(string $sortedField = 'id'): ?Proxy
183
    {
184
        return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null;
185
    }
186
187
    /**
188
     * Remove all rows.
189
     */
190 80
    public function truncate(): void
191
    {
192 80
        /** @var DocumentManager $om */
193 10
        $om = Factory::configuration()->objectManagerFor($this->getClassName());
194
195
        if ($om instanceof EntityManagerInterface) {
196 70
            $om->createQuery("DELETE {$this->getClassName()} e")->execute();
197
198
            return;
199
        }
200
201
        if ($om instanceof DocumentManager) {
0 ignored issues
show
$om is always a sub-type of Doctrine\ODM\MongoDB\DocumentManager.
Loading history...
202
            $om->getDocumentCollection($this->getClassName())->deleteMany([]);
203
        }
204
    }
205
206
    /**
207
     * Fetch one random object.
208
     *
209
     * @param array $attributes The findBy criteria
210
     *
211 120
     * @return Proxy&TProxiedObject
212
     *
213 120
     * @throws \RuntimeException if no objects are persisted
214 10
     *
215
     * @psalm-return Proxy<TProxiedObject>
216
     */
217 110
    public function random(array $attributes = []): Proxy
218 10
    {
219
        return $this->randomSet(1, $attributes)[0];
220
    }
221 100
222
    /**
223 100
     * Fetch a random set of objects.
224
     *
225 100
     * @param int   $number     The number of objects to return
226 30
     * @param array $attributes The findBy criteria
227
     *
228
     * @return Proxy[]|object[]
229 70
     *
230
     * @throws \RuntimeException         if not enough persisted objects to satisfy the number requested
231
     * @throws \InvalidArgumentException if number is less than zero
232
     */
233
    public function randomSet(int $number, array $attributes = []): array
234
    {
235
        if ($number < 0) {
236
            throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number));
237
        }
238
239
        return $this->randomRange($number, $number, $attributes);
240 40
    }
241
242 40
    /**
243 10
     * Fetch a random range of objects.
244
     *
245
     * @param int   $min        The minimum number of objects to return
246 40
     * @param int   $max        The maximum number of objects to return
247 10
     * @param array $attributes The findBy criteria
248
     *
249
     * @return Proxy[]|object[]
250 40
     *
251
     * @throws \RuntimeException         if not enough persisted objects to satisfy the max
252
     * @throws \InvalidArgumentException if min is less than zero
253
     * @throws \InvalidArgumentException if max is less than min
254
     */
255
    public function randomRange(int $min, int $max, array $attributes = []): array
256 120
    {
257
        if ($min < 0) {
258 120
            throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min));
259
        }
260
261
        if ($max < $min) {
262
            throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min));
263
        }
264 30
265
        $all = \array_values($this->findBy($attributes));
266 30
267
        \shuffle($all);
268
269
        if (\count($all) < $max) {
270
            throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all)));
271
        }
272
273
        return \array_slice($all, 0, \random_int($min, $max));
274
    }
275
276
    /**
277
     * @param object|array|mixed $criteria
278 90
     *
279
     * @return Proxy&TProxiedObject|null
280 90
     *
281 50
     * @psalm-param Proxy<TProxiedObject>|array|mixed $criteria
282
     * @psalm-return Proxy<TProxiedObject>|null
283 50
     * @psalm-suppress ParamNameMismatch
284 40
     */
285
    public function find($criteria)
286
    {
287
        if ($criteria instanceof Proxy) {
288 90
            $criteria = $criteria->object();
289 50
        }
290 20
291
        if (!\is_array($criteria)) {
292
            return $this->proxyResult($this->repository->find($criteria));
293 50
        }
294
295
        return $this->findOneBy($criteria);
296
    }
297
298
    /**
299 190
     * @return Proxy[]|object[]
300
     */
301 190
    public function findAll(): array
302
    {
303
        return $this->proxyResult($this->repository->findAll());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->proxyResul...>repository->findAll()) could return the type Zenstruck\Foundry\Proxy which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
304
    }
305
306
    /**
307
     * @return Proxy[]|object[]
308
     */
309
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
310
    {
311
        return $this->proxyResult($this->repository->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->proxyResul...erBy, $limit, $offset)) could return the type Zenstruck\Foundry\Proxy which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
312
    }
313
314
    /**
315 200
     * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter
316
     *
317 200
     * @return Proxy&TProxiedObject|null
318 170
     *
319
     * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter
320
     *
321 150
     * @psalm-return Proxy<TProxiedObject>|null
322 140
     */
323
    public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy
324
    {
325 10
        if (\is_array($orderBy)) {
326
            $wrappedParams = (new \ReflectionClass($this->repository))->getMethod('findOneBy')->getParameters();
327
328 120
            if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) {
329
                throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', \get_class($this->repository)));
330 120
            }
331
        }
332 40
333 120
        $result = $this->repository->findOneBy(self::normalizeCriteria($criteria), $orderBy);
0 ignored issues
show
The call to Doctrine\Persistence\ObjectRepository::findOneBy() has too many arguments starting with $orderBy. ( Ignorable by Annotation )

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

333
        /** @scrutinizer ignore-call */ 
334
        $result = $this->repository->findOneBy(self::normalizeCriteria($criteria), $orderBy);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
334 120
        if (null === $result) {
335
            return null;
336
        }
337
338
        return $this->proxyResult($result);
339
    }
340
341
    /**
342
     * @psalm-return class-string<TProxiedObject>
343
     */
344
    public function getClassName(): string
345
    {
346
        return $this->repository->getClassName();
347
    }
348
349
    /**
350
     * @param mixed $result
351
     *
352
     * @return Proxy|Proxy[]|object|object[]|mixed
353
     *
354
     * @psalm-suppress InvalidReturnStatement
355
     * @psalm-suppress InvalidReturnType
356
     * @template TResult of object
357
     * @psalm-param TResult|list<TResult> $result
358
     * @psalm-return ($result is array ? list<Proxy<TResult>> : Proxy<TResult>)
359
     */
360
    private function proxyResult($result)
361
    {
362
        if (\is_a($result, $this->getClassName())) {
363
            return Proxy::createFromPersisted($result);
364
        }
365
366
        if (\is_array($result)) {
367
            return \array_map([$this, 'proxyResult'], $result);
368
        }
369
370
        return $result;
371
    }
372
373
    private static function normalizeCriteria(array $criteria): array
374
    {
375
        return \array_map(
376
            function($value) {
377
                return $value instanceof Proxy ? $value->object() : $value;
378
            },
379
            $criteria
380
        );
381
    }
382
}
383