vendor/zenstruck/foundry/src/Persistence/ProxyRepositoryDecorator.php line 436

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the zenstruck/foundry package.
  4. *
  5. * (c) Kevin Bond <kevinbond@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Zenstruck\Foundry\Persistence;
  11. use Doctrine\ODM\MongoDB\DocumentManager;
  12. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Doctrine\ORM\EntityRepository;
  15. use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata;
  16. use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
  17. use Doctrine\Persistence\Mapping\MappingException;
  18. use Doctrine\Persistence\ObjectManager;
  19. use Doctrine\Persistence\ObjectRepository;
  20. use Symfony\Component\PropertyAccess\PropertyAccess;
  21. use Zenstruck\Foundry\Factory;
  22. use Zenstruck\Foundry\Proxy as ProxyObject;
  23. /**
  24. * @mixin EntityRepository<TProxiedObject>
  25. * @extends RepositoryDecorator<TProxiedObject>
  26. * @template TProxiedObject of object
  27. *
  28. * @author Kevin Bond <kevinbond@gmail.com>
  29. *
  30. * @final
  31. */
  32. class ProxyRepositoryDecorator extends RepositoryDecorator
  33. {
  34. /**
  35. * @return list<Proxy<TProxiedObject>>|Proxy<TProxiedObject>
  36. */
  37. public function __call(string $method, array $arguments)
  38. {
  39. return $this->proxyResult($this->inner()->{$method}(...$arguments));
  40. }
  41. public function getIterator(): \Traversable
  42. {
  43. // TODO: $this->inner() is set to ObjectRepository, which is not
  44. // iterable. Can this every be another RepositoryDecorator?
  45. if (\is_iterable($this->inner())) {
  46. return yield from $this->inner();
  47. }
  48. yield from $this->findAll();
  49. }
  50. /**
  51. * @deprecated use RepositoryDecorator::count()
  52. */
  53. public function getCount(): int
  54. {
  55. trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using RepositoryDecorator::getCount() is deprecated, use RepositoryDecorator::count() (it is now Countable).');
  56. return $this->count();
  57. }
  58. /**
  59. * @deprecated use RepositoryDecorator::assert()->empty()
  60. */
  61. public function assertEmpty(string $message = ''): self
  62. {
  63. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertEmpty() is deprecated, use RepositoryDecorator::assert()->empty().');
  64. $this->assert()->empty($message);
  65. return $this;
  66. }
  67. /**
  68. * @deprecated use RepositoryDecorator::assert()->count()
  69. */
  70. public function assertCount(int $expectedCount, string $message = ''): self
  71. {
  72. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCount() is deprecated, use RepositoryDecorator::assert()->count().');
  73. $this->assert()->count($expectedCount, $message);
  74. return $this;
  75. }
  76. /**
  77. * @deprecated use RepositoryDecorator::assert()->countGreaterThan()
  78. */
  79. public function assertCountGreaterThan(int $expected, string $message = ''): self
  80. {
  81. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountGreaterThan() is deprecated, use RepositoryDecorator::assert()->countGreaterThan().');
  82. $this->assert()->countGreaterThan($expected, $message);
  83. return $this;
  84. }
  85. /**
  86. * @deprecated use RepositoryDecorator::assert()->countGreaterThanOrEqual()
  87. */
  88. public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self
  89. {
  90. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountGreaterThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countGreaterThanOrEqual().');
  91. $this->assert()->countGreaterThanOrEqual($expected, $message);
  92. return $this;
  93. }
  94. /**
  95. * @deprecated use RepositoryDecorator::assert()->countLessThan()
  96. */
  97. public function assertCountLessThan(int $expected, string $message = ''): self
  98. {
  99. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountLessThan() is deprecated, use RepositoryDecorator::assert()->countLessThan().');
  100. $this->assert()->countLessThan($expected, $message);
  101. return $this;
  102. }
  103. /**
  104. * @deprecated use RepositoryDecorator::assert()->countLessThanOrEqual()
  105. */
  106. public function assertCountLessThanOrEqual(int $expected, string $message = ''): self
  107. {
  108. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountLessThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countLessThanOrEqual().');
  109. $this->assert()->countLessThanOrEqual($expected, $message);
  110. return $this;
  111. }
  112. /**
  113. * @deprecated use RepositoryDecorator::assert()->exists()
  114. * @phpstan-param Proxy<TProxiedObject>|array|mixed $criteria
  115. */
  116. public function assertExists($criteria, string $message = ''): self
  117. {
  118. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertExists() is deprecated, use RepositoryDecorator::assert()->exists().');
  119. $this->assert()->exists($criteria, $message);
  120. return $this;
  121. }
  122. /**
  123. * @deprecated use RepositoryDecorator::assert()->notExists()
  124. * @phpstan-param Proxy<TProxiedObject>|array|mixed $criteria
  125. */
  126. public function assertNotExists($criteria, string $message = ''): self
  127. {
  128. trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertNotExists() is deprecated, use RepositoryDecorator::assert()->notExists().');
  129. $this->assert()->notExists($criteria, $message);
  130. return $this;
  131. }
  132. /**
  133. * @return (Proxy&TProxiedObject)|null
  134. *
  135. * @phpstan-return Proxy<TProxiedObject>|null
  136. */
  137. public function first(string $sortedField = 'id'): ?Proxy
  138. {
  139. return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null;
  140. }
  141. /**
  142. * @return (Proxy&TProxiedObject)|null
  143. *
  144. * @phpstan-return Proxy<TProxiedObject>|null
  145. */
  146. public function last(string $sortedField = 'id'): ?Proxy
  147. {
  148. return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null;
  149. }
  150. /**
  151. * Remove all rows.
  152. */
  153. public function truncate(): void
  154. {
  155. $om = $this->getObjectManager();
  156. if ($om instanceof EntityManagerInterface) {
  157. $om->createQuery("DELETE {$this->getClassName()} e")->execute();
  158. return;
  159. }
  160. if ($om instanceof DocumentManager) {
  161. $om->getDocumentCollection($this->getClassName())->deleteMany([]);
  162. }
  163. }
  164. /**
  165. * Fetch one random object.
  166. *
  167. * @param array $attributes The findBy criteria
  168. *
  169. * @return Proxy&TProxiedObject
  170. *
  171. * @throws \RuntimeException if no objects are persisted
  172. *
  173. * @phpstan-return Proxy<TProxiedObject>
  174. */
  175. public function random(array $attributes = []): Proxy
  176. {
  177. return $this->randomSet(1, $attributes)[0];
  178. }
  179. /**
  180. * Fetch a random set of objects.
  181. *
  182. * @param int $number The number of objects to return
  183. * @param array $attributes The findBy criteria
  184. *
  185. * @return list<Proxy<TProxiedObject>>
  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. public function randomSet(int $number, array $attributes = []): array
  191. {
  192. if ($number < 0) {
  193. throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number));
  194. }
  195. return $this->randomRange($number, $number, $attributes);
  196. }
  197. /**
  198. * Fetch a random range of objects.
  199. *
  200. * @param int $min The minimum number of objects to return
  201. * @param int $max The maximum number of objects to return
  202. * @param array $attributes The findBy criteria
  203. *
  204. * @return list<Proxy<TProxiedObject>>
  205. *
  206. * @throws \RuntimeException if not enough persisted objects to satisfy the max
  207. * @throws \InvalidArgumentException if min is less than zero
  208. * @throws \InvalidArgumentException if max is less than min
  209. */
  210. public function randomRange(int $min, int $max, array $attributes = []): array
  211. {
  212. if ($min < 0) {
  213. throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min));
  214. }
  215. if ($max < $min) {
  216. throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min));
  217. }
  218. $all = \array_values($this->findBy($attributes));
  219. \shuffle($all);
  220. if (\count($all) < $max) {
  221. throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all)));
  222. }
  223. return \array_slice($all, 0, \random_int($min, $max)); // @phpstan-ignore-line
  224. }
  225. /**
  226. * @param object|array|mixed $criteria
  227. *
  228. * @return (Proxy&TProxiedObject)|null
  229. *
  230. * @phpstan-param Proxy<TProxiedObject>|array|mixed $criteria
  231. * @phpstan-return Proxy<TProxiedObject>|null
  232. */
  233. public function find($criteria)
  234. {
  235. if ($criteria instanceof Proxy) {
  236. $criteria = $criteria->_real();
  237. }
  238. if (!\is_array($criteria)) {
  239. /** @var TProxiedObject|null $result */
  240. $result = $this->inner()->find($criteria);
  241. return $this->proxyResult($result);
  242. }
  243. $normalizedCriteria = [];
  244. $propertyAccessor = PropertyAccess::createPropertyAccessor();
  245. foreach ($criteria as $attributeName => $attributeValue) {
  246. if (!\is_object($attributeValue)) {
  247. $normalizedCriteria[$attributeName] = $attributeValue;
  248. continue;
  249. }
  250. if ($attributeValue instanceof Factory) {
  251. $attributeValue = $attributeValue->withoutPersisting()->createAndUnproxify();
  252. } elseif ($attributeValue instanceof Proxy) {
  253. $attributeValue = $attributeValue->_real();
  254. }
  255. try {
  256. $metadataForAttribute = $this->getObjectManager()->getClassMetadata($attributeValue::class);
  257. } catch (MappingException|ORMMappingException) {
  258. $normalizedCriteria[$attributeName] = $attributeValue;
  259. continue;
  260. }
  261. $isEmbedded = match ($metadataForAttribute::class) {
  262. ORMClassMetadata::class => $metadataForAttribute->isEmbeddedClass,
  263. ODMClassMetadata::class => $metadataForAttribute->isEmbeddedDocument,
  264. default => throw new \LogicException(\sprintf('Metadata class %s is not supported.', $metadataForAttribute::class)),
  265. };
  266. // it's a regular entity
  267. if (!$isEmbedded) {
  268. $normalizedCriteria[$attributeName] = $attributeValue;
  269. continue;
  270. }
  271. foreach ($metadataForAttribute->getFieldNames() as $field) {
  272. $embeddableFieldValue = $propertyAccessor->getValue($attributeValue, $field);
  273. if (\is_object($embeddableFieldValue)) {
  274. throw new \InvalidArgumentException('Nested embeddable objects are still not supported in "find()" method.');
  275. }
  276. $normalizedCriteria["{$attributeName}.{$field}"] = $embeddableFieldValue;
  277. }
  278. }
  279. return $this->findOneBy($normalizedCriteria);
  280. }
  281. /**
  282. * @return list<Proxy<TProxiedObject>>
  283. */
  284. public function findAll(): array
  285. {
  286. return $this->proxyResult($this->inner()->findAll());
  287. }
  288. /**
  289. * @param int|null $limit
  290. * @param int|null $offset
  291. *
  292. * @return list<Proxy<TProxiedObject>>
  293. */
  294. public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
  295. {
  296. return $this->proxyResult($this->inner()->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset));
  297. }
  298. /**
  299. * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter
  300. *
  301. * @return (Proxy&TProxiedObject)|null
  302. *
  303. * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter
  304. *
  305. * @phpstan-return Proxy<TProxiedObject>|null
  306. */
  307. public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy
  308. {
  309. if (null !== $orderBy) {
  310. trigger_deprecation('zenstruck\foundry', '1.38.0', 'Argument "$orderBy" of method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::findBy()" instead if you need an order.', __METHOD__, __CLASS__);
  311. }
  312. if (\is_array($orderBy)) {
  313. $wrappedParams = (new \ReflectionClass($this->inner()))->getMethod('findOneBy')->getParameters();
  314. if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) {
  315. throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', $this->inner()::class));
  316. }
  317. }
  318. /** @var TProxiedObject|null $result */
  319. $result = $this->inner()->findOneBy(self::normalizeCriteria($criteria), $orderBy); // @phpstan-ignore-line
  320. if (null === $result) {
  321. return null;
  322. }
  323. return $this->proxyResult($result);
  324. }
  325. /**
  326. * @return class-string<TProxiedObject>
  327. */
  328. public function getClassName(): string
  329. {
  330. return $this->inner()->getClassName();
  331. }
  332. /**
  333. * @param TProxiedObject|list<TProxiedObject>|null $result
  334. *
  335. * @return Proxy|Proxy[]|object|object[]|mixed
  336. *
  337. * @phpstan-return ($result is array ? list<Proxy<TProxiedObject>> : Proxy<TProxiedObject>)
  338. */
  339. private function proxyResult(mixed $result)
  340. {
  341. if (\is_array($result)) {
  342. return \array_map(fn(mixed $o): mixed => $this->proxyResult($o), $result);
  343. }
  344. if ($result && \is_a($result, $this->getClassName())) {
  345. return ProxyObject::createFromPersisted($result);
  346. }
  347. return $result;
  348. }
  349. private static function normalizeCriteria(array $criteria): array
  350. {
  351. return \array_map(
  352. static fn($value) => $value instanceof Proxy ? $value->_real() : $value,
  353. $criteria,
  354. );
  355. }
  356. private function getObjectManager(): ObjectManager
  357. {
  358. return Factory::configuration()->objectManagerFor($this->getClassName());
  359. }
  360. }
  361. \class_exists(\Zenstruck\Foundry\RepositoryProxy::class);