vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Query/Query.php line 375

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ODM\MongoDB\Query;
  4. use BadMethodCallException;
  5. use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort;
  6. use Doctrine\ODM\MongoDB\DocumentManager;
  7. use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
  8. use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
  9. use Doctrine\ODM\MongoDB\Iterator\Iterator;
  10. use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
  11. use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
  12. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
  13. use Doctrine\ODM\MongoDB\MongoDBException;
  14. use Doctrine\ODM\MongoDB\UnitOfWork;
  15. use InvalidArgumentException;
  16. use IteratorAggregate;
  17. use MongoDB\Collection;
  18. use MongoDB\DeleteResult;
  19. use MongoDB\Driver\ReadPreference;
  20. use MongoDB\InsertOneResult;
  21. use MongoDB\Operation\FindOneAndUpdate;
  22. use MongoDB\UpdateResult;
  23. use Traversable;
  24. use UnexpectedValueException;
  25. use function array_combine;
  26. use function array_filter;
  27. use function array_flip;
  28. use function array_intersect_key;
  29. use function array_keys;
  30. use function array_map;
  31. use function array_merge;
  32. use function array_values;
  33. use function is_array;
  34. use function is_callable;
  35. use function is_string;
  36. use function key;
  37. use function reset;
  38. /**
  39.  * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
  40.  * and to hydrate the raw arrays of data to Doctrine document objects.
  41.  *
  42.  * @psalm-type QueryShape = array{
  43.  *     distinct?: string,
  44.  *     hint?: string|array<string, -1|1>,
  45.  *     limit?: int,
  46.  *     multiple?: bool,
  47.  *     new?: bool,
  48.  *     newObj?: array<string, mixed>,
  49.  *     query?: array<string, mixed>,
  50.  *     readPreference?: ReadPreference,
  51.  *     select?: array<string, 0|1|array<string, mixed>>,
  52.  *     skip?: int,
  53.  *     sort?: array<string, -1|1|SortMeta>,
  54.  *     type?: Query::TYPE_*,
  55.  *     upsert?: bool,
  56.  * }
  57.  * @psalm-import-type Hints from UnitOfWork
  58.  * @psalm-import-type SortMeta from Sort
  59.  */
  60. final class Query implements IteratorAggregate
  61. {
  62.     public const TYPE_FIND            1;
  63.     public const TYPE_FIND_AND_UPDATE 2;
  64.     public const TYPE_FIND_AND_REMOVE 3;
  65.     public const TYPE_INSERT          4;
  66.     public const TYPE_UPDATE          5;
  67.     public const TYPE_REMOVE          6;
  68.     public const TYPE_DISTINCT        9;
  69.     public const TYPE_COUNT           11;
  70.     public const HINT_REFRESH 1;
  71.     // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0
  72.     public const HINT_READ_PREFERENCE 3;
  73.     public const HINT_READ_ONLY       5;
  74.     /**
  75.      * The DocumentManager instance.
  76.      *
  77.      * @var DocumentManager
  78.      */
  79.     private $dm;
  80.     /**
  81.      * The ClassMetadata instance.
  82.      *
  83.      * @var ClassMetadata
  84.      */
  85.     private $class;
  86.     /**
  87.      * Whether to hydrate results as document class instances.
  88.      *
  89.      * @var bool
  90.      */
  91.     private $hydrate true;
  92.     /**
  93.      * Array of primer Closure instances.
  94.      *
  95.      * @var array<string, callable|true|null>
  96.      */
  97.     private $primers = [];
  98.     /** @var bool */
  99.     private $rewindable true;
  100.     /**
  101.      * Hints for UnitOfWork behavior.
  102.      *
  103.      * @var array
  104.      * @psalm-var Hints
  105.      */
  106.     private $unitOfWorkHints = [];
  107.     /**
  108.      * The Collection instance.
  109.      *
  110.      * @var Collection
  111.      */
  112.     protected $collection;
  113.     /**
  114.      * Query structure generated by the Builder class.
  115.      *
  116.      * @var array
  117.      * @psalm-var QueryShape
  118.      */
  119.     private $query;
  120.     /** @var Iterator|null */
  121.     private $iterator;
  122.     /**
  123.      * Query options
  124.      *
  125.      * @var array<string, mixed>
  126.      */
  127.     private $options;
  128.     /**
  129.      * @param QueryShape                        $query
  130.      * @param array<string, mixed>              $options
  131.      * @param array<string, callable|true|null> $primers
  132.      */
  133.     public function __construct(DocumentManager $dmClassMetadata $classCollection $collection, array $query = [], array $options = [], bool $hydrate truebool $refresh false, array $primers = [], bool $readOnly falsebool $rewindable true)
  134.     {
  135.         $primers array_filter($primers);
  136.         switch ($query['type']) {
  137.             case self::TYPE_FIND:
  138.             case self::TYPE_FIND_AND_UPDATE:
  139.             case self::TYPE_FIND_AND_REMOVE:
  140.             case self::TYPE_INSERT:
  141.             case self::TYPE_UPDATE:
  142.             case self::TYPE_REMOVE:
  143.             case self::TYPE_DISTINCT:
  144.             case self::TYPE_COUNT:
  145.                 break;
  146.             default:
  147.                 throw new InvalidArgumentException('Invalid query type: ' $query['type']);
  148.         }
  149.         $this->collection $collection;
  150.         $this->query      $query;
  151.         $this->options    $options;
  152.         $this->dm         $dm;
  153.         $this->class      $class;
  154.         $this->hydrate    $hydrate;
  155.         $this->primers    $primers;
  156.         $this->setReadOnly($readOnly);
  157.         $this->setRefresh($refresh);
  158.         $this->setRewindable($rewindable);
  159.         if (! isset($query['readPreference'])) {
  160.             return;
  161.         }
  162.         $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
  163.     }
  164.     public function __clone()
  165.     {
  166.         $this->iterator null;
  167.     }
  168.     /**
  169.      * Return an array of information about the query structure for debugging.
  170.      *
  171.      * The $name parameter may be used to return a specific key from the
  172.      * internal $query array property. If omitted, the entire array will be
  173.      * returned.
  174.      *
  175.      * @return array<string, mixed>|mixed
  176.      */
  177.     public function debug(?string $name null)
  178.     {
  179.         return $name !== null $this->query[$name] : $this->query;
  180.     }
  181.     /**
  182.      * Execute the query and returns the results.
  183.      *
  184.      * @return Iterator|UpdateResult|InsertOneResult|DeleteResult|array<string, mixed>|object|int|null
  185.      *
  186.      * @throws MongoDBException
  187.      */
  188.     public function execute()
  189.     {
  190.         $results $this->runQuery();
  191.         if (! $this->hydrate) {
  192.             return $results;
  193.         }
  194.         $uow $this->dm->getUnitOfWork();
  195.         /* If a single document is returned from a findAndModify command and it
  196.          * includes the identifier field, attempt hydration.
  197.          */
  198.         if (
  199.             ($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
  200.                 $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
  201.             is_array($results) && isset($results['_id'])
  202.         ) {
  203.             $results $uow->getOrCreateDocument($this->class->name$results$this->unitOfWorkHints);
  204.             if (! empty($this->primers)) {
  205.                 $referencePrimer = new ReferencePrimer($this->dm$uow);
  206.                 foreach ($this->primers as $fieldName => $primer) {
  207.                     $primer is_callable($primer) ? $primer null;
  208.                     $referencePrimer->primeReferences($this->class, [$results], $fieldName$this->unitOfWorkHints$primer);
  209.                 }
  210.             }
  211.         }
  212.         return $results;
  213.     }
  214.     /**
  215.      * Gets the ClassMetadata instance.
  216.      */
  217.     public function getClass(): ClassMetadata
  218.     {
  219.         return $this->class;
  220.     }
  221.     public function getDocumentManager(): DocumentManager
  222.     {
  223.         return $this->dm;
  224.     }
  225.     /**
  226.      * Execute the query and return its result, which must be an Iterator.
  227.      *
  228.      * If the query type is not expected to return an Iterator,
  229.      * BadMethodCallException will be thrown before executing the query.
  230.      * Otherwise, the query will be executed and UnexpectedValueException will
  231.      * be thrown if {@link Query::execute()} does not return an Iterator.
  232.      *
  233.      * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
  234.      *
  235.      * @throws BadMethodCallException If the query type would not return an Iterator.
  236.      * @throws UnexpectedValueException If the query did not return an Iterator.
  237.      * @throws MongoDBException
  238.      */
  239.     public function getIterator(): Iterator
  240.     {
  241.         switch ($this->query['type']) {
  242.             case self::TYPE_FIND:
  243.             case self::TYPE_DISTINCT:
  244.                 break;
  245.             default:
  246.                 throw new BadMethodCallException('Iterator would not be returned for query type: ' $this->query['type']);
  247.         }
  248.         if ($this->iterator === null) {
  249.             $result $this->execute();
  250.             if (! $result instanceof Iterator) {
  251.                 throw new UnexpectedValueException('Iterator was not returned for query type: ' $this->query['type']);
  252.             }
  253.             $this->iterator $result;
  254.         }
  255.         return $this->iterator;
  256.     }
  257.     /**
  258.      * Return the query structure.
  259.      *
  260.      * @return QueryShape
  261.      */
  262.     public function getQuery(): array
  263.     {
  264.         return $this->query;
  265.     }
  266.     /**
  267.      * Execute the query and return the first result.
  268.      *
  269.      * @return array<string, mixed>|object|null
  270.      */
  271.     public function getSingleResult()
  272.     {
  273.         $clonedQuery                 = clone $this;
  274.         $clonedQuery->query['limit'] = 1;
  275.         return $clonedQuery->getIterator()->current() ?: null;
  276.     }
  277.     /**
  278.      * Return the query type.
  279.      */
  280.     public function getType(): int
  281.     {
  282.         return $this->query['type'];
  283.     }
  284.     /**
  285.      * Sets whether or not to hydrate the documents to objects.
  286.      */
  287.     public function setHydrate(bool $hydrate): void
  288.     {
  289.         $this->hydrate $hydrate;
  290.     }
  291.     /**
  292.      * Set whether documents should be registered in UnitOfWork. If document would
  293.      * already be managed it will be left intact and new instance returned.
  294.      *
  295.      * This option has no effect if hydration is disabled.
  296.      */
  297.     public function setReadOnly(bool $readOnly): void
  298.     {
  299.         $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
  300.     }
  301.     /**
  302.      * Set whether to refresh hydrated documents that are already in the
  303.      * identity map.
  304.      *
  305.      * This option has no effect if hydration is disabled.
  306.      */
  307.     public function setRefresh(bool $refresh): void
  308.     {
  309.         $this->unitOfWorkHints[self::HINT_REFRESH] = $refresh;
  310.     }
  311.     /**
  312.      * Set to enable wrapping of resulting Iterator with CachingIterator
  313.      */
  314.     public function setRewindable(bool $rewindable true): void
  315.     {
  316.         $this->rewindable $rewindable;
  317.     }
  318.     /**
  319.      * Execute the query and return its results as an array.
  320.      *
  321.      * @see IteratorAggregate::toArray()
  322.      *
  323.      * @return mixed[]
  324.      */
  325.     public function toArray(): array
  326.     {
  327.         return $this->getIterator()->toArray();
  328.     }
  329.     /**
  330.      * Returns an array containing the specified keys and their values from the
  331.      * query array, provided they exist and are not null.
  332.      *
  333.      * @return array<string, mixed>
  334.      */
  335.     private function getQueryOptions(string ...$keys): array
  336.     {
  337.         return array_filter(
  338.             array_intersect_key($this->queryarray_flip($keys)),
  339.             static function ($value) {
  340.                 return $value !== null;
  341.             }
  342.         );
  343.     }
  344.     /**
  345.      * Decorate the cursor with caching, hydration, and priming behavior.
  346.      *
  347.      * Note: while this method could strictly take a MongoDB\Driver\Cursor, we
  348.      * accept Traversable for testing purposes since Cursor cannot be mocked.
  349.      * HydratingIterator, CachingIterator, and BaseIterator expect a Traversable
  350.      * so this should not have any adverse effects.
  351.      *
  352.      * @param Traversable<mixed> $cursor
  353.      */
  354.     private function makeIterator(Traversable $cursor): Iterator
  355.     {
  356.         if ($this->hydrate) {
  357.             $cursor = new HydratingIterator($cursor$this->dm->getUnitOfWork(), $this->class$this->unitOfWorkHints);
  358.         }
  359.         $cursor $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
  360.         if (! empty($this->primers)) {
  361.             $referencePrimer = new ReferencePrimer($this->dm$this->dm->getUnitOfWork());
  362.             $cursor          = new PrimingIterator($cursor$this->class$referencePrimer$this->primers$this->unitOfWorkHints);
  363.         }
  364.         return $cursor;
  365.     }
  366.     /**
  367.      * Returns an array with its keys renamed based on the translation map.
  368.      *
  369.      * @param array<string, mixed>  $options
  370.      * @param array<string, string> $rename
  371.      *
  372.      * @return array<string, mixed> $rename Translation map (from => to) for renaming keys
  373.      */
  374.     private function renameQueryOptions(array $options, array $rename): array
  375.     {
  376.         if (empty($options)) {
  377.             return $options;
  378.         }
  379.         $options array_combine(
  380.             array_map(
  381.                 static function ($key) use ($rename) {
  382.                     return $rename[$key] ?? $key;
  383.                 },
  384.                 array_keys($options)
  385.             ),
  386.             array_values($options)
  387.         );
  388.         return $options;
  389.     }
  390.     /**
  391.      * Execute the query and return its result.
  392.      *
  393.      * The return value will vary based on the query type. Commands with results
  394.      * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
  395.      * commands and operations may return a status array or a boolean, depending
  396.      * on the driver's write concern. Queries and some mapReduce commands will
  397.      * return an Iterator.
  398.      *
  399.      * @return Iterator|UpdateResult|InsertOneResult|DeleteResult|array<string, mixed>|object|int|null
  400.      */
  401.     private function runQuery()
  402.     {
  403.         $options $this->options;
  404.         switch ($this->query['type']) {
  405.             case self::TYPE_FIND:
  406.                 $queryOptions $this->getQueryOptions('select''sort''skip''limit''readPreference''hint');
  407.                 $queryOptions $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
  408.                 $cursor $this->collection->find(
  409.                     $this->query['query'],
  410.                     array_merge($options$queryOptions)
  411.                 );
  412.                 return $this->makeIterator($cursor);
  413.             case self::TYPE_FIND_AND_UPDATE:
  414.                 $queryOptions                   $this->getQueryOptions('select''sort''upsert');
  415.                 $queryOptions                   $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
  416.                 $queryOptions['returnDocument'] = $this->query['new'] ?? false FindOneAndUpdate::RETURN_DOCUMENT_AFTER FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
  417.                 $operation $this->isFirstKeyUpdateOperator() ? 'findOneAndUpdate' 'findOneAndReplace';
  418.                 return $this->collection->{$operation}(
  419.                     $this->query['query'],
  420.                     $this->query['newObj'],
  421.                     array_merge($options$queryOptions)
  422.                 );
  423.             case self::TYPE_FIND_AND_REMOVE:
  424.                 $queryOptions $this->getQueryOptions('select''sort');
  425.                 $queryOptions $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
  426.                 return $this->collection->findOneAndDelete(
  427.                     $this->query['query'],
  428.                     array_merge($options$queryOptions)
  429.                 );
  430.             case self::TYPE_INSERT:
  431.                 return $this->collection->insertOne($this->query['newObj'], $options);
  432.             case self::TYPE_UPDATE:
  433.                 $multiple $this->query['multiple'] ?? false;
  434.                 if ($this->isFirstKeyUpdateOperator()) {
  435.                     $operation 'updateOne';
  436.                 } else {
  437.                     if ($multiple) {
  438.                         throw new InvalidArgumentException('Combining the "multiple" option without using an update operator as first operation in a query is not supported.');
  439.                     }
  440.                     $operation 'replaceOne';
  441.                 }
  442.                 if ($multiple) {
  443.                     return $this->collection->updateMany(
  444.                         $this->query['query'],
  445.                         $this->query['newObj'],
  446.                         array_merge($options$this->getQueryOptions('upsert'))
  447.                     );
  448.                 }
  449.                 return $this->collection->{$operation}(
  450.                     $this->query['query'],
  451.                     $this->query['newObj'],
  452.                     array_merge($options$this->getQueryOptions('upsert'))
  453.                 );
  454.             case self::TYPE_REMOVE:
  455.                 return $this->collection->deleteMany($this->query['query'], $options);
  456.             case self::TYPE_DISTINCT:
  457.                 $collection $this->collection;
  458.                 $query      $this->query;
  459.                 return $collection->distinct(
  460.                     $query['distinct'],
  461.                     $query['query'],
  462.                     array_merge($options$this->getQueryOptions('readPreference'))
  463.                 );
  464.             case self::TYPE_COUNT:
  465.                 $collection $this->collection;
  466.                 $query      $this->query;
  467.                 return $collection->count(
  468.                     $query['query'],
  469.                     array_merge($options$this->getQueryOptions('hint''limit''skip''readPreference'))
  470.                 );
  471.             default:
  472.                 throw new InvalidArgumentException('Invalid query type: ' $this->query['type']);
  473.         }
  474.     }
  475.     private function isFirstKeyUpdateOperator(): bool
  476.     {
  477.         reset($this->query['newObj']);
  478.         $firstKey key($this->query['newObj']);
  479.         return is_string($firstKey) && $firstKey[0] === '$';
  480.     }
  481. }