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

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