Passed
Pull Request — master (#95)
by Wouter
26:49 queued 06:39
created

RepositoryProxy::findOneBy()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 8
c 2
b 0
f 0
dl 0
loc 16
ccs 6
cts 6
cp 1
rs 8.8333
cc 7
nc 5
nop 2
crap 7
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\ORM\EntityRepository;
7
use Doctrine\Persistence\ObjectRepository;
8
use PHPUnit\Framework\Assert;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Assert was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 assertEmpty(string $message = ''): self
67
    {
68 40
        return $this->assertCount(0, $message);
69
    }
70
71 120
    public function assertCount(int $expectedCount, string $message = ''): self
72
    {
73 120
        Assert::assertSame($expectedCount, $this->count(), $message);
74
75 120
        return $this;
76
    }
77
78 10
    public function assertCountGreaterThan(int $expected, string $message = ''): self
79
    {
80 10
        Assert::assertGreaterThan($expected, $this->count(), $message);
81
82 10
        return $this;
83
    }
84
85 10
    public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self
86
    {
87 10
        Assert::assertGreaterThanOrEqual($expected, $this->count(), $message);
88
89 10
        return $this;
90
    }
91
92 10
    public function assertCountLessThan(int $expected, string $message = ''): self
93
    {
94 10
        Assert::assertLessThan($expected, $this->count(), $message);
95
96 10
        return $this;
97
    }
98
99 10
    public function assertCountLessThanOrEqual(int $expected, string $message = ''): self
100
    {
101 10
        Assert::assertLessThanOrEqual($expected, $this->count(), $message);
102
103 10
        return $this;
104
    }
105
106
    /**
107
     * @param object|array|mixed $criteria
108
     */
109 20
    public function assertExists($criteria, string $message = ''): self
110
    {
111 20
        Assert::assertNotNull($this->find($criteria), $message);
112
113 20
        return $this;
114
    }
115
116
    /**
117
     * @param object|array|mixed $criteria
118
     */
119 10
    public function assertNotExists($criteria, string $message = ''): self
120
    {
121 10
        Assert::assertNull($this->find($criteria), $message);
122
123 10
        return $this;
124
    }
125
126
    /**
127
     * @return Proxy|object|null
128
     *
129
     * @psalm-return Proxy<TProxiedObject>|null
130
     */
131 20
    public function first(string $sortedField = 'id'): ?Proxy
132
    {
133 20
        return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null;
134
    }
135
136
    /**
137
     * @return Proxy|object|null
138
     *
139
     * @psalm-return Proxy<TProxiedObject>|null
140
     */
141 20
    public function last(string $sortedField = 'id'): ?Proxy
142
    {
143 20
        return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null;
144
    }
145
146
    /**
147
     * Remove all rows.
148
     */
149 10
    public function truncate(): void
150
    {
151 10
        $om = Factory::configuration()->objectManagerFor($this->getClassName());
152
153 10
        if ($om instanceof EntityManagerInterface) {
154 10
            $om->createQuery("DELETE {$this->getClassName()} e")->execute();
155
156 10
            return;
157
        }
158
159
        foreach ($this as $object) {
160
            $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

160
            $om->remove(/** @scrutinizer ignore-type */ $object);
Loading history...
161
        }
162
163
        $om->flush();
164
    }
165
166
    /**
167
     * Fetch one random object.
168
     *
169
     * @return Proxy|object
170
     *
171
     * @throws \RuntimeException if no objects are persisted
172
     *
173
     * @psalm-return Proxy<TProxiedObject>
174
     */
175 40
    public function random(): Proxy
176
    {
177 40
        return $this->randomSet(1)[0];
178
    }
179
180
    /**
181
     * Fetch a random set of objects.
182
     *
183
     * @param int $number The number of objects to return
184
     *
185
     * @return Proxy[]|object[]
186
     *
187
     * @throws \RuntimeException         if not enough persisted objects to satisfy the number requested
188
     * @throws \InvalidArgumentException if number is less than zero
189
     */
190 80
    public function randomSet(int $number): array
191
    {
192 80
        if ($number < 0) {
193 10
            throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number));
194
        }
195
196 70
        return $this->randomRange($number, $number);
197
    }
198
199
    /**
200
     * Fetch a random range of objects.
201
     *
202
     * @param int $min The minimum number of objects to return
203
     * @param int $max The maximum number of objects to return
204
     *
205
     * @return Proxy[]|object[]
206
     *
207
     * @throws \RuntimeException         if not enough persisted objects to satisfy the max
208
     * @throws \InvalidArgumentException if min is less than zero
209
     * @throws \InvalidArgumentException if max is less than min
210
     */
211 120
    public function randomRange(int $min, int $max): array
212
    {
213 120
        if ($min < 0) {
214 10
            throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min));
215
        }
216
217 110
        if ($max < $min) {
218 10
            throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min));
219
        }
220
221 100
        $all = \array_values($this->findAll());
222
223 100
        \shuffle($all);
224
225 100
        if (\count($all) < $max) {
226 30
            throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all)));
227
        }
228
229 70
        return \array_slice($all, 0, \random_int($min, $max));
230
    }
231
232
    /**
233
     * @param object|array|mixed $criteria
234
     *
235
     * @return Proxy|object|null
236
     *
237
     * @psalm-param Proxy<TProxiedObject>|array|mixed $criteria
238
     * @psalm-return Proxy<TProxiedObject>|list<Proxy<TProxiedObject>>|null
239
     */
240 40
    public function find($criteria)
241
    {
242 40
        if ($criteria instanceof Proxy) {
243 10
            $criteria = $criteria->object();
244
        }
245
246 40
        if (!\is_array($criteria)) {
247 10
            return $this->proxyResult($this->repository->find($criteria));
248
        }
249
250 40
        return $this->findOneBy($criteria);
251
    }
252
253
    /**
254
     * @return Proxy[]|object[]
255
     */
256 120
    public function findAll(): array
257
    {
258 120
        return $this->proxyResult($this->repository->findAll());
259
    }
260
261
    /**
262
     * @return Proxy[]|object[]
263
     */
264 30
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
265
    {
266 30
        return $this->proxyResult($this->repository->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset));
267
    }
268
269
    /**
270
     * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter
271
     *
272
     * @return Proxy|object|null
273
     *
274
     * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter
275
     *
276
     * @psalm-return Proxy<TProxiedObject>|null
277
     */
278 90
    public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy
279
    {
280 90
        if (\is_array($orderBy)) {
281 50
            $wrappedParams = (new \ReflectionClass($this->repository))->getMethod('findOneBy')->getParameters();
282
283 50
            if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !$wrappedParams[1]->getType() instanceof \ReflectionNamedType || 'array' !== $wrappedParams[1]->getType()->getName()) {
284 40
                throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', \get_class($this->repository)));
285
            }
286
        }
287
288 90
        $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

288
        /** @scrutinizer ignore-call */ 
289
        $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...
289
        if (null === $result) {
290
            return null;
291
        }
292
293
        return $this->proxyResult($result);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->proxyResult($result) could return the type Zenstruck\Foundry\Proxy[]|array<mixed,object> which is incompatible with the type-hinted return Zenstruck\Foundry\Proxy|null. Consider adding an additional type-check to rule them out.
Loading history...
294 190
    }
295
296 190
    /**
297
     * @psalm-return class-string<TProxiedObject>
298
     */
299
    public function getClassName(): string
300
    {
301
        return $this->repository->getClassName();
302
    }
303
304
    /**
305
     * @param mixed $result
306
     *
307
     * @return Proxy|Proxy[]|object|object[]|mixed
308
     *
309 200
     * @psalm-suppress InvalidReturnStatement
310
     * @psalm-suppress InvalidReturnType
311 200
     * @template TResult of object
312 170
     * @psalm-param TResult|list<TResult> $result
313
     * @psalm-return ($result is array ? list<Proxy<TResult>> : Proxy<TResult>)
314
     */
315 170
    private function proxyResult($result)
316 140
    {
317
        if (\is_object($result) && \is_a($result, $this->getClassName())) {
318
            return Proxy::createFromPersisted($result);
319 30
        }
320
321
        if (\is_array($result)) {
322 120
            return \array_map([$this, 'proxyResult'], $result);
323
        }
324 120
325
        return $result;
326 40
    }
327 120
328 120
    private static function normalizeCriteria(array $criteria): array
329
    {
330
        return \array_map(
331
            function($value) {
332
                return $value instanceof Proxy ? $value->object() : $value;
333
            },
334
            $criteria
335
        );
336
    }
337
}
338