Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
70 / 70
90.00% covered (success)
90.00%
54 / 60
3.22% covered (danger)
3.22%
14 / 435
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Script
100.00% covered (success)
100.00%
70 / 70
90.00% covered (success)
90.00%
54 / 60
3.22% covered (danger)
3.22%
14 / 435
100.00% covered (success)
100.00%
8 / 8
591.58
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 invoke
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
8 / 8
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
4.25
 evaluateTemplate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 buildTemplateContext
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 renderTemplateFile
100.00% covered (success)
100.00%
10 / 10
83.33% covered (warning)
83.33%
5 / 6
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 renderTemplateSource
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
7 / 7
0.00% covered (danger)
0.00%
0 / 3
100.00% covered (success)
100.00%
1 / 1
4
 prepareTemplateVars
100.00% covered (success)
100.00%
22 / 22
80.77% covered (warning)
80.77%
21 / 26
0.48% covered (danger)
0.48%
2 / 416
100.00% covered (success)
100.00%
1 / 1
41.48
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma;
6
7use Closure;
8use InvalidArgumentException;
9use RuntimeException;
10use Throwable;
11
12/** @api */
13class 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}

Paths

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.

Script->__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    }
Script->__invoke
38    public function __invoke(mixed ...$args): Query
39    {
40        return $this->invoke(...$args);
41    }
Script->buildTemplateContext
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    }
Script->evaluateTemplate
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    }
Script->invoke
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    }
Script->renderTemplateFile
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    }
Script->renderTemplateSource
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);
{closure:/workspace/celemas/quma/src/Script.php:113-118}
113            (static function (string $__templatePath, array $__context): void {
114                extract($__context, EXTR_SKIP);
115
116                /** @psalm-suppress UnresolvableInclude */
117                require $__templatePath;
118            })($templatePath, $context);