Skip to content

Commit 0bc9c35

Browse files
committed
Deprecate TypedExpression in favor of ExpressionWithReturnType
`TypedExpression::getReturnType()` returns a `Doctrine\DBAL\Types\Type` instance, which forces implementers to perform a static, global type lookup (`Type::getType()`). `SqlWalker` then converts the instance back to a name via `TypeRegistry::lookupName()`, making the round-trip pointless and introducing two static `Type::*` calls that block moving the ORM to instance-based type registries. Introduce `Doctrine\ORM\Query\AST\ExpressionWithReturnType`, which exposes the DBAL type name directly (`getReturnTypeName(): string`). `SqlWalker` prefers the new interface, falls back to the legacy one with a deprecation notice, and finally to the `'string'` default. `CountFunction` and `LengthFunction` now implement both interfaces: `getReturnTypeName()` returns a plain `Types::*` constant, and the legacy `getReturnType()` delegates and is marked `@deprecated`.
1 parent db722a0 commit 0bc9c35

7 files changed

Lines changed: 202 additions & 11 deletions

File tree

UPGRADE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ and directly start using native lazy objects.
2929

3030
# Upgrade to 3.7
3131

32+
## Deprecated `Doctrine\ORM\Query\AST\TypedExpression`
33+
34+
Implement `Doctrine\ORM\Query\AST\ExpressionWithReturnType` instead, which exposes
35+
the DBAL type name directly.
36+
37+
```diff
38+
-use Doctrine\DBAL\Types\Type;
39+
use Doctrine\DBAL\Types\Types;
40+
-use Doctrine\ORM\Query\AST\TypedExpression;
41+
+use Doctrine\ORM\Query\AST\ExpressionWithReturnType;
42+
43+
-class MyFunction extends FunctionNode implements TypedExpression
44+
+class MyFunction extends FunctionNode implements ExpressionWithReturnType
45+
{
46+
- public function getReturnType(): Type
47+
+ public function getReturnTypeName(): string
48+
{
49+
- return Type::getType(Types::INTEGER);
50+
+ return Types::INTEGER;
51+
}
52+
}
53+
```
54+
55+
Libraries that need to support older ORM versions can safely implement both
56+
interfaces at the same time: `SqlWalker` prefers `ExpressionWithReturnType`
57+
when available and no deprecation is triggered.
58+
3259
## Deprecated using strings or null as sort directions
3360

3461
PHP 8.6 provides a native `\SortDirection` enum that should be used instead of
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Query\AST;
6+
7+
/**
8+
* Provides an API for resolving the DBAL type name of a Node.
9+
*/
10+
interface ExpressionWithReturnType
11+
{
12+
/**
13+
* Returns the DBAL type name (see {@see \Doctrine\DBAL\Types\Types}) of
14+
* the value produced by this expression.
15+
*/
16+
public function getReturnTypeName(): string;
17+
}

src/Query/AST/Functions/CountFunction.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
use Doctrine\DBAL\Types\Type;
88
use Doctrine\DBAL\Types\Types;
99
use Doctrine\ORM\Query\AST\AggregateExpression;
10+
use Doctrine\ORM\Query\AST\ExpressionWithReturnType;
1011
use Doctrine\ORM\Query\AST\TypedExpression;
1112
use Doctrine\ORM\Query\Parser;
1213
use Doctrine\ORM\Query\SqlWalker;
1314

1415
/**
1516
* "COUNT" "(" ["DISTINCT"] StringPrimary ")"
17+
*
18+
* @phpstan-ignore class.implementsDeprecatedInterface
1619
*/
17-
final class CountFunction extends FunctionNode implements TypedExpression
20+
final class CountFunction extends FunctionNode implements ExpressionWithReturnType, TypedExpression
1821
{
1922
private AggregateExpression $aggregateExpression;
2023

@@ -28,8 +31,14 @@ public function parse(Parser $parser): void
2831
$this->aggregateExpression = $parser->AggregateExpression();
2932
}
3033

34+
public function getReturnTypeName(): string
35+
{
36+
return Types::INTEGER;
37+
}
38+
39+
/** @deprecated Use {@see getReturnTypeName()} instead. */
3140
public function getReturnType(): Type
3241
{
33-
return Type::getType(Types::INTEGER);
42+
return Type::getType($this->getReturnTypeName());
3443
}
3544
}

src/Query/AST/Functions/LengthFunction.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Doctrine\DBAL\Types\Type;
88
use Doctrine\DBAL\Types\Types;
9+
use Doctrine\ORM\Query\AST\ExpressionWithReturnType;
910
use Doctrine\ORM\Query\AST\Node;
1011
use Doctrine\ORM\Query\AST\TypedExpression;
1112
use Doctrine\ORM\Query\Parser;
@@ -16,8 +17,10 @@
1617
* "LENGTH" "(" StringPrimary ")"
1718
*
1819
* @link www.doctrine-project.org
20+
*
21+
* @phpstan-ignore class.implementsDeprecatedInterface
1922
*/
20-
class LengthFunction extends FunctionNode implements TypedExpression
23+
class LengthFunction extends FunctionNode implements ExpressionWithReturnType, TypedExpression
2124
{
2225
public Node $stringPrimary;
2326

@@ -38,8 +41,14 @@ public function parse(Parser $parser): void
3841
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
3942
}
4043

44+
public function getReturnTypeName(): string
45+
{
46+
return Types::INTEGER;
47+
}
48+
49+
/** @deprecated Use {@see getReturnTypeName()} instead. */
4150
public function getReturnType(): Type
4251
{
43-
return Type::getType(Types::INTEGER);
52+
return Type::getType($this->getReturnTypeName());
4453
}
4554
}

src/Query/AST/TypedExpression.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
use Doctrine\DBAL\Types\Type;
88

99
/**
10-
* Provides an API for resolving the type of a Node
10+
* Provides an API for resolving the type of a Node.
11+
*
12+
* @deprecated Implement {@see ExpressionWithReturnType} instead, which returns
13+
* the type name as a string and avoids a static Type lookup.
1114
*/
1215
interface TypedExpression
1316
{

src/Query/SqlWalker.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Doctrine\DBAL\LockMode;
1010
use Doctrine\DBAL\Platforms\AbstractPlatform;
1111
use Doctrine\DBAL\Types\TypeRegistry;
12+
use Doctrine\DBAL\Types\Types;
1213
use Doctrine\Deprecations\Deprecation;
1314
use Doctrine\ORM\EntityManagerInterface;
1415
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -1340,15 +1341,30 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st
13401341
break;
13411342
}
13421343

1343-
if (! $expr instanceof Query\AST\TypedExpression) {
1344-
// Conceptually we could resolve field type here by traverse through AST to retrieve field type,
1345-
// but this is not a feasible solution; assume 'string'.
1346-
$this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1344+
if ($expr instanceof Query\AST\ExpressionWithReturnType) {
1345+
$this->rsm->addScalarResult($columnAlias, $resultAlias, $expr->getReturnTypeName());
13471346

13481347
break;
13491348
}
13501349

1351-
$this->rsm->addScalarResult($columnAlias, $resultAlias, $this->typeRegistry->lookupName($expr->getReturnType()));
1350+
if ($expr instanceof Query\AST\TypedExpression) {
1351+
Deprecation::trigger(
1352+
'doctrine/orm',
1353+
'https://github.com/doctrine/orm/pull/12543',
1354+
'Implementing %s is deprecated, implement %s instead.',
1355+
Query\AST\TypedExpression::class, // @phpstan-ignore classConstant.deprecatedInterface
1356+
Query\AST\ExpressionWithReturnType::class,
1357+
);
1358+
1359+
// @phpstan-ignore method.deprecatedInterface
1360+
$this->rsm->addScalarResult($columnAlias, $resultAlias, $this->typeRegistry->lookupName($expr->getReturnType()));
1361+
1362+
break;
1363+
}
1364+
1365+
// Conceptually we could resolve field type here by traverse through AST to retrieve field type,
1366+
// but this is not a feasible solution; assume 'string'.
1367+
$this->rsm->addScalarResult($columnAlias, $resultAlias, Types::STRING);
13521368

13531369
break;
13541370

@@ -1362,7 +1378,7 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st
13621378

13631379
if (! $hidden) {
13641380
// We cannot resolve field type here; assume 'string'.
1365-
$this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1381+
$this->rsm->addScalarResult($columnAlias, $resultAlias, Types::STRING);
13661382
}
13671383

13681384
break;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Query;
6+
7+
use Doctrine\DBAL\Types\Type;
8+
use Doctrine\DBAL\Types\Types;
9+
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
10+
use Doctrine\ORM\EntityManagerInterface;
11+
use Doctrine\ORM\Query\AST\ExpressionWithReturnType;
12+
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
13+
use Doctrine\ORM\Query\AST\Node;
14+
use Doctrine\ORM\Query\AST\TypedExpression;
15+
use Doctrine\ORM\Query\Parser;
16+
use Doctrine\ORM\Query\SqlWalker;
17+
use Doctrine\ORM\Query\TokenType;
18+
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
19+
use Doctrine\Tests\OrmTestCase;
20+
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
21+
22+
class TypedExpressionDeprecationTest extends OrmTestCase
23+
{
24+
use VerifyDeprecations;
25+
26+
private EntityManagerInterface $entityManager;
27+
28+
protected function setUp(): void
29+
{
30+
$this->entityManager = $this->getTestEntityManager();
31+
}
32+
33+
#[IgnoreDeprecations]
34+
public function testImplementingLegacyTypedExpressionTriggersDeprecation(): void
35+
{
36+
$this->entityManager
37+
->getConfiguration()
38+
->addCustomNumericFunction('LEGACY_TYPED', LegacyTypedFunctionStub::class);
39+
40+
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12543');
41+
42+
$this->entityManager
43+
->createQuery('SELECT LEGACY_TYPED(p.phonenumber) FROM ' . CmsPhonenumber::class . ' p')
44+
->getSQL();
45+
}
46+
47+
public function testImplementingExpressionWithReturnTypeDoesNotTriggerDeprecation(): void
48+
{
49+
$this->entityManager
50+
->getConfiguration()
51+
->addCustomNumericFunction('MODERN_TYPED', ModernTypedFunctionStub::class);
52+
53+
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12543');
54+
55+
$this->entityManager
56+
->createQuery('SELECT MODERN_TYPED(p.phonenumber) FROM ' . CmsPhonenumber::class . ' p')
57+
->getSQL();
58+
}
59+
}
60+
61+
final class LegacyTypedFunctionStub extends FunctionNode implements TypedExpression
62+
{
63+
private Node $arithmeticExpression;
64+
65+
public function getSql(SqlWalker $sqlWalker): string
66+
{
67+
return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression($this->arithmeticExpression) . ')';
68+
}
69+
70+
public function parse(Parser $parser): void
71+
{
72+
$parser->match(TokenType::T_IDENTIFIER);
73+
$parser->match(TokenType::T_OPEN_PARENTHESIS);
74+
$this->arithmeticExpression = $parser->SimpleArithmeticExpression();
75+
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
76+
}
77+
78+
public function getReturnType(): Type
79+
{
80+
return Type::getType(Types::INTEGER);
81+
}
82+
}
83+
84+
final class ModernTypedFunctionStub extends FunctionNode implements ExpressionWithReturnType, TypedExpression
85+
{
86+
private Node $arithmeticExpression;
87+
88+
public function getSql(SqlWalker $sqlWalker): string
89+
{
90+
return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression($this->arithmeticExpression) . ')';
91+
}
92+
93+
public function parse(Parser $parser): void
94+
{
95+
$parser->match(TokenType::T_IDENTIFIER);
96+
$parser->match(TokenType::T_OPEN_PARENTHESIS);
97+
$this->arithmeticExpression = $parser->SimpleArithmeticExpression();
98+
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
99+
}
100+
101+
public function getReturnTypeName(): string
102+
{
103+
return Types::INTEGER;
104+
}
105+
106+
public function getReturnType(): Type
107+
{
108+
return Type::getType($this->getReturnTypeName());
109+
}
110+
}

0 commit comments

Comments
 (0)