1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Conia\Quma; |
6
|
|
|
|
7
|
|
|
use InvalidArgumentException; |
8
|
|
|
|
9
|
|
|
/** @psalm-api */ |
10
|
|
|
class Script |
11
|
|
|
{ |
12
|
|
|
protected $db; |
13
|
|
|
protected $script; |
14
|
|
|
protected $isTemplate; |
15
|
|
|
|
16
|
25 |
|
public function __construct(Database $db, string $script, bool $isTemplate) |
17
|
|
|
{ |
18
|
25 |
|
$this->db = $db; |
19
|
25 |
|
$this->script = $script; |
20
|
25 |
|
$this->isTemplate = $isTemplate; |
21
|
|
|
} |
22
|
|
|
|
23
|
1 |
|
public function __invoke(mixed ...$args): Query |
24
|
|
|
{ |
25
|
1 |
|
return $this->invoke(...$args); |
26
|
|
|
} |
27
|
|
|
|
28
|
25 |
|
public function invoke(mixed ...$argsArray): Query |
29
|
|
|
{ |
30
|
25 |
|
$args = new Args($argsArray); |
31
|
|
|
|
32
|
25 |
|
if ($this->isTemplate) { |
33
|
5 |
|
if ($args->type() === ArgType::Positional) { |
34
|
1 |
|
throw new InvalidArgumentException( |
35
|
1 |
|
'Template queries `*.sql.php` allow named parameters only' |
36
|
1 |
|
); |
37
|
|
|
} |
38
|
|
|
|
39
|
4 |
|
$script = $this->evaluateTemplate($this->script, $args); |
40
|
|
|
|
41
|
|
|
// We need to wrap the result of the prepare call in an array |
42
|
|
|
// to get back to the format of ...$argsArray. |
43
|
4 |
|
$args = new Args([$this->prepareTemplateVars($script, $args)]); |
44
|
|
|
} else { |
45
|
20 |
|
$script = $this->script; |
46
|
|
|
} |
47
|
|
|
|
48
|
24 |
|
return new Query($this->db, $script, $args); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** @psalm-suppress PossiblyUnusedParam - $path and $args are used but psalm complains anyway */ |
52
|
4 |
|
protected function evaluateTemplate(string $path, Args $args): string |
53
|
|
|
{ |
54
|
|
|
// Hide $path. Could be overwritten if 'path' exists in $args. |
55
|
4 |
|
$____template_path____ = $path; |
56
|
4 |
|
unset($path); |
57
|
|
|
|
58
|
4 |
|
extract(array_merge( |
59
|
|
|
// Add the pdo driver to args to allow dynamic |
60
|
|
|
// queries based on the platform. |
61
|
4 |
|
['pdodriver' => $this->db->getPdoDriver()], |
62
|
4 |
|
$args->get() |
63
|
4 |
|
)); |
64
|
|
|
|
65
|
4 |
|
ob_start(); |
66
|
|
|
|
67
|
|
|
/** @psalm-suppress UnresolvableInclude */ |
68
|
4 |
|
include $____template_path____; |
69
|
|
|
|
70
|
4 |
|
return ob_get_clean(); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Removes all keys from $params which are not present |
75
|
|
|
* in the $script. |
76
|
|
|
* |
77
|
|
|
* PDO does not allow unused parameters. |
78
|
|
|
*/ |
79
|
4 |
|
protected function prepareTemplateVars(string $script, Args $args): array |
80
|
|
|
{ |
81
|
|
|
// Remove PostgreSQL blocks |
82
|
4 |
|
$script = preg_replace(Query::PATTERN_BLOCK, ' ', $script); |
83
|
|
|
// Remove strings |
84
|
4 |
|
$script = preg_replace(Query::PATTERN_STRING, ' ', $script); |
85
|
|
|
// Remove /* */ comments |
86
|
4 |
|
$script = preg_replace(Query::PATTERN_COMMENT_MULTI, ' ', $script); |
87
|
|
|
// Remove single line comments |
88
|
4 |
|
$script = preg_replace(Query::PATTERN_COMMENT_SINGLE, ' ', $script); |
89
|
|
|
|
90
|
4 |
|
$newArgs = []; |
91
|
|
|
|
92
|
|
|
// Match everything starting with : and a letter. |
93
|
|
|
// Exclude multiple colons, like type casts (::text). |
94
|
|
|
// Would not find a var if it is at the very beginning of script. |
95
|
|
|
if ( |
96
|
4 |
|
preg_match_all( |
97
|
4 |
|
'/[^:]:[a-zA-Z][a-zA-Z0-9_]*/', |
98
|
4 |
|
$script, |
99
|
4 |
|
$result, |
100
|
4 |
|
PREG_PATTERN_ORDER |
101
|
4 |
|
) |
102
|
|
|
) { |
103
|
3 |
|
$argsArray = $args->get(); |
104
|
3 |
|
$newArgs = []; |
105
|
|
|
|
106
|
3 |
|
foreach (array_unique($result[0]) as $arg) { |
107
|
3 |
|
$a = substr($arg, 2); |
108
|
|
|
assert(!empty($a)); |
109
|
|
|
|
110
|
|
|
/** @psalm-var array<non-empty-string, mixed> */ |
111
|
3 |
|
$newArgs[$a] = $argsArray[$a]; |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
4 |
|
return $newArgs; |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
|