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 <[email protected]>
  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 <[email protected]>
  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 $expectedCountstring $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 $expectedstring $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 $expectedstring $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 $expectedstring $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 $expectedstring $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($criteriastring $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($criteriastring $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 $minint $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($all0\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);