vendor/doctrine/orm/src/QueryBuilder.php line 39

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM;
  4. use Doctrine\Common\Collections\ArrayCollection;
  5. use Doctrine\Common\Collections\Criteria;
  6. use Doctrine\ORM\Internal\QueryType;
  7. use Doctrine\ORM\Query\Expr;
  8. use Doctrine\ORM\Query\Parameter;
  9. use Doctrine\ORM\Query\QueryExpressionVisitor;
  10. use InvalidArgumentException;
  11. use RuntimeException;
  12. use Stringable;
  13. use function array_keys;
  14. use function array_unshift;
  15. use function assert;
  16. use function count;
  17. use function implode;
  18. use function in_array;
  19. use function is_array;
  20. use function is_numeric;
  21. use function is_object;
  22. use function is_string;
  23. use function key;
  24. use function reset;
  25. use function sprintf;
  26. use function str_starts_with;
  27. use function strpos;
  28. use function strrpos;
  29. use function substr;
  30. /**
  31.  * This class is responsible for building DQL query strings via an object oriented
  32.  * PHP interface.
  33.  */
  34. class QueryBuilder implements Stringable
  35. {
  36.     /**
  37.      * The array of DQL parts collected.
  38.      *
  39.      * @psalm-var array<string, mixed>
  40.      */
  41.     private array $dqlParts = [
  42.         'distinct' => false,
  43.         'select'  => [],
  44.         'from'    => [],
  45.         'join'    => [],
  46.         'set'     => [],
  47.         'where'   => null,
  48.         'groupBy' => [],
  49.         'having'  => null,
  50.         'orderBy' => [],
  51.     ];
  52.     private QueryType $type QueryType::Select;
  53.     /**
  54.      * The complete DQL string for this query.
  55.      */
  56.     private string|null $dql null;
  57.     /**
  58.      * The query parameters.
  59.      *
  60.      * @psalm-var ArrayCollection<int, Parameter>
  61.      */
  62.     private ArrayCollection $parameters;
  63.     /**
  64.      * The index of the first result to retrieve.
  65.      */
  66.     private int $firstResult 0;
  67.     /**
  68.      * The maximum number of results to retrieve.
  69.      */
  70.     private int|null $maxResults null;
  71.     /**
  72.      * Keeps root entity alias names for join entities.
  73.      *
  74.      * @psalm-var array<string, string>
  75.      */
  76.     private array $joinRootAliases = [];
  77.     /**
  78.      * Whether to use second level cache, if available.
  79.      */
  80.     protected bool $cacheable false;
  81.     /**
  82.      * Second level cache region name.
  83.      */
  84.     protected string|null $cacheRegion null;
  85.     /**
  86.      * Second level query cache mode.
  87.      *
  88.      * @psalm-var Cache::MODE_*|null
  89.      */
  90.     protected int|null $cacheMode null;
  91.     protected int $lifetime 0;
  92.     /**
  93.      * Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
  94.      *
  95.      * @param EntityManagerInterface $em The EntityManager to use.
  96.      */
  97.     public function __construct(
  98.         private readonly EntityManagerInterface $em,
  99.     ) {
  100.         $this->parameters = new ArrayCollection();
  101.     }
  102.     /**
  103.      * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
  104.      * This producer method is intended for convenient inline usage. Example:
  105.      *
  106.      * <code>
  107.      *     $qb = $em->createQueryBuilder();
  108.      *     $qb
  109.      *         ->select('u')
  110.      *         ->from('User', 'u')
  111.      *         ->where($qb->expr()->eq('u.id', 1));
  112.      * </code>
  113.      *
  114.      * For more complex expression construction, consider storing the expression
  115.      * builder object in a local variable.
  116.      */
  117.     public function expr(): Expr
  118.     {
  119.         return $this->em->getExpressionBuilder();
  120.     }
  121.     /**
  122.      * Enable/disable second level query (result) caching for this query.
  123.      *
  124.      * @return $this
  125.      */
  126.     public function setCacheable(bool $cacheable): static
  127.     {
  128.         $this->cacheable $cacheable;
  129.         return $this;
  130.     }
  131.     /**
  132.      * Are the query results enabled for second level cache?
  133.      */
  134.     public function isCacheable(): bool
  135.     {
  136.         return $this->cacheable;
  137.     }
  138.     /** @return $this */
  139.     public function setCacheRegion(string $cacheRegion): static
  140.     {
  141.         $this->cacheRegion $cacheRegion;
  142.         return $this;
  143.     }
  144.     /**
  145.      * Obtain the name of the second level query cache region in which query results will be stored
  146.      *
  147.      * @return string|null The cache region name; NULL indicates the default region.
  148.      */
  149.     public function getCacheRegion(): string|null
  150.     {
  151.         return $this->cacheRegion;
  152.     }
  153.     public function getLifetime(): int
  154.     {
  155.         return $this->lifetime;
  156.     }
  157.     /**
  158.      * Sets the life-time for this query into second level cache.
  159.      *
  160.      * @return $this
  161.      */
  162.     public function setLifetime(int $lifetime): static
  163.     {
  164.         $this->lifetime $lifetime;
  165.         return $this;
  166.     }
  167.     /** @psalm-return Cache::MODE_*|null */
  168.     public function getCacheMode(): int|null
  169.     {
  170.         return $this->cacheMode;
  171.     }
  172.     /**
  173.      * @psalm-param Cache::MODE_* $cacheMode
  174.      *
  175.      * @return $this
  176.      */
  177.     public function setCacheMode(int $cacheMode): static
  178.     {
  179.         $this->cacheMode $cacheMode;
  180.         return $this;
  181.     }
  182.     /**
  183.      * Gets the associated EntityManager for this query builder.
  184.      */
  185.     public function getEntityManager(): EntityManagerInterface
  186.     {
  187.         return $this->em;
  188.     }
  189.     /**
  190.      * Gets the complete DQL string formed by the current specifications of this QueryBuilder.
  191.      *
  192.      * <code>
  193.      *     $qb = $em->createQueryBuilder()
  194.      *         ->select('u')
  195.      *         ->from('User', 'u');
  196.      *     echo $qb->getDql(); // SELECT u FROM User u
  197.      * </code>
  198.      */
  199.     public function getDQL(): string
  200.     {
  201.         return $this->dql ??= match ($this->type) {
  202.             QueryType::Select => $this->getDQLForSelect(),
  203.             QueryType::Delete => $this->getDQLForDelete(),
  204.             QueryType::Update => $this->getDQLForUpdate(),
  205.         };
  206.     }
  207.     /**
  208.      * Constructs a Query instance from the current specifications of the builder.
  209.      *
  210.      * <code>
  211.      *     $qb = $em->createQueryBuilder()
  212.      *         ->select('u')
  213.      *         ->from('User', 'u');
  214.      *     $q = $qb->getQuery();
  215.      *     $results = $q->execute();
  216.      * </code>
  217.      */
  218.     public function getQuery(): Query
  219.     {
  220.         $parameters = clone $this->parameters;
  221.         $query      $this->em->createQuery($this->getDQL())
  222.             ->setParameters($parameters)
  223.             ->setFirstResult($this->firstResult)
  224.             ->setMaxResults($this->maxResults);
  225.         if ($this->lifetime) {
  226.             $query->setLifetime($this->lifetime);
  227.         }
  228.         if ($this->cacheMode) {
  229.             $query->setCacheMode($this->cacheMode);
  230.         }
  231.         if ($this->cacheable) {
  232.             $query->setCacheable($this->cacheable);
  233.         }
  234.         if ($this->cacheRegion) {
  235.             $query->setCacheRegion($this->cacheRegion);
  236.         }
  237.         return $query;
  238.     }
  239.     /**
  240.      * Finds the root entity alias of the joined entity.
  241.      *
  242.      * @param string $alias       The alias of the new join entity
  243.      * @param string $parentAlias The parent entity alias of the join relationship
  244.      */
  245.     private function findRootAlias(string $aliasstring $parentAlias): string
  246.     {
  247.         if (in_array($parentAlias$this->getRootAliases(), true)) {
  248.             $rootAlias $parentAlias;
  249.         } elseif (isset($this->joinRootAliases[$parentAlias])) {
  250.             $rootAlias $this->joinRootAliases[$parentAlias];
  251.         } else {
  252.             // Should never happen with correct joining order. Might be
  253.             // thoughtful to throw exception instead.
  254.             $rootAlias $this->getRootAlias();
  255.         }
  256.         $this->joinRootAliases[$alias] = $rootAlias;
  257.         return $rootAlias;
  258.     }
  259.     /**
  260.      * Gets the FIRST root alias of the query. This is the first entity alias involved
  261.      * in the construction of the query.
  262.      *
  263.      * <code>
  264.      * $qb = $em->createQueryBuilder()
  265.      *     ->select('u')
  266.      *     ->from('User', 'u');
  267.      *
  268.      * echo $qb->getRootAlias(); // u
  269.      * </code>
  270.      *
  271.      * @deprecated Please use $qb->getRootAliases() instead.
  272.      *
  273.      * @throws RuntimeException
  274.      */
  275.     public function getRootAlias(): string
  276.     {
  277.         $aliases $this->getRootAliases();
  278.         if (! isset($aliases[0])) {
  279.             throw new RuntimeException('No alias was set before invoking getRootAlias().');
  280.         }
  281.         return $aliases[0];
  282.     }
  283.     /**
  284.      * Gets the root aliases of the query. This is the entity aliases involved
  285.      * in the construction of the query.
  286.      *
  287.      * <code>
  288.      *     $qb = $em->createQueryBuilder()
  289.      *         ->select('u')
  290.      *         ->from('User', 'u');
  291.      *
  292.      *     $qb->getRootAliases(); // array('u')
  293.      * </code>
  294.      *
  295.      * @return string[]
  296.      * @psalm-return list<string>
  297.      */
  298.     public function getRootAliases(): array
  299.     {
  300.         $aliases = [];
  301.         foreach ($this->dqlParts['from'] as &$fromClause) {
  302.             if (is_string($fromClause)) {
  303.                 $spacePos strrpos($fromClause' ');
  304.                 /** @psalm-var class-string $from */
  305.                 $from  substr($fromClause0$spacePos);
  306.                 $alias substr($fromClause$spacePos 1);
  307.                 $fromClause = new Query\Expr\From($from$alias);
  308.             }
  309.             $aliases[] = $fromClause->getAlias();
  310.         }
  311.         return $aliases;
  312.     }
  313.     /**
  314.      * Gets all the aliases that have been used in the query.
  315.      * Including all select root aliases and join aliases
  316.      *
  317.      * <code>
  318.      *     $qb = $em->createQueryBuilder()
  319.      *         ->select('u')
  320.      *         ->from('User', 'u')
  321.      *         ->join('u.articles','a');
  322.      *
  323.      *     $qb->getAllAliases(); // array('u','a')
  324.      * </code>
  325.      *
  326.      * @return string[]
  327.      * @psalm-return list<string>
  328.      */
  329.     public function getAllAliases(): array
  330.     {
  331.         return [...$this->getRootAliases(), ...array_keys($this->joinRootAliases)];
  332.     }
  333.     /**
  334.      * Gets the root entities of the query. This is the entity classes involved
  335.      * in the construction of the query.
  336.      *
  337.      * <code>
  338.      *     $qb = $em->createQueryBuilder()
  339.      *         ->select('u')
  340.      *         ->from('User', 'u');
  341.      *
  342.      *     $qb->getRootEntities(); // array('User')
  343.      * </code>
  344.      *
  345.      * @return string[]
  346.      * @psalm-return list<class-string>
  347.      */
  348.     public function getRootEntities(): array
  349.     {
  350.         $entities = [];
  351.         foreach ($this->dqlParts['from'] as &$fromClause) {
  352.             if (is_string($fromClause)) {
  353.                 $spacePos strrpos($fromClause' ');
  354.                 /** @psalm-var class-string $from */
  355.                 $from  substr($fromClause0$spacePos);
  356.                 $alias substr($fromClause$spacePos 1);
  357.                 $fromClause = new Query\Expr\From($from$alias);
  358.             }
  359.             $entities[] = $fromClause->getFrom();
  360.         }
  361.         return $entities;
  362.     }
  363.     /**
  364.      * Sets a query parameter for the query being constructed.
  365.      *
  366.      * <code>
  367.      *     $qb = $em->createQueryBuilder()
  368.      *         ->select('u')
  369.      *         ->from('User', 'u')
  370.      *         ->where('u.id = :user_id')
  371.      *         ->setParameter('user_id', 1);
  372.      * </code>
  373.      *
  374.      * @param string|int      $key  The parameter position or name.
  375.      * @param string|int|null $type ParameterType::* or \Doctrine\DBAL\Types\Type::* constant
  376.      *
  377.      * @return $this
  378.      */
  379.     public function setParameter(string|int $keymixed $valuestring|int|null $type null): static
  380.     {
  381.         $existingParameter $this->getParameter($key);
  382.         if ($existingParameter !== null) {
  383.             $existingParameter->setValue($value$type);
  384.             return $this;
  385.         }
  386.         $this->parameters->add(new Parameter($key$value$type));
  387.         return $this;
  388.     }
  389.     /**
  390.      * Sets a collection of query parameters for the query being constructed.
  391.      *
  392.      * <code>
  393.      *     $qb = $em->createQueryBuilder()
  394.      *         ->select('u')
  395.      *         ->from('User', 'u')
  396.      *         ->where('u.id = :user_id1 OR u.id = :user_id2')
  397.      *         ->setParameters(new ArrayCollection(array(
  398.      *             new Parameter('user_id1', 1),
  399.      *             new Parameter('user_id2', 2)
  400.      *        )));
  401.      * </code>
  402.      *
  403.      * @psalm-param ArrayCollection<int, Parameter> $parameters
  404.      *
  405.      * @return $this
  406.      */
  407.     public function setParameters(ArrayCollection $parameters): static
  408.     {
  409.         $this->parameters $parameters;
  410.         return $this;
  411.     }
  412.     /**
  413.      * Gets all defined query parameters for the query being constructed.
  414.      *
  415.      * @psalm-return ArrayCollection<int, Parameter>
  416.      */
  417.     public function getParameters(): ArrayCollection
  418.     {
  419.         return $this->parameters;
  420.     }
  421.     /**
  422.      * Gets a (previously set) query parameter of the query being constructed.
  423.      */
  424.     public function getParameter(string|int $key): Parameter|null
  425.     {
  426.         $key Parameter::normalizeName($key);
  427.         $filteredParameters $this->parameters->filter(
  428.             static fn (Parameter $parameter): bool => $key === $parameter->getName()
  429.         );
  430.         return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
  431.     }
  432.     /**
  433.      * Sets the position of the first result to retrieve (the "offset").
  434.      *
  435.      * @return $this
  436.      */
  437.     public function setFirstResult(int|null $firstResult): static
  438.     {
  439.         $this->firstResult = (int) $firstResult;
  440.         return $this;
  441.     }
  442.     /**
  443.      * Gets the position of the first result the query object was set to retrieve (the "offset").
  444.      */
  445.     public function getFirstResult(): int
  446.     {
  447.         return $this->firstResult;
  448.     }
  449.     /**
  450.      * Sets the maximum number of results to retrieve (the "limit").
  451.      *
  452.      * @return $this
  453.      */
  454.     public function setMaxResults(int|null $maxResults): static
  455.     {
  456.         $this->maxResults $maxResults;
  457.         return $this;
  458.     }
  459.     /**
  460.      * Gets the maximum number of results the query object was set to retrieve (the "limit").
  461.      * Returns NULL if {@link setMaxResults} was not applied to this query builder.
  462.      */
  463.     public function getMaxResults(): int|null
  464.     {
  465.         return $this->maxResults;
  466.     }
  467.     /**
  468.      * Either appends to or replaces a single, generic query part.
  469.      *
  470.      * The available parts are: 'select', 'from', 'join', 'set', 'where',
  471.      * 'groupBy', 'having' and 'orderBy'.
  472.      *
  473.      * @psalm-param string|object|list<string>|array{join: array<int|string, object>} $dqlPart
  474.      *
  475.      * @return $this
  476.      */
  477.     public function add(string $dqlPartNamestring|object|array $dqlPartbool $append false): static
  478.     {
  479.         if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) {
  480.             throw new InvalidArgumentException(
  481.                 "Using \$append = true does not have an effect with 'where' or 'having' " .
  482.                 'parts. See QueryBuilder#andWhere() for an example for correct usage.',
  483.             );
  484.         }
  485.         $isMultiple is_array($this->dqlParts[$dqlPartName])
  486.             && ! ($dqlPartName === 'join' && ! $append);
  487.         // Allow adding any part retrieved from self::getDQLParts().
  488.         if (is_array($dqlPart) && $dqlPartName !== 'join') {
  489.             $dqlPart reset($dqlPart);
  490.         }
  491.         // This is introduced for backwards compatibility reasons.
  492.         // TODO: Remove for 3.0
  493.         if ($dqlPartName === 'join') {
  494.             $newDqlPart = [];
  495.             foreach ($dqlPart as $k => $v) {
  496.                 $k is_numeric($k) ? $this->getRootAlias() : $k;
  497.                 $newDqlPart[$k] = $v;
  498.             }
  499.             $dqlPart $newDqlPart;
  500.         }
  501.         if ($append && $isMultiple) {
  502.             if (is_array($dqlPart)) {
  503.                 $key key($dqlPart);
  504.                 $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key];
  505.             } else {
  506.                 $this->dqlParts[$dqlPartName][] = $dqlPart;
  507.             }
  508.         } else {
  509.             $this->dqlParts[$dqlPartName] = $isMultiple ? [$dqlPart] : $dqlPart;
  510.         }
  511.         $this->dql null;
  512.         return $this;
  513.     }
  514.     /**
  515.      * Specifies an item that is to be returned in the query result.
  516.      * Replaces any previously specified selections, if any.
  517.      *
  518.      * <code>
  519.      *     $qb = $em->createQueryBuilder()
  520.      *         ->select('u', 'p')
  521.      *         ->from('User', 'u')
  522.      *         ->leftJoin('u.Phonenumbers', 'p');
  523.      * </code>
  524.      *
  525.      * @return $this
  526.      */
  527.     public function select(mixed ...$select): static
  528.     {
  529.         $this->type QueryType::Select;
  530.         if ($select === []) {
  531.             return $this;
  532.         }
  533.         return $this->add('select', new Expr\Select($select), false);
  534.     }
  535.     /**
  536.      * Adds a DISTINCT flag to this query.
  537.      *
  538.      * <code>
  539.      *     $qb = $em->createQueryBuilder()
  540.      *         ->select('u')
  541.      *         ->distinct()
  542.      *         ->from('User', 'u');
  543.      * </code>
  544.      *
  545.      * @return $this
  546.      */
  547.     public function distinct(bool $flag true): static
  548.     {
  549.         if ($this->dqlParts['distinct'] !== $flag) {
  550.             $this->dqlParts['distinct'] = $flag;
  551.             $this->dql                  null;
  552.         }
  553.         return $this;
  554.     }
  555.     /**
  556.      * Adds an item that is to be returned in the query result.
  557.      *
  558.      * <code>
  559.      *     $qb = $em->createQueryBuilder()
  560.      *         ->select('u')
  561.      *         ->addSelect('p')
  562.      *         ->from('User', 'u')
  563.      *         ->leftJoin('u.Phonenumbers', 'p');
  564.      * </code>
  565.      *
  566.      * @return $this
  567.      */
  568.     public function addSelect(mixed ...$select): static
  569.     {
  570.         $this->type QueryType::Select;
  571.         if ($select === []) {
  572.             return $this;
  573.         }
  574.         return $this->add('select', new Expr\Select($select), true);
  575.     }
  576.     /**
  577.      * Turns the query being built into a bulk delete query that ranges over
  578.      * a certain entity type.
  579.      *
  580.      * <code>
  581.      *     $qb = $em->createQueryBuilder()
  582.      *         ->delete('User', 'u')
  583.      *         ->where('u.id = :user_id')
  584.      *         ->setParameter('user_id', 1);
  585.      * </code>
  586.      *
  587.      * @param class-string|null $delete The class/type whose instances are subject to the deletion.
  588.      * @param string|null       $alias  The class/type alias used in the constructed query.
  589.      *
  590.      * @return $this
  591.      */
  592.     public function delete(string|null $delete nullstring|null $alias null): static
  593.     {
  594.         $this->type QueryType::Delete;
  595.         if (! $delete) {
  596.             return $this;
  597.         }
  598.         if (! $alias) {
  599.             throw new InvalidArgumentException(sprintf(
  600.                 '%s(): The alias for entity %s must not be omitted.',
  601.                 __METHOD__,
  602.                 $delete,
  603.             ));
  604.         }
  605.         return $this->add('from', new Expr\From($delete$alias));
  606.     }
  607.     /**
  608.      * Turns the query being built into a bulk update query that ranges over
  609.      * a certain entity type.
  610.      *
  611.      * <code>
  612.      *     $qb = $em->createQueryBuilder()
  613.      *         ->update('User', 'u')
  614.      *         ->set('u.password', '?1')
  615.      *         ->where('u.id = ?2');
  616.      * </code>
  617.      *
  618.      * @param class-string|null $update The class/type whose instances are subject to the update.
  619.      * @param string|null       $alias  The class/type alias used in the constructed query.
  620.      *
  621.      * @return $this
  622.      */
  623.     public function update(string|null $update nullstring|null $alias null): static
  624.     {
  625.         $this->type QueryType::Update;
  626.         if (! $update) {
  627.             return $this;
  628.         }
  629.         if (! $alias) {
  630.             throw new InvalidArgumentException(sprintf(
  631.                 '%s(): The alias for entity %s must not be omitted.',
  632.                 __METHOD__,
  633.                 $update,
  634.             ));
  635.         }
  636.         return $this->add('from', new Expr\From($update$alias));
  637.     }
  638.     /**
  639.      * Creates and adds a query root corresponding to the entity identified by the given alias,
  640.      * forming a cartesian product with any existing query roots.
  641.      *
  642.      * <code>
  643.      *     $qb = $em->createQueryBuilder()
  644.      *         ->select('u')
  645.      *         ->from('User', 'u');
  646.      * </code>
  647.      *
  648.      * @param class-string $from    The class name.
  649.      * @param string       $alias   The alias of the class.
  650.      * @param string|null  $indexBy The index for the from.
  651.      *
  652.      * @return $this
  653.      */
  654.     public function from(string $fromstring $aliasstring|null $indexBy null): static
  655.     {
  656.         return $this->add('from', new Expr\From($from$alias$indexBy), true);
  657.     }
  658.     /**
  659.      * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with
  660.      * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it
  661.      * setting an index by.
  662.      *
  663.      * <code>
  664.      *     $qb = $userRepository->createQueryBuilder('u')
  665.      *         ->indexBy('u', 'u.id');
  666.      *
  667.      *     // Is equivalent to...
  668.      *
  669.      *     $qb = $em->createQueryBuilder()
  670.      *         ->select('u')
  671.      *         ->from('User', 'u', 'u.id');
  672.      * </code>
  673.      *
  674.      * @return $this
  675.      *
  676.      * @throws Query\QueryException
  677.      */
  678.     public function indexBy(string $aliasstring $indexBy): static
  679.     {
  680.         $rootAliases $this->getRootAliases();
  681.         if (! in_array($alias$rootAliasestrue)) {
  682.             throw new Query\QueryException(
  683.                 sprintf('Specified root alias %s must be set before invoking indexBy().'$alias),
  684.             );
  685.         }
  686.         foreach ($this->dqlParts['from'] as &$fromClause) {
  687.             assert($fromClause instanceof Expr\From);
  688.             if ($fromClause->getAlias() !== $alias) {
  689.                 continue;
  690.             }
  691.             $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy);
  692.         }
  693.         return $this;
  694.     }
  695.     /**
  696.      * Creates and adds a join over an entity association to the query.
  697.      *
  698.      * The entities in the joined association will be fetched as part of the query
  699.      * result if the alias used for the joined association is placed in the select
  700.      * expressions.
  701.      *
  702.      * <code>
  703.      *     $qb = $em->createQueryBuilder()
  704.      *         ->select('u')
  705.      *         ->from('User', 'u')
  706.      *         ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
  707.      * </code>
  708.      *
  709.      * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
  710.      *
  711.      * @return $this
  712.      */
  713.     public function join(
  714.         string $join,
  715.         string $alias,
  716.         string|null $conditionType null,
  717.         string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition null,
  718.         string|null $indexBy null,
  719.     ): static {
  720.         return $this->innerJoin($join$alias$conditionType$condition$indexBy);
  721.     }
  722.     /**
  723.      * Creates and adds a join over an entity association to the query.
  724.      *
  725.      * The entities in the joined association will be fetched as part of the query
  726.      * result if the alias used for the joined association is placed in the select
  727.      * expressions.
  728.      *
  729.      *     [php]
  730.      *     $qb = $em->createQueryBuilder()
  731.      *         ->select('u')
  732.      *         ->from('User', 'u')
  733.      *         ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
  734.      *
  735.      * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
  736.      *
  737.      * @return $this
  738.      */
  739.     public function innerJoin(
  740.         string $join,
  741.         string $alias,
  742.         string|null $conditionType null,
  743.         string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition null,
  744.         string|null $indexBy null,
  745.     ): static {
  746.         $parentAlias substr($join0, (int) strpos($join'.'));
  747.         $rootAlias $this->findRootAlias($alias$parentAlias);
  748.         $join = new Expr\Join(
  749.             Expr\Join::INNER_JOIN,
  750.             $join,
  751.             $alias,
  752.             $conditionType,
  753.             $condition,
  754.             $indexBy,
  755.         );
  756.         return $this->add('join', [$rootAlias => $join], true);
  757.     }
  758.     /**
  759.      * Creates and adds a left join over an entity association to the query.
  760.      *
  761.      * The entities in the joined association will be fetched as part of the query
  762.      * result if the alias used for the joined association is placed in the select
  763.      * expressions.
  764.      *
  765.      * <code>
  766.      *     $qb = $em->createQueryBuilder()
  767.      *         ->select('u')
  768.      *         ->from('User', 'u')
  769.      *         ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
  770.      * </code>
  771.      *
  772.      * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
  773.      *
  774.      * @return $this
  775.      */
  776.     public function leftJoin(
  777.         string $join,
  778.         string $alias,
  779.         string|null $conditionType null,
  780.         string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition null,
  781.         string|null $indexBy null,
  782.     ): static {
  783.         $parentAlias substr($join0, (int) strpos($join'.'));
  784.         $rootAlias $this->findRootAlias($alias$parentAlias);
  785.         $join = new Expr\Join(
  786.             Expr\Join::LEFT_JOIN,
  787.             $join,
  788.             $alias,
  789.             $conditionType,
  790.             $condition,
  791.             $indexBy,
  792.         );
  793.         return $this->add('join', [$rootAlias => $join], true);
  794.     }
  795.     /**
  796.      * Sets a new value for a field in a bulk update query.
  797.      *
  798.      * <code>
  799.      *     $qb = $em->createQueryBuilder()
  800.      *         ->update('User', 'u')
  801.      *         ->set('u.password', '?1')
  802.      *         ->where('u.id = ?2');
  803.      * </code>
  804.      *
  805.      * @return $this
  806.      */
  807.     public function set(string $keymixed $value): static
  808.     {
  809.         return $this->add('set', new Expr\Comparison($keyExpr\Comparison::EQ$value), true);
  810.     }
  811.     /**
  812.      * Specifies one or more restrictions to the query result.
  813.      * Replaces any previously specified restrictions, if any.
  814.      *
  815.      * <code>
  816.      *     $qb = $em->createQueryBuilder()
  817.      *         ->select('u')
  818.      *         ->from('User', 'u')
  819.      *         ->where('u.id = ?');
  820.      *
  821.      *     // You can optionally programmatically build and/or expressions
  822.      *     $qb = $em->createQueryBuilder();
  823.      *
  824.      *     $or = $qb->expr()->orX();
  825.      *     $or->add($qb->expr()->eq('u.id', 1));
  826.      *     $or->add($qb->expr()->eq('u.id', 2));
  827.      *
  828.      *     $qb->update('User', 'u')
  829.      *         ->set('u.password', '?')
  830.      *         ->where($or);
  831.      * </code>
  832.      *
  833.      * @return $this
  834.      */
  835.     public function where(mixed ...$predicates): static
  836.     {
  837.         if (! (count($predicates) === && $predicates[0] instanceof Expr\Composite)) {
  838.             $predicates = new Expr\Andx($predicates);
  839.         }
  840.         return $this->add('where'$predicates);
  841.     }
  842.     /**
  843.      * Adds one or more restrictions to the query results, forming a logical
  844.      * conjunction with any previously specified restrictions.
  845.      *
  846.      * <code>
  847.      *     $qb = $em->createQueryBuilder()
  848.      *         ->select('u')
  849.      *         ->from('User', 'u')
  850.      *         ->where('u.username LIKE ?')
  851.      *         ->andWhere('u.is_active = 1');
  852.      * </code>
  853.      *
  854.      * @see where()
  855.      *
  856.      * @return $this
  857.      */
  858.     public function andWhere(mixed ...$where): static
  859.     {
  860.         $dql $this->getDQLPart('where');
  861.         if ($dql instanceof Expr\Andx) {
  862.             $dql->addMultiple($where);
  863.         } else {
  864.             array_unshift($where$dql);
  865.             $dql = new Expr\Andx($where);
  866.         }
  867.         return $this->add('where'$dql);
  868.     }
  869.     /**
  870.      * Adds one or more restrictions to the query results, forming a logical
  871.      * disjunction with any previously specified restrictions.
  872.      *
  873.      * <code>
  874.      *     $qb = $em->createQueryBuilder()
  875.      *         ->select('u')
  876.      *         ->from('User', 'u')
  877.      *         ->where('u.id = 1')
  878.      *         ->orWhere('u.id = 2');
  879.      * </code>
  880.      *
  881.      * @see where()
  882.      *
  883.      * @return $this
  884.      */
  885.     public function orWhere(mixed ...$where): static
  886.     {
  887.         $dql $this->getDQLPart('where');
  888.         if ($dql instanceof Expr\Orx) {
  889.             $dql->addMultiple($where);
  890.         } else {
  891.             array_unshift($where$dql);
  892.             $dql = new Expr\Orx($where);
  893.         }
  894.         return $this->add('where'$dql);
  895.     }
  896.     /**
  897.      * Specifies a grouping over the results of the query.
  898.      * Replaces any previously specified groupings, if any.
  899.      *
  900.      * <code>
  901.      *     $qb = $em->createQueryBuilder()
  902.      *         ->select('u')
  903.      *         ->from('User', 'u')
  904.      *         ->groupBy('u.id');
  905.      * </code>
  906.      *
  907.      * @return $this
  908.      */
  909.     public function groupBy(string ...$groupBy): static
  910.     {
  911.         return $this->add('groupBy', new Expr\GroupBy($groupBy));
  912.     }
  913.     /**
  914.      * Adds a grouping expression to the query.
  915.      *
  916.      * <code>
  917.      *     $qb = $em->createQueryBuilder()
  918.      *         ->select('u')
  919.      *         ->from('User', 'u')
  920.      *         ->groupBy('u.lastLogin')
  921.      *         ->addGroupBy('u.createdAt');
  922.      * </code>
  923.      *
  924.      * @return $this
  925.      */
  926.     public function addGroupBy(string ...$groupBy): static
  927.     {
  928.         return $this->add('groupBy', new Expr\GroupBy($groupBy), true);
  929.     }
  930.     /**
  931.      * Specifies a restriction over the groups of the query.
  932.      * Replaces any previous having restrictions, if any.
  933.      *
  934.      * @return $this
  935.      */
  936.     public function having(mixed ...$having): static
  937.     {
  938.         if (! (count($having) === && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) {
  939.             $having = new Expr\Andx($having);
  940.         }
  941.         return $this->add('having'$having);
  942.     }
  943.     /**
  944.      * Adds a restriction over the groups of the query, forming a logical
  945.      * conjunction with any existing having restrictions.
  946.      *
  947.      * @return $this
  948.      */
  949.     public function andHaving(mixed ...$having): static
  950.     {
  951.         $dql $this->getDQLPart('having');
  952.         if ($dql instanceof Expr\Andx) {
  953.             $dql->addMultiple($having);
  954.         } else {
  955.             array_unshift($having$dql);
  956.             $dql = new Expr\Andx($having);
  957.         }
  958.         return $this->add('having'$dql);
  959.     }
  960.     /**
  961.      * Adds a restriction over the groups of the query, forming a logical
  962.      * disjunction with any existing having restrictions.
  963.      *
  964.      * @return $this
  965.      */
  966.     public function orHaving(mixed ...$having): static
  967.     {
  968.         $dql $this->getDQLPart('having');
  969.         if ($dql instanceof Expr\Orx) {
  970.             $dql->addMultiple($having);
  971.         } else {
  972.             array_unshift($having$dql);
  973.             $dql = new Expr\Orx($having);
  974.         }
  975.         return $this->add('having'$dql);
  976.     }
  977.     /**
  978.      * Specifies an ordering for the query results.
  979.      * Replaces any previously specified orderings, if any.
  980.      *
  981.      * @return $this
  982.      */
  983.     public function orderBy(string|Expr\OrderBy $sortstring|null $order null): static
  984.     {
  985.         $orderBy $sort instanceof Expr\OrderBy $sort : new Expr\OrderBy($sort$order);
  986.         return $this->add('orderBy'$orderBy);
  987.     }
  988.     /**
  989.      * Adds an ordering to the query results.
  990.      *
  991.      * @return $this
  992.      */
  993.     public function addOrderBy(string|Expr\OrderBy $sortstring|null $order null): static
  994.     {
  995.         $orderBy $sort instanceof Expr\OrderBy $sort : new Expr\OrderBy($sort$order);
  996.         return $this->add('orderBy'$orderBytrue);
  997.     }
  998.     /**
  999.      * Adds criteria to the query.
  1000.      *
  1001.      * Adds where expressions with AND operator.
  1002.      * Adds orderings.
  1003.      * Overrides firstResult and maxResults if they're set.
  1004.      *
  1005.      * @return $this
  1006.      *
  1007.      * @throws Query\QueryException
  1008.      */
  1009.     public function addCriteria(Criteria $criteria): static
  1010.     {
  1011.         $allAliases $this->getAllAliases();
  1012.         if (! isset($allAliases[0])) {
  1013.             throw new Query\QueryException('No aliases are set before invoking addCriteria().');
  1014.         }
  1015.         $visitor = new QueryExpressionVisitor($this->getAllAliases());
  1016.         $whereExpression $criteria->getWhereExpression();
  1017.         if ($whereExpression) {
  1018.             $this->andWhere($visitor->dispatch($whereExpression));
  1019.             foreach ($visitor->getParameters() as $parameter) {
  1020.                 $this->parameters->add($parameter);
  1021.             }
  1022.         }
  1023.         if ($criteria->getOrderings()) {
  1024.             foreach ($criteria->getOrderings() as $sort => $order) {
  1025.                 $hasValidAlias false;
  1026.                 foreach ($allAliases as $alias) {
  1027.                     if (str_starts_with($sort '.'$alias '.')) {
  1028.                         $hasValidAlias true;
  1029.                         break;
  1030.                     }
  1031.                 }
  1032.                 if (! $hasValidAlias) {
  1033.                     $sort $allAliases[0] . '.' $sort;
  1034.                 }
  1035.                 $this->addOrderBy($sort$order);
  1036.             }
  1037.         }
  1038.         // Overwrite limits only if they was set in criteria
  1039.         $firstResult $criteria->getFirstResult();
  1040.         if ($firstResult 0) {
  1041.             $this->setFirstResult($firstResult);
  1042.         }
  1043.         $maxResults $criteria->getMaxResults();
  1044.         if ($maxResults !== null) {
  1045.             $this->setMaxResults($maxResults);
  1046.         }
  1047.         return $this;
  1048.     }
  1049.     /**
  1050.      * Gets a query part by its name.
  1051.      */
  1052.     public function getDQLPart(string $queryPartName): mixed
  1053.     {
  1054.         return $this->dqlParts[$queryPartName];
  1055.     }
  1056.     /**
  1057.      * Gets all query parts.
  1058.      *
  1059.      * @psalm-return array<string, mixed> $dqlParts
  1060.      */
  1061.     public function getDQLParts(): array
  1062.     {
  1063.         return $this->dqlParts;
  1064.     }
  1065.     private function getDQLForDelete(): string
  1066.     {
  1067.          return 'DELETE'
  1068.               $this->getReducedDQLQueryPart('from', ['pre' => ' ''separator' => ', '])
  1069.               . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
  1070.               . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ''separator' => ', ']);
  1071.     }
  1072.     private function getDQLForUpdate(): string
  1073.     {
  1074.          return 'UPDATE'
  1075.               $this->getReducedDQLQueryPart('from', ['pre' => ' ''separator' => ', '])
  1076.               . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ''separator' => ', '])
  1077.               . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
  1078.               . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ''separator' => ', ']);
  1079.     }
  1080.     private function getDQLForSelect(): string
  1081.     {
  1082.         $dql 'SELECT'
  1083.              . ($this->dqlParts['distinct'] === true ' DISTINCT' '')
  1084.              . $this->getReducedDQLQueryPart('select', ['pre' => ' ''separator' => ', ']);
  1085.         $fromParts   $this->getDQLPart('from');
  1086.         $joinParts   $this->getDQLPart('join');
  1087.         $fromClauses = [];
  1088.         // Loop through all FROM clauses
  1089.         if (! empty($fromParts)) {
  1090.             $dql .= ' FROM ';
  1091.             foreach ($fromParts as $from) {
  1092.                 $fromClause = (string) $from;
  1093.                 if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) {
  1094.                     foreach ($joinParts[$from->getAlias()] as $join) {
  1095.                         $fromClause .= ' ' . ((string) $join);
  1096.                     }
  1097.                 }
  1098.                 $fromClauses[] = $fromClause;
  1099.             }
  1100.         }
  1101.         $dql .= implode(', '$fromClauses)
  1102.               . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
  1103.               . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ''separator' => ', '])
  1104.               . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING '])
  1105.               . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ''separator' => ', ']);
  1106.         return $dql;
  1107.     }
  1108.     /** @psalm-param array<string, mixed> $options */
  1109.     private function getReducedDQLQueryPart(string $queryPartName, array $options = []): string
  1110.     {
  1111.         $queryPart $this->getDQLPart($queryPartName);
  1112.         if (empty($queryPart)) {
  1113.             return $options['empty'] ?? '';
  1114.         }
  1115.         return ($options['pre'] ?? '')
  1116.              . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart)
  1117.              . ($options['post'] ?? '');
  1118.     }
  1119.     /**
  1120.      * Resets DQL parts.
  1121.      *
  1122.      * @param string[]|null $parts
  1123.      * @psalm-param list<string>|null $parts
  1124.      *
  1125.      * @return $this
  1126.      */
  1127.     public function resetDQLParts(array|null $parts null): static
  1128.     {
  1129.         if ($parts === null) {
  1130.             $parts array_keys($this->dqlParts);
  1131.         }
  1132.         foreach ($parts as $part) {
  1133.             $this->resetDQLPart($part);
  1134.         }
  1135.         return $this;
  1136.     }
  1137.     /**
  1138.      * Resets single DQL part.
  1139.      *
  1140.      * @return $this
  1141.      */
  1142.     public function resetDQLPart(string $part): static
  1143.     {
  1144.         $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null;
  1145.         $this->dql             null;
  1146.         return $this;
  1147.     }
  1148.     /**
  1149.      * Gets a string representation of this QueryBuilder which corresponds to
  1150.      * the final DQL query being constructed.
  1151.      */
  1152.     public function __toString(): string
  1153.     {
  1154.         return $this->getDQL();
  1155.     }
  1156.     /**
  1157.      * Deep clones all expression objects in the DQL parts.
  1158.      *
  1159.      * @return void
  1160.      */
  1161.     public function __clone()
  1162.     {
  1163.         foreach ($this->dqlParts as $part => $elements) {
  1164.             if (is_array($this->dqlParts[$part])) {
  1165.                 foreach ($this->dqlParts[$part] as $idx => $element) {
  1166.                     if (is_object($element)) {
  1167.                         $this->dqlParts[$part][$idx] = clone $element;
  1168.                     }
  1169.                 }
  1170.             } elseif (is_object($elements)) {
  1171.                 $this->dqlParts[$part] = clone $elements;
  1172.             }
  1173.         }
  1174.         $parameters = [];
  1175.         foreach ($this->parameters as $parameter) {
  1176.             $parameters[] = clone $parameter;
  1177.         }
  1178.         $this->parameters = new ArrayCollection($parameters);
  1179.     }
  1180. }