vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php line 170

  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ODM\MongoDB\Aggregation;
  4. use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort;
  5. use Doctrine\ODM\MongoDB\DocumentManager;
  6. use Doctrine\ODM\MongoDB\Iterator\Iterator;
  7. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
  8. use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
  9. use Doctrine\ODM\MongoDB\Query\Expr as QueryExpr;
  10. use GeoJson\Geometry\Point;
  11. use MongoDB\Collection;
  12. use OutOfRangeException;
  13. use TypeError;
  14. use function array_map;
  15. use function array_unshift;
  16. use function func_get_arg;
  17. use function func_num_args;
  18. use function gettype;
  19. use function is_array;
  20. use function is_bool;
  21. use function sprintf;
  22. use function trigger_deprecation;
  23. /**
  24.  * Fluent interface for building aggregation pipelines.
  25.  *
  26.  * @psalm-import-type SortShape from Sort
  27.  */
  28. class Builder
  29. {
  30.     /**
  31.      * The DocumentManager instance for this query
  32.      */
  33.     private DocumentManager $dm;
  34.     /**
  35.      * The ClassMetadata instance.
  36.      */
  37.     private ClassMetadata $class;
  38.     /** @psalm-var class-string */
  39.     private ?string $hydrationClass null;
  40.     /**
  41.      * The Collection instance.
  42.      */
  43.     private Collection $collection;
  44.     /** @var Stage[] */
  45.     private array $stages = [];
  46.     private bool $rewindable true;
  47.     /**
  48.      * Create a new aggregation builder.
  49.      *
  50.      * @psalm-param class-string $documentName
  51.      */
  52.     public function __construct(DocumentManager $dmstring $documentName)
  53.     {
  54.         $this->dm         $dm;
  55.         $this->class      $this->dm->getClassMetadata($documentName);
  56.         $this->collection $this->dm->getDocumentCollection($documentName);
  57.     }
  58.     /**
  59.      * Adds new fields to documents. $addFields outputs documents that contain all
  60.      * existing fields from the input documents and newly added fields.
  61.      *
  62.      * The $addFields stage is equivalent to a $project stage that explicitly specifies
  63.      * all existing fields in the input documents and adds the new fields.
  64.      *
  65.      * If the name of the new field is the same as an existing field name (including _id),
  66.      * $addFields overwrites the existing value of that field with the value of the
  67.      * specified expression.
  68.      *
  69.      * @see http://docs.mongodb.com/manual/reference/operator/aggregation/addFields/
  70.      */
  71.     public function addFields(): Stage\AddFields
  72.     {
  73.         $stage = new Stage\AddFields($this);
  74.         $this->addStage($stage);
  75.         return $stage;
  76.     }
  77.     /**
  78.      * Categorizes incoming documents into groups, called buckets, based on a
  79.      * specified expression and bucket boundaries.
  80.      *
  81.      * Each bucket is represented as a document in the output. The document for
  82.      * each bucket contains an _id field, whose value specifies the inclusive
  83.      * lower bound of the bucket and a count field that contains the number of
  84.      * documents in the bucket. The count field is included by default when the
  85.      * output is not specified.
  86.      *
  87.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
  88.      */
  89.     public function bucket(): Stage\Bucket
  90.     {
  91.         $stage = new Stage\Bucket($this$this->dm$this->class);
  92.         $this->addStage($stage);
  93.         return $stage;
  94.     }
  95.     /**
  96.      * Categorizes incoming documents into a specific number of groups, called
  97.      * buckets, based on a specified expression.
  98.      *
  99.      * Bucket boundaries are automatically determined in an attempt to evenly
  100.      * distribute the documents into the specified number of buckets. Each
  101.      * bucket is represented as a document in the output. The document for each
  102.      * bucket contains an _id field, whose value specifies the inclusive lower
  103.      * bound and the exclusive upper bound for the bucket, and a count field
  104.      * that contains the number of documents in the bucket. The count field is
  105.      * included by default when the output is not specified.
  106.      *
  107.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/
  108.      */
  109.     public function bucketAuto(): Stage\BucketAuto
  110.     {
  111.         $stage = new Stage\BucketAuto($this$this->dm$this->class);
  112.         $this->addStage($stage);
  113.         return $stage;
  114.     }
  115.     /**
  116.      * Returns statistics regarding a collection or view.
  117.      *
  118.      * $collStats must be the first stage in an aggregation pipeline, or else
  119.      * the pipeline returns an error.
  120.      *
  121.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/collStats/
  122.      */
  123.     public function collStats(): Stage\CollStats
  124.     {
  125.         $stage = new Stage\CollStats($this);
  126.         $this->addStage($stage);
  127.         return $stage;
  128.     }
  129.     /**
  130.      * Returns a document that contains a count of the number of documents input
  131.      * to the stage.
  132.      *
  133.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/count/
  134.      */
  135.     public function count(string $fieldName): Stage\Count
  136.     {
  137.         $stage = new Stage\Count($this$fieldName);
  138.         $this->addStage($stage);
  139.         return $stage;
  140.     }
  141.     /**
  142.      * Executes the aggregation pipeline
  143.      *
  144.      * @deprecated This method was deprecated in doctrine/mongodb-odm 2.2. Please use getAggregation() instead.
  145.      *
  146.      * @param array<string, mixed> $options
  147.      */
  148.     public function execute(array $options = []): Iterator
  149.     {
  150.         trigger_deprecation(
  151.             'doctrine/mongodb-odm',
  152.             '2.2',
  153.             'Using "%s" is deprecated. Please use "%s::getAggregation()" instead.',
  154.             __METHOD__,
  155.             self::class,
  156.         );
  157.         return $this->getAggregation($options)->getIterator();
  158.     }
  159.     public function expr(): Expr
  160.     {
  161.         return new Expr($this->dm$this->class);
  162.     }
  163.     /**
  164.      * Processes multiple aggregation pipelines within a single stage on the
  165.      * same set of input documents.
  166.      *
  167.      * Each sub-pipeline has its own field in the output document where its
  168.      * results are stored as an array of documents.
  169.      */
  170.     public function facet(): Stage\Facet
  171.     {
  172.         $stage = new Stage\Facet($this);
  173.         $this->addStage($stage);
  174.         return $stage;
  175.     }
  176.     /**
  177.      * Outputs documents in order of nearest to farthest from a specified point.
  178.      *
  179.      * A GeoJSON point may be provided as the first and only argument for
  180.      * 2dsphere queries. This single parameter may be a GeoJSON point object or
  181.      * an array corresponding to the point's JSON representation. If GeoJSON is
  182.      * used, the "spherical" option will default to true.
  183.      *
  184.      * You can only use this as the first stage of a pipeline.
  185.      *
  186.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/
  187.      *
  188.      * @param float|array<string, mixed>|Point $x
  189.      * @param float                            $y
  190.      */
  191.     public function geoNear($x$y null): Stage\GeoNear
  192.     {
  193.         $stage = new Stage\GeoNear($this$x$y);
  194.         $this->addStage($stage);
  195.         return $stage;
  196.     }
  197.     /**
  198.      * Returns an aggregation object for the current pipeline
  199.      *
  200.      * @param array<string, mixed> $options
  201.      */
  202.     public function getAggregation(array $options = []): Aggregation
  203.     {
  204.         $class $this->hydrationClass $this->dm->getClassMetadata($this->hydrationClass) : null;
  205.         return new Aggregation($this->dm$class$this->collection$this->getPipeline(), $options$this->rewindable);
  206.     }
  207.     // phpcs:disable Squiz.Commenting.FunctionComment.ExtraParamComment
  208.     /**
  209.      * Returns the assembled aggregation pipeline
  210.      *
  211.      * @param bool $applyFilters Whether to apply filters on the aggregation
  212.      * pipeline stage
  213.      *
  214.      * For pipelines where the first stage is a $geoNear stage, it will apply
  215.      * the document filters and discriminator queries to the query portion of
  216.      * the geoNear operation. For all other pipelines, it prepends a $match stage
  217.      * containing the required query.
  218.      *
  219.      * For aggregation pipelines that will be nested (e.g. in a facet stage),
  220.      * you should not apply filters as this may cause wrong results to be
  221.      * given.
  222.      *
  223.      * @return array<array<string, mixed>>
  224.      */
  225.     // phpcs:enable Squiz.Commenting.FunctionComment.ExtraParamComment
  226.     public function getPipeline(/* bool $applyFilters = true */): array
  227.     {
  228.         $applyFilters func_num_args() > func_get_arg(0) : true;
  229.         if (! is_bool($applyFilters)) {
  230.             throw new TypeError(sprintf(
  231.                 'Argument 1 passed to %s must be of the type bool, %s given',
  232.                 __METHOD__,
  233.                 gettype($applyFilters),
  234.             ));
  235.         }
  236.         $pipeline array_map(
  237.             static fn (Stage $stage) => $stage->getExpression(),
  238.             $this->stages,
  239.         );
  240.         if ($this->getStage(0) instanceof Stage\IndexStats) {
  241.             // Don't apply any filters when using an IndexStats stage: since it
  242.             // needs to be the first pipeline stage, prepending a match stage
  243.             // with discriminator information will not work
  244.             $applyFilters false;
  245.         }
  246.         if (! $applyFilters) {
  247.             return $pipeline;
  248.         }
  249.         if ($this->getStage(0) instanceof Stage\GeoNear) {
  250.             $pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
  251.             return $pipeline;
  252.         }
  253.         $matchExpression $this->applyFilters([]);
  254.         if ($matchExpression !== []) {
  255.             array_unshift($pipeline, ['$match' => $matchExpression]);
  256.         }
  257.         return $pipeline;
  258.     }
  259.     /**
  260.      * Returns a certain stage from the pipeline
  261.      */
  262.     public function getStage(int $index): Stage
  263.     {
  264.         if (! isset($this->stages[$index])) {
  265.             throw new OutOfRangeException(sprintf('Could not find stage with index %d.'$index));
  266.         }
  267.         return $this->stages[$index];
  268.     }
  269.     /**
  270.      * Performs a recursive search on a collection, with options for restricting
  271.      * the search by recursion depth and query filter.
  272.      *
  273.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/
  274.      *
  275.      * @param string $from Target collection for the $graphLookup operation to
  276.      * search, recursively matching the connectFromField to the connectToField.
  277.      */
  278.     public function graphLookup(string $from): Stage\GraphLookup
  279.     {
  280.         $stage = new Stage\GraphLookup($this$from$this->dm$this->class);
  281.         $this->addStage($stage);
  282.         return $stage;
  283.     }
  284.     /**
  285.      * Groups documents by some specified expression and outputs to the next
  286.      * stage a document for each distinct grouping.
  287.      *
  288.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/group/
  289.      */
  290.     public function group(): Stage\Group
  291.     {
  292.         $stage = new Stage\Group($this);
  293.         $this->addStage($stage);
  294.         return $stage;
  295.     }
  296.     /**
  297.      * Set which class to use when hydrating results as document class instances.
  298.      */
  299.     public function hydrate(?string $className): self
  300.     {
  301.         $this->hydrationClass $className;
  302.         return $this;
  303.     }
  304.     /**
  305.      * Returns statistics regarding the use of each index for the collection.
  306.      *
  307.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/
  308.      */
  309.     public function indexStats(): Stage\IndexStats
  310.     {
  311.         $stage = new Stage\IndexStats($this);
  312.         $this->addStage($stage);
  313.         return $stage;
  314.     }
  315.     /**
  316.      * Limits the number of documents passed to the next stage in the pipeline.
  317.      *
  318.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
  319.      */
  320.     public function limit(int $limit): Stage\Limit
  321.     {
  322.         $stage = new Stage\Limit($this$limit);
  323.         $this->addStage($stage);
  324.         return $stage;
  325.     }
  326.     /**
  327.      * Performs a left outer join to an unsharded collection in the same
  328.      * database to filter in documents from the “joined” collection for
  329.      * processing.
  330.      *
  331.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
  332.      */
  333.     public function lookup(string $from): Stage\Lookup
  334.     {
  335.         $stage = new Stage\Lookup($this$from$this->dm$this->class);
  336.         $this->addStage($stage);
  337.         return $stage;
  338.     }
  339.     /**
  340.      * Filters the documents to pass only the documents that match the specified
  341.      * condition(s) to the next pipeline stage.
  342.      *
  343.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/match/
  344.      */
  345.     public function match(): Stage\MatchStage
  346.     {
  347.         $stage = new Stage\MatchStage($this);
  348.         $this->addStage($stage);
  349.         return $stage;
  350.     }
  351.     /**
  352.      * Returns a query expression to be used in match stages
  353.      */
  354.     public function matchExpr(): QueryExpr
  355.     {
  356.         $expr = new QueryExpr($this->dm);
  357.         $expr->setClassMetadata($this->class);
  358.         return $expr;
  359.     }
  360.     /**
  361.      * Takes the documents returned by the aggregation pipeline and writes them
  362.      * to a specified collection. This must be the last stage in the pipeline.
  363.      *
  364.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/out/
  365.      */
  366.     public function out(string $from): Stage\Out
  367.     {
  368.         $stage = new Stage\Out($this$from$this->dm);
  369.         $this->addStage($stage);
  370.         return $stage;
  371.     }
  372.     /**
  373.      * Passes along the documents with only the specified fields to the next
  374.      * stage in the pipeline. The specified fields can be existing fields from
  375.      * the input documents or newly computed fields.
  376.      *
  377.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/project/
  378.      */
  379.     public function project(): Stage\Project
  380.     {
  381.         $stage = new Stage\Project($this);
  382.         $this->addStage($stage);
  383.         return $stage;
  384.     }
  385.     /**
  386.      * Restricts the contents of the documents based on information stored in
  387.      * the documents themselves.
  388.      *
  389.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/
  390.      */
  391.     public function redact(): Stage\Redact
  392.     {
  393.         $stage = new Stage\Redact($this);
  394.         $this->addStage($stage);
  395.         return $stage;
  396.     }
  397.     /**
  398.      * Promotes a specified document to the top level and replaces all other
  399.      * fields.
  400.      *
  401.      * The operation replaces all existing fields in the input document,
  402.      * including the _id field. You can promote an existing embedded document to
  403.      * the top level, or create a new document for promotion.
  404.      *
  405.      * @param string|mixed[]|Expr|null $expression Optional. A replacement expression that
  406.      * resolves to a document.
  407.      */
  408.     public function replaceRoot($expression null): Stage\ReplaceRoot
  409.     {
  410.         $stage = new Stage\ReplaceRoot($this$this->dm$this->class$expression);
  411.         $this->addStage($stage);
  412.         return $stage;
  413.     }
  414.     /**
  415.      * Controls if resulting iterator should be wrapped with CachingIterator.
  416.      */
  417.     public function rewindable(bool $rewindable true): self
  418.     {
  419.         $this->rewindable $rewindable;
  420.         return $this;
  421.     }
  422.     /**
  423.      * Randomly selects the specified number of documents from its input.
  424.      *
  425.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/sample/
  426.      */
  427.     public function sample(int $size): Stage\Sample
  428.     {
  429.         $stage = new Stage\Sample($this$size);
  430.         $this->addStage($stage);
  431.         return $stage;
  432.     }
  433.     /**
  434.      * Skips over the specified number of documents that pass into the stage and
  435.      * passes the remaining documents to the next stage in the pipeline.
  436.      *
  437.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/skip/
  438.      */
  439.     public function skip(int $skip): Stage\Skip
  440.     {
  441.         $stage = new Stage\Skip($this$skip);
  442.         $this->addStage($stage);
  443.         return $stage;
  444.     }
  445.     /**
  446.      * Sorts all input documents and returns them to the pipeline in sorted
  447.      * order.
  448.      *
  449.      * If sorting by multiple fields, the first argument should be an array of
  450.      * field name (key) and order (value) pairs.
  451.      *
  452.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/sort/
  453.      *
  454.      * @param array<string, int|string|array<string, string>>|string $fieldName Field name or array of field/order pairs
  455.      * @param int|string|null                                        $order     Field order (if one field is specified)
  456.      * @psalm-param SortShape|string $fieldName Field name or array of field/order pairs
  457.      */
  458.     public function sort($fieldName$order null): Stage\Sort
  459.     {
  460.         $fields is_array($fieldName) ? $fieldName : [$fieldName => $order];
  461.         // fixme: move to sort stage
  462.         $stage = new Stage\Sort($this$this->getDocumentPersister()->prepareSort($fields));
  463.         $this->addStage($stage);
  464.         return $stage;
  465.     }
  466.     /**
  467.      * Groups incoming documents based on the value of a specified expression,
  468.      * then computes the count of documents in each distinct group.
  469.      *
  470.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/
  471.      */
  472.     public function sortByCount(string $expression): Stage\SortByCount
  473.     {
  474.         $stage = new Stage\SortByCount($this$expression$this->dm$this->class);
  475.         $this->addStage($stage);
  476.         return $stage;
  477.     }
  478.     /**
  479.      * Deconstructs an array field from the input documents to output a document
  480.      * for each element. Each output document is the input document with the
  481.      * value of the array field replaced by the element.
  482.      *
  483.      * @see https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/
  484.      */
  485.     public function unwind(string $fieldName): Stage\Unwind
  486.     {
  487.         // Fixme: move field name translation to stage
  488.         $stage = new Stage\Unwind($this$this->getDocumentPersister()->prepareFieldName($fieldName));
  489.         $this->addStage($stage);
  490.         return $stage;
  491.     }
  492.     /**
  493.      * Allows adding an arbitrary stage to the pipeline
  494.      *
  495.      * @return Stage The method returns the stage given as an argument
  496.      */
  497.     public function addStage(Stage $stage): Stage
  498.     {
  499.         $this->stages[] = $stage;
  500.         return $stage;
  501.     }
  502.     /**
  503.      * Applies filters and discriminator queries to the pipeline
  504.      *
  505.      * @param array<string, mixed> $query
  506.      *
  507.      * @return array<string, mixed>
  508.      */
  509.     private function applyFilters(array $query): array
  510.     {
  511.         $documentPersister $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
  512.         $query $documentPersister->addDiscriminatorToPreparedQuery($query);
  513.         $query $documentPersister->addFilterToPreparedQuery($query);
  514.         return $query;
  515.     }
  516.     private function getDocumentPersister(): DocumentPersister
  517.     {
  518.         return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
  519.     }
  520. }