Skip to content

Commit 587caf8

Browse files
committed
Update DQL arbitrary joins to use the ON keyword instead of WITH
DQL arbitrary joins are semantically equivalent to SQL joins, so using the same keyword reduces confusion. It also means that in next major version, the WITH keyword will only be about applying adhoc filtering on relations instead of having 2 responsibilities.
1 parent 28d9472 commit 587caf8

File tree

14 files changed

+77
-41
lines changed

14 files changed

+77
-41
lines changed

UPGRADE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ Using `Doctrine\ORM\QueryBuilder::add('join', ...)` with a list of join parts
6767
is deprecated in favor of using an associative array of join parts with the
6868
root alias as key.
6969

70+
## Deprecate using the `WITH` keyword for arbitrary DQL joins
71+
72+
Using the `WITH` keyword to specify the condition for an arbitrary DQL join is
73+
deprecated in favor of using the `ON` keyword (similar to the SQL syntax for
74+
joins).
75+
The `WITH` keyword is now meant to be used only for filtering conditions in
76+
association joins.
77+
7078
# Upgrade to 3.5
7179

7280
See the General notes to upgrading to 3.x versions above.

docs/en/reference/dql-doctrine-query-language.rst

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ where you can generate an arbitrary join with the following syntax:
490490
.. code-block:: php
491491
492492
<?php
493-
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b WITH u.email = b.email');
493+
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b ON u.email = b.email');
494494
495495
With an arbitrary join the result differs from the joins using a mapped property.
496496
The result of an arbitrary join is an one dimensional array with a mix of the entity from the ``SELECT``
@@ -513,13 +513,15 @@ it loads all the related ``Banlist`` objects corresponding to this ``User``. Thi
513513
when the DQL is switched to an arbitrary join.
514514

515515
.. note::
516-
The differences between WHERE, WITH and HAVING clauses may be
516+
The differences between WHERE, WITH, ON and HAVING clauses may be
517517
confusing.
518518

519519
- WHERE is applied to the results of an entire query
520-
- WITH is applied to a join as an additional condition. For
521-
arbitrary joins (SELECT f, b FROM Foo f, Bar b WITH f.id = b.id)
522-
the WITH is required, even if it is 1 = 1
520+
- ON is applied to arbitrary joins as the join condition. For
521+
arbitrary joins (SELECT f, b FROM Foo f, Bar b ON f.id = b.id)
522+
the ON is required, even if it is 1 = 1. WITH is also
523+
supported as alternative keyword for that case for BC reasons.
524+
- WITH is applied to an association join as an additional condition.
523525
- HAVING is applied to the results of a query after
524526
aggregation (GROUP BY)
525527

@@ -1699,9 +1701,14 @@ From, Join and Index by
16991701
SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
17001702
RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
17011703
JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1702-
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration | RangeVariableDeclaration) ["WITH" ConditionalExpression]
1704+
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
17031705
IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
17041706
1707+
.. note::
1708+
Using the ``WITH`` keyword for the ``ConditionalExpression`` of a
1709+
``RangeVariableDeclaration`` is deprecated and will be removed in
1710+
ORM 4.0. Use the ``ON`` keyword instead.
1711+
17051712
Select Expressions
17061713
~~~~~~~~~~~~~~~~~~
17071714

src/Query/Parser.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,8 +1609,7 @@ public function SubselectIdentificationVariableDeclaration(): AST\Identification
16091609

16101610
/**
16111611
* Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1612-
* (JoinAssociationDeclaration | RangeVariableDeclaration)
1613-
* ["WITH" ConditionalExpression]
1612+
* (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
16141613
*/
16151614
public function Join(): AST\Join
16161615
{
@@ -1644,22 +1643,32 @@ public function Join(): AST\Join
16441643

16451644
$next = $this->lexer->glimpse();
16461645
assert($next !== null);
1647-
$joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1648-
$adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);
1649-
$join = new AST\Join($joinType, $joinDeclaration);
1646+
$conditionalExpression = null;
16501647

1651-
// Describe non-root join declaration
1652-
if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1653-
$joinDeclaration->isRoot = false;
1654-
}
1648+
if ($next->type === TokenType::T_DOT) {
1649+
$joinDeclaration = $this->JoinAssociationDeclaration();
16551650

1656-
// Check for ad-hoc Join conditions
1657-
if ($adhocConditions) {
1658-
$this->match(TokenType::T_WITH);
1651+
if ($this->lexer->isNextToken(TokenType::T_WITH)) {
1652+
$this->match(TokenType::T_WITH);
1653+
$conditionalExpression = $this->ConditionalExpression();
1654+
}
1655+
} else {
1656+
$joinDeclaration = $this->RangeVariableDeclaration();
1657+
$joinDeclaration->isRoot = false;
16591658

1660-
$join->conditionalExpression = $this->ConditionalExpression();
1659+
if ($this->lexer->isNextToken(TokenType::T_ON)) {
1660+
$this->match(TokenType::T_ON);
1661+
$conditionalExpression = $this->ConditionalExpression();
1662+
} elseif ($this->lexer->isNextToken(TokenType::T_WITH)) {
1663+
$this->match(TokenType::T_WITH);
1664+
$conditionalExpression = $this->ConditionalExpression();
1665+
Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/issues/12192', 'Using WITH for the join condition of arbitrary joins is deprecated. Use ON instead.');
1666+
}
16611667
}
16621668

1669+
$join = new AST\Join($joinType, $joinDeclaration);
1670+
$join->conditionalExpression = $conditionalExpression;
1671+
16631672
return $join;
16641673
}
16651674

src/Query/TokenType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,5 @@ enum TokenType: int
9090
case T_WHERE = 255;
9191
case T_WITH = 256;
9292
case T_NAMED = 257;
93+
case T_ON = 258;
9394
}

tests/Tests/ORM/Functional/QueryTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ public function testToIterableWithMultipleSelectElements(): void
390390
$this->_em->flush();
391391
$this->_em->clear();
392392

393-
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u WITH a.user = u');
393+
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u ON a.user = u');
394394

395395
$result = iterator_to_array($query->toIterable());
396396

@@ -1059,7 +1059,7 @@ public function testMultipleJoinComponentsUsingInnerJoin(): void
10591059
$query = $this->_em->createQuery('
10601060
SELECT u, p
10611061
FROM Doctrine\Tests\Models\CMS\CmsUser u
1062-
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
1062+
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
10631063
');
10641064
$users = $query->execute();
10651065

@@ -1092,7 +1092,7 @@ public function testMultipleJoinComponentsUsingLeftJoin(): void
10921092
$query = $this->_em->createQuery('
10931093
SELECT u, p
10941094
FROM Doctrine\Tests\Models\CMS\CmsUser u
1095-
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
1095+
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
10961096
');
10971097
$users = $query->execute();
10981098

tests/Tests/ORM/Functional/Ticket/DDC3042Test.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function testSQLGenerationDoesNotProvokeAliasCollisions(): void
3131
$this
3232
->_em
3333
->createQuery(
34-
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b WITH 1 = 1',
34+
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b ON 1 = 1',
3535
)
3636
->getSQL(),
3737
'field_11',

tests/Tests/ORM/Functional/Ticket/GH6362Test.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected function setUp(): void
3939
* SELECT a as base, b, c, d
4040
* FROM Start a
4141
* LEFT JOIN a.bases b
42-
* LEFT JOIN Child c WITH b.id = c.id
42+
* LEFT JOIN Child c ON b.id = c.id
4343
* LEFT JOIN c.joins d
4444
*/
4545
#[Group('GH-6362')]

tests/Tests/ORM/Functional/Ticket/GH6464Test.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function testIssue(): void
3939
$query = $this->_em->createQueryBuilder()
4040
->select('p')
4141
->from(GH6464Post::class, 'p')
42-
->innerJoin(GH6464Author::class, 'a', 'WITH', 'p.authorId = a.id')
42+
->innerJoin(GH6464Author::class, 'a', 'ON', 'p.authorId = a.id')
4343
->getQuery();
4444

4545
self::assertDoesNotMatchRegularExpression(

tests/Tests/ORM/Functional/Ticket/GH7496WithToIterableTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected function setUp(): void
4040
public function testNonUniqueObjectHydrationDuringIteration(): void
4141
{
4242
$q = $this->_em->createQuery(
43-
'SELECT b FROM ' . GH7496EntityAinB::class . ' aib JOIN ' . GH7496EntityB::class . ' b WITH aib.eB = b',
43+
'SELECT b FROM ' . GH7496EntityAinB::class . ' aib JOIN ' . GH7496EntityB::class . ' b ON aib.eB = b',
4444
);
4545

4646
$bs = IterableTester::iterableToArray(

tests/Tests/ORM/Query/LanguageRecognitionTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\Tests\ORM\Query;
66

7+
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
78
use Doctrine\ORM\AbstractQuery;
89
use Doctrine\ORM\EntityManagerInterface;
910
use Doctrine\ORM\Mapping\Column;
@@ -20,9 +21,12 @@
2021
use Doctrine\Tests\OrmTestCase;
2122
use PHPUnit\Framework\Attributes\DataProvider;
2223
use PHPUnit\Framework\Attributes\Group;
24+
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
2325

2426
class LanguageRecognitionTest extends OrmTestCase
2527
{
28+
use VerifyDeprecations;
29+
2630
private EntityManagerInterface $entityManager;
2731
private int $hydrationMode = AbstractQuery::HYDRATE_OBJECT;
2832

@@ -262,8 +266,15 @@ public function testMixingOfJoins(): void
262266
$this->assertValidDQL('SELECT u.name, a.topic, p.phonenumber FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a LEFT JOIN u.phonenumbers p');
263267
}
264268

269+
public function testJoinClassPathUsingON(): void
270+
{
271+
$this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a ON a.user = u.id');
272+
}
273+
274+
#[IgnoreDeprecations]
265275
public function testJoinClassPathUsingWITH(): void
266276
{
277+
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12192');
267278
$this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a WITH a.user = u.id');
268279
}
269280

@@ -638,7 +649,7 @@ public function testHavingSupportIsNullExpression(): void
638649
#[Group('DDC-3085')]
639650
public function testHavingSupportResultVariableInNullComparisonExpression(): void
640651
{
641-
$this->assertValidDQL('SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a WITH a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5');
652+
$this->assertValidDQL('SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a ON a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5');
642653
}
643654

644655
#[Group('DDC-1858')]

0 commit comments

Comments
 (0)