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
introduced
by
![]() |
|||||||
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
|
|||||||
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
|
|||||||
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
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. ![]() |
|||||||
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 |