Skip to content

Commit e111dd2

Browse files
refactor: allow multiple node mapping patterns to be used and their arguments to be validated in variadic functions (#350)
1 parent 6a5ba9e commit e111dd2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+525
-255
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fixtures\MartinGeorgiev\Doctrine\Function;
6+
7+
use Doctrine\ORM\Query\AST\Literal;
8+
use Doctrine\ORM\Query\AST\Node;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
10+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
11+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
12+
13+
/**
14+
* A concrete implementation of BaseVariadicFunction for testing purposes.
15+
*/
16+
class TestVariadicFunction extends BaseVariadicFunction
17+
{
18+
use BooleanValidationTrait;
19+
20+
protected function getNodeMappingPattern(): array
21+
{
22+
return ['StringPrimary'];
23+
}
24+
25+
protected function getFunctionName(): string
26+
{
27+
return 'test_function';
28+
}
29+
30+
protected function getMinArgumentCount(): int
31+
{
32+
return 2;
33+
}
34+
35+
protected function getMaxArgumentCount(): int
36+
{
37+
return 10;
38+
}
39+
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Arr.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

7-
use Doctrine\ORM\Query\AST\Node;
8-
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9-
107
/**
118
* Implementation of PostgreSQL ARRAY[].
129
*
@@ -22,16 +19,23 @@ protected function getNodeMappingPattern(): array
2219
return ['StringPrimary'];
2320
}
2421

22+
protected function getFunctionName(): string
23+
{
24+
return 'ARRAY';
25+
}
26+
2527
protected function customizeFunction(): void
2628
{
27-
$this->setFunctionPrototype('ARRAY[%s]');
29+
$this->setFunctionPrototype(\sprintf('%s[%%s]', $this->getFunctionName()));
30+
}
31+
32+
protected function getMinArgumentCount(): int
33+
{
34+
return 1;
2835
}
2936

30-
protected function validateArguments(Node ...$arguments): void
37+
protected function getMaxArgumentCount(): int
3138
{
32-
$argumentCount = \count($arguments);
33-
if ($argumentCount === 0) {
34-
throw InvalidArgumentForVariadicFunctionException::atLeast('ARRAY', 1);
35-
}
39+
return PHP_INT_MAX; // No upper limit
3640
}
3741
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Doctrine\ORM\Query\AST\Node;
8-
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
98
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
109

1110
/**
@@ -31,21 +30,28 @@ protected function getNodeMappingPattern(): array
3130
return ['StringPrimary'];
3231
}
3332

34-
protected function customizeFunction(): void
33+
protected function getFunctionName(): string
3534
{
36-
$this->setFunctionPrototype('array_to_json(%s)');
35+
return 'array_to_json';
36+
}
37+
38+
protected function getMinArgumentCount(): int
39+
{
40+
return 1;
41+
}
42+
43+
protected function getMaxArgumentCount(): int
44+
{
45+
return 2;
3746
}
3847

3948
protected function validateArguments(Node ...$arguments): void
4049
{
41-
$argumentCount = \count($arguments);
42-
if ($argumentCount < 1 || $argumentCount > 2) {
43-
throw InvalidArgumentForVariadicFunctionException::between('array_to_json', 1, 2);
44-
}
50+
parent::validateArguments(...$arguments);
4551

4652
// Validate that the second parameter is a valid boolean if provided
47-
if ($argumentCount === 2) {
48-
$this->validateBoolean($arguments[1], 'ARRAY_TO_JSON');
53+
if (\count($arguments) === 2) {
54+
$this->validateBoolean($arguments[1], $this->getFunctionName());
4955
}
5056
}
5157
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseComparisonFunction.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,14 @@ protected function getNodeMappingPattern(): array
1515
{
1616
return ['SimpleArithmeticExpression'];
1717
}
18+
19+
protected function getMinArgumentCount(): int
20+
{
21+
return 2;
22+
}
23+
24+
protected function getMaxArgumentCount(): int
25+
{
26+
return PHP_INT_MAX; // No hard limit apart the PHP internals
27+
}
1828
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunction.php

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@
1919
*/
2020
abstract class BaseVariadicFunction extends BaseFunction
2121
{
22+
protected function customizeFunction(): void
23+
{
24+
$this->setFunctionPrototype(\sprintf('%s(%%s)', $this->getFunctionName()));
25+
}
26+
27+
abstract protected function getFunctionName(): string;
28+
2229
/**
23-
* @return array<int|string, string>
30+
* @return array<string>
2431
*/
2532
abstract protected function getNodeMappingPattern(): array;
2633

27-
/**
28-
* @throws ParserException
29-
*/
34+
abstract protected function getMinArgumentCount(): int;
35+
36+
abstract protected function getMaxArgumentCount(): int;
37+
3038
protected function feedParserWithNodes(Parser $parser): void
3139
{
3240
foreach ($this->getNodeMappingPattern() as $nodeMappingPattern) {
@@ -38,22 +46,24 @@ protected function feedParserWithNodes(Parser $parser): void
3846
// swallow and continue with next pattern
3947
}
4048
}
41-
42-
$this->validateArguments(...$this->nodes); // @phpstan-ignore-line
4349
}
4450

51+
/**
52+
* @throws InvalidArgumentForVariadicFunctionException
53+
* @throws ParserException
54+
*/
4555
private function feedParserWithNodesForNodeMappingPattern(Parser $parser, string $nodeMappingPattern): void
4656
{
4757
$nodeMapping = \explode(',', $nodeMappingPattern);
4858
$lexer = $parser->getLexer();
4959

5060
try {
51-
// @phpstan-ignore-next-line
52-
$this->nodes[] = $parser->{$nodeMapping[0]}();
5361
$lookaheadType = DoctrineLexer::getLookaheadType($lexer);
5462
if ($lookaheadType === null) {
55-
throw ParserException::missingLookaheadType();
63+
throw InvalidArgumentForVariadicFunctionException::atLeast($this->getFunctionName(), $this->getMinArgumentCount());
5664
}
65+
66+
$this->nodes[] = $parser->{$nodeMapping[0]}(); // @phpstan-ignore-line
5767
} catch (\Throwable $throwable) {
5868
throw ParserException::withThrowable($throwable);
5969
}
@@ -64,13 +74,53 @@ private function feedParserWithNodesForNodeMappingPattern(Parser $parser, string
6474
while (($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS) !== $lookaheadType) {
6575
if (($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA) === $lookaheadType) {
6676
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);
67-
// @phpstan-ignore-next-line
68-
$this->nodes[] = $parser->{$nodeMapping[$isNodeMappingASimplePattern ? 0 : $nodeIndex]}();
77+
78+
// Check if we're about to exceed the maximum number of arguments
79+
// nodeIndex starts at 1 and counts up for each argument after the first
80+
// So when nodeIndex=1, we're about to add the 2nd argument (total: 2)
81+
// When nodeIndex=2, we're about to add the 3rd argument (total: 3)
82+
$foundMoreNodesThanMappingExpected = ($nodeIndex + 1) > $this->getMaxArgumentCount();
83+
if ($foundMoreNodesThanMappingExpected) {
84+
throw InvalidArgumentForVariadicFunctionException::between($this->getFunctionName(), $this->getMinArgumentCount(), $this->getMaxArgumentCount());
85+
}
86+
87+
$expectedNodeIndex = $isNodeMappingASimplePattern ? 0 : $nodeIndex;
88+
$argumentCountExceedsMappingPatternExpectation = !\array_key_exists($expectedNodeIndex, $nodeMapping);
89+
if ($argumentCountExceedsMappingPatternExpectation) {
90+
throw InvalidArgumentForVariadicFunctionException::unsupportedCombination(
91+
$this->getFunctionName(),
92+
\count($this->nodes) + 1,
93+
'implementation defines fewer node mappings than the actually provided argument count'
94+
);
95+
}
96+
97+
$this->nodes[] = $parser->{$nodeMapping[$expectedNodeIndex]}(); // @phpstan-ignore-line
6998
$nodeIndex++;
7099
}
71100

72101
$lookaheadType = DoctrineLexer::getLookaheadType($lexer);
73102
}
103+
104+
// Final validation ensures all arguments meet requirements, including any special rules in subclass implementations
105+
$this->validateArguments(...$this->nodes); // @phpstan-ignore-line
106+
}
107+
108+
/**
109+
* @throws InvalidArgumentForVariadicFunctionException
110+
*/
111+
protected function validateArguments(Node ...$arguments): void
112+
{
113+
$minArgumentCount = $this->getMinArgumentCount();
114+
$maxArgumentCount = $this->getMaxArgumentCount();
115+
$argumentCount = \count($arguments);
116+
117+
if ($argumentCount < $minArgumentCount) {
118+
throw InvalidArgumentForVariadicFunctionException::atLeast($this->getFunctionName(), $this->getMinArgumentCount());
119+
}
120+
121+
if ($argumentCount > $maxArgumentCount) {
122+
throw InvalidArgumentForVariadicFunctionException::between($this->getFunctionName(), $this->getMinArgumentCount(), $this->getMaxArgumentCount());
123+
}
74124
}
75125

76126
public function getSql(SqlWalker $sqlWalker): string
@@ -82,11 +132,4 @@ public function getSql(SqlWalker $sqlWalker): string
82132

83133
return \sprintf($this->functionPrototype, \implode(', ', $dispatched));
84134
}
85-
86-
/**
87-
* Validates the arguments passed to the function.
88-
*
89-
* @throws InvalidArgumentForVariadicFunctionException
90-
*/
91-
abstract protected function validateArguments(Node ...$arguments): void;
92135
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DateAdd.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Doctrine\ORM\Query\AST\Node;
8-
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
98
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\TimezoneValidationTrait;
109

1110
/**
@@ -30,21 +29,28 @@ protected function getNodeMappingPattern(): array
3029
return ['StringPrimary'];
3130
}
3231

33-
protected function customizeFunction(): void
32+
protected function getFunctionName(): string
3433
{
35-
$this->setFunctionPrototype('date_add(%s)');
34+
return 'date_add';
35+
}
36+
37+
protected function getMinArgumentCount(): int
38+
{
39+
return 2;
40+
}
41+
42+
protected function getMaxArgumentCount(): int
43+
{
44+
return 3;
3645
}
3746

3847
protected function validateArguments(Node ...$arguments): void
3948
{
40-
$argumentCount = \count($arguments);
41-
if ($argumentCount < 2 || $argumentCount > 3) {
42-
throw InvalidArgumentForVariadicFunctionException::between('date_add', 2, 3);
43-
}
49+
parent::validateArguments(...$arguments);
4450

4551
// Validate that the third parameter is a valid timezone if provided
46-
if ($argumentCount === 3) {
47-
$this->validateTimezone($arguments[2], 'DATE_ADD');
52+
if (\count($arguments) === 3) {
53+
$this->validateTimezone($arguments[2], $this->getFunctionName());
4854
}
4955
}
5056
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DateSubtract.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Doctrine\ORM\Query\AST\Node;
8-
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
98
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\TimezoneValidationTrait;
109

1110
/**
@@ -30,21 +29,28 @@ protected function getNodeMappingPattern(): array
3029
return ['StringPrimary'];
3130
}
3231

33-
protected function customizeFunction(): void
32+
protected function getFunctionName(): string
3433
{
35-
$this->setFunctionPrototype('date_subtract(%s)');
34+
return 'date_subtract';
35+
}
36+
37+
protected function getMinArgumentCount(): int
38+
{
39+
return 2;
40+
}
41+
42+
protected function getMaxArgumentCount(): int
43+
{
44+
return 3;
3645
}
3746

3847
protected function validateArguments(Node ...$arguments): void
3948
{
40-
$argumentCount = \count($arguments);
41-
if ($argumentCount < 2 || $argumentCount > 3) {
42-
throw InvalidArgumentForVariadicFunctionException::between('date_subtract', 2, 3);
43-
}
49+
parent::validateArguments(...$arguments);
4450

4551
// Validate that the third parameter is a valid timezone if provided
46-
if ($argumentCount === 3) {
47-
$this->validateTimezone($arguments[2], 'DATE_SUBTRACT');
52+
if (\count($arguments) === 3) {
53+
$this->validateTimezone($arguments[2], $this->getFunctionName());
4854
}
4955
}
5056
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidArgumentForVariadicFunctionException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ public static function evenNumber(string $functionName): self
4343
$functionName
4444
));
4545
}
46+
47+
public static function unsupportedCombination(string $functionName, int $count, string $note): self
48+
{
49+
return new self(\sprintf(
50+
'%s() cannot be called with %d arguments, because %s',
51+
$functionName,
52+
$count,
53+
$note,
54+
));
55+
}
4656
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Greatest.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

7-
use Doctrine\ORM\Query\AST\Node;
8-
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9-
107
/**
118
* Implementation of PostgreSQL GREATEST().
129
*
@@ -17,16 +14,8 @@
1714
*/
1815
class Greatest extends BaseComparisonFunction
1916
{
20-
protected function customizeFunction(): void
21-
{
22-
$this->setFunctionPrototype('greatest(%s)');
23-
}
24-
25-
protected function validateArguments(Node ...$arguments): void
17+
protected function getFunctionName(): string
2618
{
27-
$argumentCount = \count($arguments);
28-
if ($argumentCount < 2) {
29-
throw InvalidArgumentForVariadicFunctionException::atLeast('greatest', 2);
30-
}
19+
return 'greatest';
3120
}
3221
}

0 commit comments

Comments
 (0)