Passed
Pull Request — master (#123)
by Kevin
02:26
created

RepositoryProxy::assertCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
rs 10
ccs 3
cts 3
cp 1
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\ORM\EntityRepository;
7
use Doctrine\Persistence\ObjectRepository;
8
9
/**
10
 * @mixin EntityRepository<TProxiedObject>
11
 * @template TProxiedObject of object
12
 *
13
 * @author Kevin Bond <[email protected]>
14
 */
15
final class RepositoryProxy implements ObjectRepository, \IteratorAggregate, \Countable
16
{
17
    /** @var ObjectRepository<TProxiedObject>|EntityRepository<TProxiedObject> */
18
    private $repository;
19
20
    public function __construct(ObjectRepository $repository)
21 400
    {
22
        $this->repository = $repository;
23 400
    }
24 400
25
    public function __call(string $method, array $arguments)
26 10
    {
27
        return $this->proxyResult($this->repository->{$method}(...$arguments));
28 10
    }
29
30
    public function count(): int
31 140
    {
32
        if ($this->repository instanceof EntityRepository) {
33 140
            // use query to avoid loading all entities
34
            return $this->repository->count([]);
35 140
        }
36
37
        if ($this->repository instanceof \Countable) {
38
            return \count($this->repository);
39
        }
40
41
        return \count($this->findAll());
42
    }
43
44
    public function getIterator(): \Traversable
45 10
    {
46
        // TODO: $this->repository is set to ObjectRepository, which is not
47
        //       iterable. Can this every be another RepositoryProxy?
48
        if (\is_iterable($this->repository)) {
49 10
            return yield from $this->repository;
50
        }
51
52
        yield from $this->findAll();
53 10
    }
54 10
55
    /**
56
     * @deprecated use RepositoryProxy::count()
57
     */
58
    public function getCount(): int
59 10
    {
60
        trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using RepositoryProxy::getCount() is deprecated, use RepositoryProxy::count() (it is now Countable).');
61 10
62
        return $this->count();
63 10
    }
64
65
    public function assert(): RepositoryAssertions
66 40
    {
67
        return new RepositoryAssertions($this);
68 40
    }
69
70
    /**
71 120
     * @deprecated use RepositoryProxy::assert()->empty()
72
     */
73 120
    public function assertEmpty(string $message = ''): self
74
    {
75 120
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertEmpty() is deprecated, use RepositoryProxy::assert()->empty().');
76
77
        $this->assert()->empty($message);
78 10
79
        return $this;
80 10
    }
81
82 10
    /**
83
     * @deprecated use RepositoryProxy::assert()->count()
84
     */
85 10
    public function assertCount(int $expectedCount, string $message = ''): self
86
    {
87 10
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCount() is deprecated, use RepositoryProxy::assert()->count().');
88
89 10
        $this->assert()->count($expectedCount, $message);
90
91
        return $this;
92 10
    }
93
94 10
    /**
95
     * @deprecated use RepositoryProxy::assert()->countGreaterThan()
96 10
     */
97
    public function assertCountGreaterThan(int $expected, string $message = ''): self
98
    {
99 10
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThan() is deprecated, use RepositoryProxy::assert()->countGreaterThan().');
100
101 10
        $this->assert()->countGreaterThan($expected, $message);
102
103 10
        return $this;
104
    }
105
106
    /**
107
     * @deprecated use RepositoryProxy::assert()->countGreaterThanOrEqual()
108
     */
109 20
    public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self
110
    {
111 20
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThanOrEqual() is deprecated, use RepositoryProxy::assert()->countGreaterThanOrEqual().');
112
113 20
        $this->assert()->countGreaterThanOrEqual($expected, $message);
114
115
        return $this;
116
    }
117
118
    /**
119 10
     * @deprecated use RepositoryProxy::assert()->countLessThan()
120
     */
121 10
    public function assertCountLessThan(int $expected, string $message = ''): self
122
    {
123 10
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThan() is deprecated, use RepositoryProxy::assert()->countLessThan().');
124
125
        $this->assert()->countLessThan($expected, $message);
126
127
        return $this;
128
    }
129
130
    /**
131 20
     * @deprecated use RepositoryProxy::assert()->countLessThanOrEqual()
132
     */
133 20
    public function assertCountLessThanOrEqual(int $expected, string $message = ''): self
134
    {
135
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThanOrEqual() is deprecated, use RepositoryProxy::assert()->countLessThanOrEqual().');
136
137
        $this->assert()->countLessThanOrEqual($expected, $message);
138
139
        return $this;
140
    }
141 20
142
    /**
143 20
     * @deprecated use RepositoryProxy::assert()->exists()
144
     */
145
    public function assertExists($criteria, string $message = ''): self
146
    {
147
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertExists() is deprecated, use RepositoryProxy::assert()->exists().');
148
149 10
        $this->assert()->exists($criteria, $message);
150
151 10
        return $this;
152
    }
153 10
154 10
    /**
155
     * @deprecated use RepositoryProxy::assert()->notExists()
156 10
     */
157
    public function assertNotExists($criteria, string $message = ''): self
158
    {
159
        trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertNotExists() is deprecated, use RepositoryProxy::assert()->notExists().');
160
161
        $this->assert()->notExists($criteria, $message);
162
163
        return $this;
164
    }
165
166
    /**
167
     * @return Proxy|object|null
168
     *
169
     * @psalm-return Proxy<TProxiedObject>|null
170
     */
171
    public function first(string $sortedField = 'id'): ?Proxy
172
    {
173
        return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null;
174
    }
175 40
176
    /**
177 40
     * @return Proxy|object|null
178
     *
179
     * @psalm-return Proxy<TProxiedObject>|null
180
     */
181
    public function last(string $sortedField = 'id'): ?Proxy
182
    {
183
        return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null;
184
    }
185
186
    /**
187
     * Remove all rows.
188
     */
189
    public function truncate(): void
190 80
    {
191
        $om = Factory::configuration()->objectManagerFor($this->getClassName());
192 80
193 10
        if ($om instanceof EntityManagerInterface) {
194
            $om->createQuery("DELETE {$this->getClassName()} e")->execute();
195
196 70
            return;
197
        }
198
199
        foreach ($this as $object) {
200
            $om->remove($object);
0 ignored issues
show
Bug introduced by
$object of type array is incompatible with the type object expected by parameter $object of Doctrine\Persistence\ObjectManager::remove(). ( Ignorable by Annotation )

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

200
            $om->remove(/** @scrutinizer ignore-type */ $object);
Loading history...
201
        }
202
203
        $om->flush();
204
    }
205
206
    /**
207
     * Fetch one random object.
208
     *
209
     * @param array $attributes The findBy criteria
210
     *
211 120
     * @return Proxy|object
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|object|null
280 90
     *
281 50
     * @psalm-param Proxy<TProxiedObject>|array|mixed $criteria
282
     * @psalm-return Proxy<TProxiedObject>|null
283 50
     */
284 40
    public function find($criteria)
285
    {
286
        if ($criteria instanceof Proxy) {
287
            $criteria = $criteria->object();
288 90
        }
289 50
290 20
        if (!\is_array($criteria)) {
291
            return $this->proxyResult($this->repository->find($criteria));
292
        }
293 50
294
        return $this->findOneBy($criteria);
295
    }
296
297
    /**
298
     * @return Proxy[]|object[]
299 190
     */
300
    public function findAll(): array
301 190
    {
302
        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...
303
    }
304
305
    /**
306
     * @return Proxy[]|object[]
307
     */
308
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
309
    {
310
        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...
311
    }
312
313
    /**
314
     * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter
315 200
     *
316
     * @return Proxy|object|null
317 200
     *
318 170
     * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter
319
     *
320
     * @psalm-return Proxy<TProxiedObject>|null
321 150
     */
322 140
    public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy
323
    {
324
        if (\is_array($orderBy)) {
325 10
            $wrappedParams = (new \ReflectionClass($this->repository))->getMethod('findOneBy')->getParameters();
326
327
            if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) {
328 120
                throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', \get_class($this->repository)));
329
            }
330 120
        }
331
332 40
        $result = $this->repository->findOneBy(self::normalizeCriteria($criteria), $orderBy);
0 ignored issues
show
Unused Code introduced by
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

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