1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Ecodev\Felix\ORM\Query; |
6
|
|
|
|
7
|
|
|
use Doctrine\ORM\Query\AST\Functions\FunctionNode; |
8
|
|
|
use Doctrine\ORM\Query\AST\Literal; |
9
|
|
|
use Doctrine\ORM\Query\AST\Node; |
10
|
|
|
use Doctrine\ORM\Query\Lexer; |
11
|
|
|
use Doctrine\ORM\Query\Parser; |
12
|
|
|
use Doctrine\ORM\Query\SqlWalker; |
13
|
|
|
use Exception; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* A custom DQL function to be able to use `IN` clause but with native SQL sub-queries. |
17
|
|
|
* |
18
|
|
|
* This is especially useful when we want to benefit from DQL builder, paginator, |
19
|
|
|
* automatic ACL filter etc., but still have to have some advanced conditions in sub-queries. |
20
|
|
|
* |
21
|
|
|
* DQL must not be handwritten, but instead `self::dql()` should be used |
22
|
|
|
*/ |
23
|
|
|
class NativeIn extends FunctionNode |
24
|
|
|
{ |
25
|
|
|
private string|Node $field; |
26
|
|
|
|
27
|
|
|
private Literal $nativeQuery; |
28
|
|
|
|
29
|
|
|
private Literal $isNot; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Generate DQL `IN` clause with a native sub-query. |
33
|
|
|
* |
34
|
|
|
* @param string $field DQL for the field |
35
|
|
|
* @param string $nativeSql native SQL sub-query |
36
|
|
|
*/ |
37
|
|
|
public static function dql(string $field, string $nativeSql, bool $isNot = false): string |
38
|
|
|
{ |
39
|
|
|
$quotedNativeSql = "'" . str_replace("'", "''", $nativeSql) . "'"; |
40
|
|
|
|
41
|
|
|
return 'NATIVE_IN(' . $field . ', ' . $quotedNativeSql . ', ' . (int) $isNot . ') = TRUE'; |
42
|
|
|
} |
43
|
|
|
|
44
|
7 |
|
public function parse(Parser $parser): void |
45
|
|
|
{ |
46
|
7 |
|
$parser->match(Lexer::T_IDENTIFIER); |
47
|
7 |
|
$parser->match(Lexer::T_OPEN_PARENTHESIS); |
48
|
|
|
|
49
|
7 |
|
$this->field = $parser->ArithmeticPrimary(); |
50
|
7 |
|
$parser->match(Lexer::T_COMMA); |
51
|
|
|
|
52
|
7 |
|
$this->nativeQuery = $parser->Literal(); |
53
|
7 |
|
$parser->match(Lexer::T_COMMA); |
54
|
|
|
|
55
|
7 |
|
$this->isNot = $parser->Literal(); |
56
|
|
|
|
57
|
7 |
|
$parser->match(Lexer::T_CLOSE_PARENTHESIS); |
58
|
|
|
} |
59
|
|
|
|
60
|
7 |
|
public function getSql(SqlWalker $sqlWalker): string |
61
|
|
|
{ |
62
|
7 |
|
$field = is_string($this->field) ? $sqlWalker->walkResultVariable($this->field) : $this->field->dispatch($sqlWalker); |
63
|
7 |
|
$nativeSql = $this->nativeQuery->dispatch($sqlWalker); |
64
|
7 |
|
$nativeSql = preg_replace("~^'(.*)'$~", '\\1', $nativeSql); |
65
|
7 |
|
if ($nativeSql === null) { |
66
|
|
|
throw new Exception('Error while unquoting native SQL'); |
67
|
|
|
} |
68
|
|
|
|
69
|
7 |
|
$unquotedNativeSql = str_replace(["\\'", '\\n'], ["'", "\n"], $nativeSql); |
70
|
|
|
|
71
|
7 |
|
$isNot = $this->isNot->dispatch($sqlWalker); |
72
|
|
|
|
73
|
7 |
|
$sql = $field . ($isNot ? ' NOT' : '') . ' IN (' . $unquotedNativeSql . ')'; |
74
|
|
|
|
75
|
7 |
|
return $sql; |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|