Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
70 / 70 |
|
90.00% |
54 / 60 |
|
3.22% |
14 / 435 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Script | |
100.00% |
70 / 70 |
|
90.00% |
54 / 60 |
|
3.22% |
14 / 435 |
|
100.00% |
8 / 8 |
591.58 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __invoke | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| invoke | |
100.00% |
12 / 12 |
|
100.00% |
8 / 8 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
4.25 | |||
| evaluateTemplate | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| buildTemplateContext | |
100.00% |
8 / 8 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| renderTemplateFile | |
100.00% |
10 / 10 |
|
83.33% |
5 / 6 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
3.33 | |||
| renderTemplateSource | |
100.00% |
6 / 6 |
|
100.00% |
7 / 7 |
|
0.00% |
0 / 3 |
|
100.00% |
1 / 1 |
4 | |||
| prepareTemplateVars | |
100.00% |
22 / 22 |
|
80.77% |
21 / 26 |
|
0.48% |
2 / 416 |
|
100.00% |
1 / 1 |
41.48 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Quma; |
| 6 | |
| 7 | use Closure; |
| 8 | use InvalidArgumentException; |
| 9 | use RuntimeException; |
| 10 | use Throwable; |
| 11 | |
| 12 | /** @api */ |
| 13 | class Script |
| 14 | { |
| 15 | private const array RESERVED_TEMPLATE_PARAMETERS = [ |
| 16 | 'pdodriver' => true, |
| 17 | ]; |
| 18 | |
| 19 | protected Database $db; |
| 20 | protected string $script; |
| 21 | protected bool $isTemplate; |
| 22 | protected string $sourcePath; |
| 23 | /** @var (Closure(string, string): string)|null */ |
| 24 | protected ?Closure $compile; |
| 25 | |
| 26 | public function __construct( |
| 27 | Database $db, |
| 28 | LoadedScript $script, |
| 29 | bool $isTemplate, |
| 30 | ) { |
| 31 | $this->db = $db; |
| 32 | $this->script = $script->source; |
| 33 | $this->isTemplate = $isTemplate; |
| 34 | $this->sourcePath = $script->sourcePath; |
| 35 | $this->compile = $script->compile; |
| 36 | } |
| 37 | |
| 38 | public function __invoke(mixed ...$args): Query |
| 39 | { |
| 40 | return $this->invoke(...$args); |
| 41 | } |
| 42 | |
| 43 | public function invoke(mixed ...$argsArray): Query |
| 44 | { |
| 45 | $args = new Args($argsArray); |
| 46 | |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 49 | throw new InvalidArgumentException( |
| 50 | 'Template queries `*.tpql` allow named parameters only', |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | $script = $this->evaluateTemplate($this->script, $args); |
| 55 | |
| 56 | if ($this->compile !== null) { |
| 57 | $script = ($this->compile)($script, $this->sourcePath); |
| 58 | } |
| 59 | |
| 60 | // We need to wrap the result of the prepare call in an array |
| 61 | // to get back to the format of ...$argsArray. |
| 62 | $args = new Args([$this->prepareTemplateVars($script, $args)]); |
| 63 | } else { |
| 64 | $script = $this->script; |
| 65 | } |
| 66 | |
| 67 | return new Query($this->db, $script, $args, $this->sourcePath); |
| 68 | } |
| 69 | |
| 70 | protected function evaluateTemplate(string $template, Args $args): string |
| 71 | { |
| 72 | $context = $this->buildTemplateContext($args); |
| 73 | |
| 74 | if ($template === $this->sourcePath) { |
| 75 | if (!is_file($this->sourcePath)) { |
| 76 | return ''; |
| 77 | } |
| 78 | |
| 79 | return $this->renderTemplateFile($this->sourcePath, $context); |
| 80 | } |
| 81 | |
| 82 | return $this->renderTemplateSource($template, $context); |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * @return array<array-key, mixed> |
| 87 | */ |
| 88 | protected function buildTemplateContext(Args $args): array |
| 89 | { |
| 90 | $named = $args->getNamed(); |
| 91 | |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 94 | throw new InvalidArgumentException("Template parameter '{$name}' is reserved."); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return array_merge( |
| 99 | $named, |
| 100 | ['pdodriver' => $this->db->getPdoDriver()], |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * @param string $templatePath |
| 106 | * @param array<array-key, mixed> $context |
| 107 | */ |
| 108 | protected function renderTemplateFile(string $templatePath, array $context): string |
| 109 | { |
| 110 | ob_start(); |
| 111 | |
| 112 | try { |
| 113 | (static function (string $__templatePath, array $__context): void { |
| 114 | extract($__context, EXTR_SKIP); |
| 115 | |
| 116 | /** @psalm-suppress UnresolvableInclude */ |
| 117 | require $__templatePath; |
| 118 | })($templatePath, $context); |
| 119 | |
| 120 | $result = ob_get_clean(); |
| 121 | |
| 122 | return is_string($result) ? $result : ''; |
| 123 | } catch (Throwable $e) { |
| 124 | ob_end_clean(); |
| 125 | |
| 126 | throw $e; |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param array<array-key, mixed> $context |
| 132 | */ |
| 133 | protected function renderTemplateSource(string $template, array $context): string |
| 134 | { |
| 135 | $templatePath = tempnam(sys_get_temp_dir(), 'quma-tpql-'); |
| 136 | |
| 137 | if ($templatePath === false) { |
| 138 | // tempnam() failure depends on system temp-dir failure and is not usefully reproducible. |
| 139 | throw new RuntimeException('Could not create temporary template file'); // @codeCoverageIgnore |
| 140 | } |
| 141 | |
| 142 | try { |
| 143 | if (file_put_contents($templatePath, $template) === false) { |
| 144 | // This would require making the just-created temp file unwritable between calls. |
| 145 | throw new RuntimeException('Could not write temporary template file'); // @codeCoverageIgnore |
| 146 | } |
| 147 | |
| 148 | return $this->renderTemplateFile($templatePath, $context); |
| 149 | } finally { |
| 150 | if (is_file($templatePath)) { |
| 151 | unlink($templatePath); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Removes all keys from $params which are not present |
| 158 | * in the $script. |
| 159 | * |
| 160 | * PDO does not allow unused parameters. |
| 161 | */ |
| 162 | protected function prepareTemplateVars(string $script, Args $args): array |
| 163 | { |
| 164 | // Remove PostgreSQL blocks |
| 165 | $cleaned = preg_replace(Query::PATTERN_BLOCK, ' ', $script); |
| 166 | // Remove strings |
| 167 | $cleaned = preg_replace(Query::PATTERN_STRING, ' ', $cleaned ?? ''); |
| 168 | // Remove /* */ comments |
| 169 | $cleaned = preg_replace(Query::PATTERN_COMMENT_MULTI, ' ', $cleaned ?? ''); |
| 170 | // Remove single line comments |
| 171 | $cleaned = preg_replace(Query::PATTERN_COMMENT_SINGLE, ' ', $cleaned ?? ''); |
| 172 | |
| 173 | $newArgs = []; |
| 174 | |
| 175 | // Match everything starting with : and a letter. |
| 176 | // Exclude multiple colons, like type casts (::text). |
| 177 | // Would not find a var if it is at the very beginning of script. |
| 178 | $matches = preg_match_all( |
| 179 | '/[^:]:[a-zA-Z][a-zA-Z0-9_]*/', |
| 180 | $cleaned ?? '', |
| 181 | $result, |
| 182 | PREG_PATTERN_ORDER, |
| 183 | ); |
| 184 | |
| 185 | if ($matches !== false && $matches > 0) { |
| 186 | $argsArray = $args->getNamed(); |
| 187 | $namedKeys = []; |
| 188 | $newArgs = []; |
| 189 | |
| 190 | foreach (array_unique($result[0]) as $arg) { |
| 191 | $a = substr($arg, 2); |
| 192 | |
| 193 | if ($a !== '') { |
| 194 | $namedKeys[$a] = true; |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | if (count($namedKeys) > 0) { |
| 199 | $newArgs = array_intersect_key($argsArray, $namedKeys); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | return $newArgs; |
| 204 | } |
| 205 | } |
Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not
necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once.
Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 27 | Database $db, |
| 28 | LoadedScript $script, |
| 29 | bool $isTemplate, |
| 30 | ) { |
| 31 | $this->db = $db; |
| 32 | $this->script = $script->source; |
| 33 | $this->isTemplate = $isTemplate; |
| 34 | $this->sourcePath = $script->sourcePath; |
| 35 | $this->compile = $script->compile; |
| 36 | } |
| 38 | public function __invoke(mixed ...$args): Query |
| 39 | { |
| 40 | return $this->invoke(...$args); |
| 41 | } |
| 88 | protected function buildTemplateContext(Args $args): array |
| 89 | { |
| 90 | $named = $args->getNamed(); |
| 91 | |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 94 | throw new InvalidArgumentException("Template parameter '{$name}' is reserved."); |
| 88 | protected function buildTemplateContext(Args $args): array |
| 89 | { |
| 90 | $named = $args->getNamed(); |
| 91 | |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 94 | throw new InvalidArgumentException("Template parameter '{$name}' is reserved."); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return array_merge( |
| 99 | $named, |
| 100 | ['pdodriver' => $this->db->getPdoDriver()], |
| 101 | ); |
| 102 | } |
| 88 | protected function buildTemplateContext(Args $args): array |
| 89 | { |
| 90 | $named = $args->getNamed(); |
| 91 | |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 94 | throw new InvalidArgumentException("Template parameter '{$name}' is reserved."); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return array_merge( |
| 99 | $named, |
| 100 | ['pdodriver' => $this->db->getPdoDriver()], |
| 101 | ); |
| 102 | } |
| 88 | protected function buildTemplateContext(Args $args): array |
| 89 | { |
| 90 | $named = $args->getNamed(); |
| 91 | |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 92 | foreach (array_keys(self::RESERVED_TEMPLATE_PARAMETERS) as $name) { |
| 93 | if (array_key_exists($name, $named)) { |
| 94 | throw new InvalidArgumentException("Template parameter '{$name}' is reserved."); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return array_merge( |
| 99 | $named, |
| 100 | ['pdodriver' => $this->db->getPdoDriver()], |
| 101 | ); |
| 102 | } |
| 70 | protected function evaluateTemplate(string $template, Args $args): string |
| 71 | { |
| 72 | $context = $this->buildTemplateContext($args); |
| 73 | |
| 74 | if ($template === $this->sourcePath) { |
| 75 | if (!is_file($this->sourcePath)) { |
| 76 | return ''; |
| 70 | protected function evaluateTemplate(string $template, Args $args): string |
| 71 | { |
| 72 | $context = $this->buildTemplateContext($args); |
| 73 | |
| 74 | if ($template === $this->sourcePath) { |
| 75 | if (!is_file($this->sourcePath)) { |
| 79 | return $this->renderTemplateFile($this->sourcePath, $context); |
| 70 | protected function evaluateTemplate(string $template, Args $args): string |
| 71 | { |
| 72 | $context = $this->buildTemplateContext($args); |
| 73 | |
| 74 | if ($template === $this->sourcePath) { |
| 82 | return $this->renderTemplateSource($template, $context); |
| 83 | } |
| 43 | public function invoke(mixed ...$argsArray): Query |
| 44 | { |
| 45 | $args = new Args($argsArray); |
| 46 | |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 49 | throw new InvalidArgumentException( |
| 50 | 'Template queries `*.tpql` allow named parameters only', |
| 43 | public function invoke(mixed ...$argsArray): Query |
| 44 | { |
| 45 | $args = new Args($argsArray); |
| 46 | |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 54 | $script = $this->evaluateTemplate($this->script, $args); |
| 55 | |
| 56 | if ($this->compile !== null) { |
| 57 | $script = ($this->compile)($script, $this->sourcePath); |
| 58 | } |
| 59 | |
| 60 | // We need to wrap the result of the prepare call in an array |
| 61 | // to get back to the format of ...$argsArray. |
| 62 | $args = new Args([$this->prepareTemplateVars($script, $args)]); |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 49 | throw new InvalidArgumentException( |
| 50 | 'Template queries `*.tpql` allow named parameters only', |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | $script = $this->evaluateTemplate($this->script, $args); |
| 55 | |
| 56 | if ($this->compile !== null) { |
| 57 | $script = ($this->compile)($script, $this->sourcePath); |
| 58 | } |
| 59 | |
| 60 | // We need to wrap the result of the prepare call in an array |
| 61 | // to get back to the format of ...$argsArray. |
| 62 | $args = new Args([$this->prepareTemplateVars($script, $args)]); |
| 67 | return new Query($this->db, $script, $args, $this->sourcePath); |
| 68 | } |
| 43 | public function invoke(mixed ...$argsArray): Query |
| 44 | { |
| 45 | $args = new Args($argsArray); |
| 46 | |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 54 | $script = $this->evaluateTemplate($this->script, $args); |
| 55 | |
| 56 | if ($this->compile !== null) { |
| 47 | if ($this->isTemplate) { |
| 48 | if ($args->type() === ArgType::Positional) { |
| 49 | throw new InvalidArgumentException( |
| 50 | 'Template queries `*.tpql` allow named parameters only', |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | $script = $this->evaluateTemplate($this->script, $args); |
| 55 | |
| 56 | if ($this->compile !== null) { |
| 57 | $script = ($this->compile)($script, $this->sourcePath); |
| 58 | } |
| 59 | |
| 60 | // We need to wrap the result of the prepare call in an array |
| 61 | // to get back to the format of ...$argsArray. |
| 62 | $args = new Args([$this->prepareTemplateVars($script, $args)]); |
| 67 | return new Query($this->db, $script, $args, $this->sourcePath); |
| 68 | } |
| 43 | public function invoke(mixed ...$argsArray): Query |
| 44 | { |
| 45 | $args = new Args($argsArray); |
| 46 | |
| 47 | if ($this->isTemplate) { |
| 64 | $script = $this->script; |
| 65 | } |
| 66 | |
| 67 | return new Query($this->db, $script, $args, $this->sourcePath); |
| 67 | return new Query($this->db, $script, $args, $this->sourcePath); |
| 68 | } |
| 108 | protected function renderTemplateFile(string $templatePath, array $context): string |
| 109 | { |
| 110 | ob_start(); |
| 111 | |
| 112 | try { |
| 113 | (static function (string $__templatePath, array $__context): void { |
| 114 | extract($__context, EXTR_SKIP); |
| 115 | |
| 116 | /** @psalm-suppress UnresolvableInclude */ |
| 117 | require $__templatePath; |
| 118 | })($templatePath, $context); |
| 119 | |
| 120 | $result = ob_get_clean(); |
| 121 | |
| 122 | return is_string($result) ? $result : ''; |
| 122 | return is_string($result) ? $result : ''; |
| 122 | return is_string($result) ? $result : ''; |
| 108 | protected function renderTemplateFile(string $templatePath, array $context): string |
| 109 | { |
| 110 | ob_start(); |
| 111 | |
| 112 | try { |
| 113 | (static function (string $__templatePath, array $__context): void { |
| 114 | extract($__context, EXTR_SKIP); |
| 115 | |
| 116 | /** @psalm-suppress UnresolvableInclude */ |
| 117 | require $__templatePath; |
| 118 | })($templatePath, $context); |
| 119 | |
| 120 | $result = ob_get_clean(); |
| 121 | |
| 122 | return is_string($result) ? $result : ''; |
| 122 | return is_string($result) ? $result : ''; |
| 122 | return is_string($result) ? $result : ''; |
| 123 | } catch (Throwable $e) { |
| 124 | ob_end_clean(); |
| 125 | |
| 126 | throw $e; |
| 127 | } |
| 128 | } |
| 133 | protected function renderTemplateSource(string $template, array $context): string |
| 134 | { |
| 135 | $templatePath = tempnam(sys_get_temp_dir(), 'quma-tpql-'); |
| 136 | |
| 137 | if ($templatePath === false) { |
| 142 | try { |
| 143 | if (file_put_contents($templatePath, $template) === false) { |
| 148 | return $this->renderTemplateFile($templatePath, $context); |
| 150 | if (is_file($templatePath)) { |
| 151 | unlink($templatePath); |
| 151 | unlink($templatePath); |
| 152 | } |
| 153 | } |
| 154 | } |
| 133 | protected function renderTemplateSource(string $template, array $context): string |
| 134 | { |
| 135 | $templatePath = tempnam(sys_get_temp_dir(), 'quma-tpql-'); |
| 136 | |
| 137 | if ($templatePath === false) { |
| 142 | try { |
| 143 | if (file_put_contents($templatePath, $template) === false) { |
| 148 | return $this->renderTemplateFile($templatePath, $context); |
| 150 | if (is_file($templatePath)) { |
| 151 | unlink($templatePath); |
| 152 | } |
| 153 | } |
| 154 | } |
| 133 | protected function renderTemplateSource(string $template, array $context): string |
| 134 | { |
| 135 | $templatePath = tempnam(sys_get_temp_dir(), 'quma-tpql-'); |
| 136 | |
| 137 | if ($templatePath === false) { |
| 142 | try { |
| 143 | if (file_put_contents($templatePath, $template) === false) { |
| 148 | return $this->renderTemplateFile($templatePath, $context); |
| 148 | return $this->renderTemplateFile($templatePath, $context); |
| 113 | (static function (string $__templatePath, array $__context): void { |
| 114 | extract($__context, EXTR_SKIP); |
| 115 | |
| 116 | /** @psalm-suppress UnresolvableInclude */ |
| 117 | require $__templatePath; |
| 118 | })($templatePath, $context); |