Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
105 / 105 |
|
89.71% |
61 / 68 |
|
30.00% |
21 / 70 |
|
55.56% |
5 / 9 |
CRAP | |
0.00% |
0 / 1 |
| Placeholders | |
100.00% |
105 / 105 |
|
89.71% |
61 / 68 |
|
30.00% |
21 / 70 |
|
100.00% |
9 / 9 |
257.87 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| compileSql | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| normalizeConfig | |
100.00% |
16 / 16 |
|
100.00% |
12 / 12 |
|
60.00% |
6 / 10 |
|
100.00% |
1 / 1 |
8.30 | |||
| normalizeValues | |
100.00% |
18 / 18 |
|
88.89% |
16 / 18 |
|
20.00% |
4 / 20 |
|
100.00% |
1 / 1 |
24.43 | |||
| substituteFragment | |
100.00% |
22 / 22 |
|
87.50% |
14 / 16 |
|
13.64% |
3 / 22 |
|
100.00% |
1 / 1 |
21.10 | |||
| assertNoMalformedTokens | |
100.00% |
7 / 7 |
|
83.33% |
10 / 12 |
|
20.00% |
2 / 10 |
|
100.00% |
1 / 1 |
7.61 | |||
| unknownPlaceholder | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| malformedPlaceholder | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| location | |
100.00% |
7 / 7 |
|
83.33% |
5 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
2.50 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Quma; |
| 6 | |
| 7 | use InvalidArgumentException; |
| 8 | use RuntimeException; |
| 9 | |
| 10 | final class Placeholders |
| 11 | { |
| 12 | public const string NAME_PATTERN = '[A-Za-z_][A-Za-z0-9_.:-]*'; |
| 13 | |
| 14 | /** @var non-empty-string */ |
| 15 | private readonly string $tokenPattern; |
| 16 | |
| 17 | /** @var non-empty-string */ |
| 18 | private readonly string $tokenStartPattern; |
| 19 | |
| 20 | /** @var array<string, string> */ |
| 21 | private readonly array $values; |
| 22 | |
| 23 | /** @param array<array-key, mixed> $config */ |
| 24 | public function __construct( |
| 25 | private readonly string $driver, |
| 26 | array $config, |
| 27 | private readonly Delimiters $delimiters, |
| 28 | ) { |
| 29 | $open = preg_quote($this->delimiters->open, '/'); |
| 30 | $close = preg_quote($this->delimiters->close, '/'); |
| 31 | $this->tokenPattern = '/' . $open . '(' . self::NAME_PATTERN . ')' . $close . '/'; |
| 32 | $this->tokenStartPattern = '/^' . $open . '(' . self::NAME_PATTERN . ')' . $close . '/'; |
| 33 | |
| 34 | $normalized = $this->normalizeConfig($config); |
| 35 | $this->values = array_replace( |
| 36 | $normalized['all'] ?? [], |
| 37 | $normalized[$this->driver] ?? [], |
| 38 | ); |
| 39 | } |
| 40 | |
| 41 | public function compileSql(string $source, string $path): string |
| 42 | { |
| 43 | return $this->substituteFragment($source, $path, $source, 0); |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * @param array<array-key, mixed> $config |
| 48 | * |
| 49 | * @return array<string, array<string, string>> |
| 50 | */ |
| 51 | private function normalizeConfig(array $config): array |
| 52 | { |
| 53 | $normalized = []; |
| 54 | |
| 55 | foreach ($config as $scope => $values) { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 57 | throw new InvalidArgumentException( |
| 58 | 'Static placeholder scopes must be non-empty strings.', |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | if ($scope === 'default') { |
| 63 | throw new InvalidArgumentException( |
| 64 | "Static placeholders use the shared scope 'all'. Replace placeholders['default'] with placeholders['all'].", |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | if (!is_array($values)) { |
| 69 | throw new InvalidArgumentException( |
| 70 | "Static placeholders for scope '{$scope}' must be an array of string values.", |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | $normalized[$scope] = $this->normalizeValues($scope, $values); |
| 75 | } |
| 76 | |
| 77 | return $normalized; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * @param array<array-key, mixed> $values |
| 82 | * |
| 83 | * @return array<string, string> |
| 84 | */ |
| 85 | private function normalizeValues(string $scope, array $values): array |
| 86 | { |
| 87 | $normalized = []; |
| 88 | |
| 89 | foreach ($values as $name => $value) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 91 | throw new InvalidArgumentException( |
| 92 | "Invalid static placeholder name in scope '{$scope}'. Names must match " |
| 93 | . self::NAME_PATTERN |
| 94 | . '.', |
| 95 | ); |
| 96 | } |
| 97 | |
| 98 | if (!is_string($value)) { |
| 99 | throw new InvalidArgumentException( |
| 100 | "Static placeholder '{$name}' in scope '{$scope}' must be a string.", |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 105 | throw new InvalidArgumentException( |
| 106 | "Static placeholder '{$name}' in scope '{$scope}' must not contain another static placeholder.", |
| 107 | ); |
| 108 | } |
| 109 | |
| 110 | $normalized[$name] = $value; |
| 111 | } |
| 112 | |
| 113 | return $normalized; |
| 114 | } |
| 115 | |
| 116 | private function substituteFragment( |
| 117 | string $fragment, |
| 118 | string $path, |
| 119 | string $source, |
| 120 | int $baseOffset, |
| 121 | ): string { |
| 122 | $this->assertNoMalformedTokens($fragment, $path, $source, $baseOffset); |
| 123 | |
| 124 | $matches = []; |
| 125 | $result = preg_match_all( |
| 126 | $this->tokenPattern, |
| 127 | $fragment, |
| 128 | $matches, |
| 129 | PREG_SET_ORDER | PREG_OFFSET_CAPTURE, |
| 130 | ); |
| 131 | |
| 132 | if ($result === false || $result === 0) { |
| 133 | return $fragment; |
| 134 | } |
| 135 | |
| 136 | $compiled = ''; |
| 137 | $cursor = 0; |
| 138 | |
| 139 | foreach ($matches as $match) { |
| 140 | $placeholder = $match[0][0]; |
| 141 | $offset = $match[0][1]; |
| 142 | $name = $match[1][0]; |
| 143 | |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 145 | |
| 146 | if (!array_key_exists($name, $this->values)) { |
| 147 | throw $this->unknownPlaceholder($placeholder, $name, $path, $source, $baseOffset + $offset); |
| 148 | } |
| 149 | |
| 150 | $compiled .= $this->values[$name]; |
| 151 | $cursor = $offset + strlen($placeholder); |
| 152 | } |
| 153 | |
| 154 | return $compiled . substr($fragment, $cursor); |
| 155 | } |
| 156 | |
| 157 | private function assertNoMalformedTokens( |
| 158 | string $fragment, |
| 159 | string $path, |
| 160 | string $source, |
| 161 | int $baseOffset, |
| 162 | ): void { |
| 163 | $offset = 0; |
| 164 | |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 166 | $matches = []; |
| 167 | $tail = substr($fragment, $position); |
| 168 | |
| 169 | if (preg_match($this->tokenStartPattern, $tail, $matches) !== 1) { |
| 170 | throw $this->malformedPlaceholder($path, $source, $baseOffset + $position); |
| 171 | } |
| 172 | |
| 173 | $offset = $position + strlen($matches[0]); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | private function unknownPlaceholder( |
| 178 | string $placeholder, |
| 179 | string $name, |
| 180 | string $path, |
| 181 | string $source, |
| 182 | int $offset, |
| 183 | ): RuntimeException { |
| 184 | [$line, $column] = $this->location($source, $offset); |
| 185 | |
| 186 | return new RuntimeException( |
| 187 | "Unknown static placeholder {$placeholder} in {$path}:{$line}:{$column} for driver \"{$this->driver}\".\n" |
| 188 | . "No value was configured for \"{$name}\".\n" |
| 189 | . "Add placeholders['all']['{$name}'] or placeholders['{$this->driver}']['{$name}'].\n" |
| 190 | . 'Static placeholders are raw SQL fragments. Use them only for trusted configuration, never for user input.', |
| 191 | ); |
| 192 | } |
| 193 | |
| 194 | private function malformedPlaceholder(string $path, string $source, int $offset): RuntimeException |
| 195 | { |
| 196 | [$line, $column] = $this->location($source, $offset); |
| 197 | |
| 198 | return new RuntimeException( |
| 199 | "Malformed static placeholder in {$path}:{$line}:{$column}.\n" |
| 200 | . 'Expected ' |
| 201 | . $this->delimiters->token('name') |
| 202 | . ' where name matches: ' |
| 203 | . self::NAME_PATTERN |
| 204 | . ".\n" |
| 205 | . 'Examples: ' |
| 206 | . $this->delimiters->token('prefix') |
| 207 | . ', ' |
| 208 | . $this->delimiters->token('schema.name') |
| 209 | . ', ' |
| 210 | . $this->delimiters->token('tenant-prefix') |
| 211 | . ', ' |
| 212 | . $this->delimiters->token('cms:prefix') |
| 213 | . '.', |
| 214 | ); |
| 215 | } |
| 216 | |
| 217 | /** @return array{0: int, 1: int} */ |
| 218 | private function location(string $source, int $offset): array |
| 219 | { |
| 220 | $before = substr($source, 0, $offset); |
| 221 | $line = substr_count($before, "\n") + 1; |
| 222 | $lineStart = strrpos($before, "\n"); |
| 223 | $column = $offset + 1; |
| 224 | |
| 225 | if ($lineStart !== false) { |
| 226 | $column = $offset - $lineStart; |
| 227 | } |
| 228 | |
| 229 | return [$line, $column]; |
| 230 | } |
| 231 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 25 | private readonly string $driver, |
| 26 | array $config, |
| 27 | private readonly Delimiters $delimiters, |
| 28 | ) { |
| 29 | $open = preg_quote($this->delimiters->open, '/'); |
| 30 | $close = preg_quote($this->delimiters->close, '/'); |
| 31 | $this->tokenPattern = '/' . $open . '(' . self::NAME_PATTERN . ')' . $close . '/'; |
| 32 | $this->tokenStartPattern = '/^' . $open . '(' . self::NAME_PATTERN . ')' . $close . '/'; |
| 33 | |
| 34 | $normalized = $this->normalizeConfig($config); |
| 35 | $this->values = array_replace( |
| 36 | $normalized['all'] ?? [], |
| 37 | $normalized[$this->driver] ?? [], |
| 38 | ); |
| 39 | } |
| 158 | string $fragment, |
| 159 | string $path, |
| 160 | string $source, |
| 161 | int $baseOffset, |
| 162 | ): void { |
| 163 | $offset = 0; |
| 164 | |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 166 | $matches = []; |
| 167 | $tail = substr($fragment, $position); |
| 167 | $tail = substr($fragment, $position); |
| 167 | $tail = substr($fragment, $position); |
| 167 | $tail = substr($fragment, $position); |
| 168 | |
| 169 | if (preg_match($this->tokenStartPattern, $tail, $matches) !== 1) { |
| 170 | throw $this->malformedPlaceholder($path, $source, $baseOffset + $position); |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 166 | $matches = []; |
| 167 | $tail = substr($fragment, $position); |
| 168 | |
| 169 | if (preg_match($this->tokenStartPattern, $tail, $matches) !== 1) { |
| 170 | throw $this->malformedPlaceholder($path, $source, $baseOffset + $position); |
| 171 | } |
| 172 | |
| 173 | $offset = $position + strlen($matches[0]); |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 165 | while (($position = strpos($fragment, $this->delimiters->open, $offset)) !== false) { |
| 175 | } |
| 41 | public function compileSql(string $source, string $path): string |
| 42 | { |
| 43 | return $this->substituteFragment($source, $path, $source, 0); |
| 44 | } |
| 218 | private function location(string $source, int $offset): array |
| 219 | { |
| 220 | $before = substr($source, 0, $offset); |
| 220 | $before = substr($source, 0, $offset); |
| 220 | $before = substr($source, 0, $offset); |
| 220 | $before = substr($source, 0, $offset); |
| 221 | $line = substr_count($before, "\n") + 1; |
| 222 | $lineStart = strrpos($before, "\n"); |
| 223 | $column = $offset + 1; |
| 224 | |
| 225 | if ($lineStart !== false) { |
| 226 | $column = $offset - $lineStart; |
| 227 | } |
| 228 | |
| 229 | return [$line, $column]; |
| 229 | return [$line, $column]; |
| 230 | } |
| 194 | private function malformedPlaceholder(string $path, string $source, int $offset): RuntimeException |
| 195 | { |
| 196 | [$line, $column] = $this->location($source, $offset); |
| 197 | |
| 198 | return new RuntimeException( |
| 199 | "Malformed static placeholder in {$path}:{$line}:{$column}.\n" |
| 200 | . 'Expected ' |
| 201 | . $this->delimiters->token('name') |
| 202 | . ' where name matches: ' |
| 203 | . self::NAME_PATTERN |
| 204 | . ".\n" |
| 205 | . 'Examples: ' |
| 206 | . $this->delimiters->token('prefix') |
| 207 | . ', ' |
| 208 | . $this->delimiters->token('schema.name') |
| 209 | . ', ' |
| 210 | . $this->delimiters->token('tenant-prefix') |
| 211 | . ', ' |
| 212 | . $this->delimiters->token('cms:prefix') |
| 213 | . '.', |
| 214 | ); |
| 215 | } |
| 51 | private function normalizeConfig(array $config): array |
| 52 | { |
| 53 | $normalized = []; |
| 54 | |
| 55 | foreach ($config as $scope => $values) { |
| 55 | foreach ($config as $scope => $values) { |
| 55 | foreach ($config as $scope => $values) { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 57 | throw new InvalidArgumentException( |
| 58 | 'Static placeholder scopes must be non-empty strings.', |
| 62 | if ($scope === 'default') { |
| 63 | throw new InvalidArgumentException( |
| 64 | "Static placeholders use the shared scope 'all'. Replace placeholders['default'] with placeholders['all'].", |
| 68 | if (!is_array($values)) { |
| 69 | throw new InvalidArgumentException( |
| 70 | "Static placeholders for scope '{$scope}' must be an array of string values.", |
| 55 | foreach ($config as $scope => $values) { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 57 | throw new InvalidArgumentException( |
| 58 | 'Static placeholder scopes must be non-empty strings.', |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | if ($scope === 'default') { |
| 63 | throw new InvalidArgumentException( |
| 64 | "Static placeholders use the shared scope 'all'. Replace placeholders['default'] with placeholders['all'].", |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | if (!is_array($values)) { |
| 69 | throw new InvalidArgumentException( |
| 70 | "Static placeholders for scope '{$scope}' must be an array of string values.", |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | $normalized[$scope] = $this->normalizeValues($scope, $values); |
| 55 | foreach ($config as $scope => $values) { |
| 56 | if (!is_string($scope) || $scope === '') { |
| 57 | throw new InvalidArgumentException( |
| 58 | 'Static placeholder scopes must be non-empty strings.', |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | if ($scope === 'default') { |
| 63 | throw new InvalidArgumentException( |
| 64 | "Static placeholders use the shared scope 'all'. Replace placeholders['default'] with placeholders['all'].", |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | if (!is_array($values)) { |
| 69 | throw new InvalidArgumentException( |
| 70 | "Static placeholders for scope '{$scope}' must be an array of string values.", |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | $normalized[$scope] = $this->normalizeValues($scope, $values); |
| 75 | } |
| 76 | |
| 77 | return $normalized; |
| 78 | } |
| 85 | private function normalizeValues(string $scope, array $values): array |
| 86 | { |
| 87 | $normalized = []; |
| 88 | |
| 89 | foreach ($values as $name => $value) { |
| 89 | foreach ($values as $name => $value) { |
| 89 | foreach ($values as $name => $value) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 91 | throw new InvalidArgumentException( |
| 92 | "Invalid static placeholder name in scope '{$scope}'. Names must match " |
| 93 | . self::NAME_PATTERN |
| 94 | . '.', |
| 98 | if (!is_string($value)) { |
| 99 | throw new InvalidArgumentException( |
| 100 | "Static placeholder '{$name}' in scope '{$scope}' must be a string.", |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 105 | throw new InvalidArgumentException( |
| 106 | "Static placeholder '{$name}' in scope '{$scope}' must not contain another static placeholder.", |
| 89 | foreach ($values as $name => $value) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 91 | throw new InvalidArgumentException( |
| 92 | "Invalid static placeholder name in scope '{$scope}'. Names must match " |
| 93 | . self::NAME_PATTERN |
| 94 | . '.', |
| 95 | ); |
| 96 | } |
| 97 | |
| 98 | if (!is_string($value)) { |
| 99 | throw new InvalidArgumentException( |
| 100 | "Static placeholder '{$name}' in scope '{$scope}' must be a string.", |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 105 | throw new InvalidArgumentException( |
| 106 | "Static placeholder '{$name}' in scope '{$scope}' must not contain another static placeholder.", |
| 107 | ); |
| 108 | } |
| 109 | |
| 110 | $normalized[$name] = $value; |
| 89 | foreach ($values as $name => $value) { |
| 90 | if (!is_string($name) || !preg_match('/^' . self::NAME_PATTERN . '$/', $name)) { |
| 91 | throw new InvalidArgumentException( |
| 92 | "Invalid static placeholder name in scope '{$scope}'. Names must match " |
| 93 | . self::NAME_PATTERN |
| 94 | . '.', |
| 95 | ); |
| 96 | } |
| 97 | |
| 98 | if (!is_string($value)) { |
| 99 | throw new InvalidArgumentException( |
| 100 | "Static placeholder '{$name}' in scope '{$scope}' must be a string.", |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | if (str_contains($value, $this->delimiters->open)) { |
| 105 | throw new InvalidArgumentException( |
| 106 | "Static placeholder '{$name}' in scope '{$scope}' must not contain another static placeholder.", |
| 107 | ); |
| 108 | } |
| 109 | |
| 110 | $normalized[$name] = $value; |
| 111 | } |
| 112 | |
| 113 | return $normalized; |
| 114 | } |
| 117 | string $fragment, |
| 118 | string $path, |
| 119 | string $source, |
| 120 | int $baseOffset, |
| 121 | ): string { |
| 122 | $this->assertNoMalformedTokens($fragment, $path, $source, $baseOffset); |
| 123 | |
| 124 | $matches = []; |
| 125 | $result = preg_match_all( |
| 126 | $this->tokenPattern, |
| 127 | $fragment, |
| 128 | $matches, |
| 129 | PREG_SET_ORDER | PREG_OFFSET_CAPTURE, |
| 130 | ); |
| 131 | |
| 132 | if ($result === false || $result === 0) { |
| 132 | if ($result === false || $result === 0) { |
| 132 | if ($result === false || $result === 0) { |
| 133 | return $fragment; |
| 136 | $compiled = ''; |
| 137 | $cursor = 0; |
| 138 | |
| 139 | foreach ($matches as $match) { |
| 139 | foreach ($matches as $match) { |
| 140 | $placeholder = $match[0][0]; |
| 141 | $offset = $match[0][1]; |
| 142 | $name = $match[1][0]; |
| 143 | |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 145 | |
| 146 | if (!array_key_exists($name, $this->values)) { |
| 147 | throw $this->unknownPlaceholder($placeholder, $name, $path, $source, $baseOffset + $offset); |
| 139 | foreach ($matches as $match) { |
| 140 | $placeholder = $match[0][0]; |
| 141 | $offset = $match[0][1]; |
| 142 | $name = $match[1][0]; |
| 143 | |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 145 | |
| 146 | if (!array_key_exists($name, $this->values)) { |
| 147 | throw $this->unknownPlaceholder($placeholder, $name, $path, $source, $baseOffset + $offset); |
| 148 | } |
| 149 | |
| 150 | $compiled .= $this->values[$name]; |
| 139 | foreach ($matches as $match) { |
| 140 | $placeholder = $match[0][0]; |
| 141 | $offset = $match[0][1]; |
| 142 | $name = $match[1][0]; |
| 143 | |
| 144 | $compiled .= substr($fragment, $cursor, $offset - $cursor); |
| 145 | |
| 146 | if (!array_key_exists($name, $this->values)) { |
| 147 | throw $this->unknownPlaceholder($placeholder, $name, $path, $source, $baseOffset + $offset); |
| 148 | } |
| 149 | |
| 150 | $compiled .= $this->values[$name]; |
| 151 | $cursor = $offset + strlen($placeholder); |
| 152 | } |
| 153 | |
| 154 | return $compiled . substr($fragment, $cursor); |
| 154 | return $compiled . substr($fragment, $cursor); |
| 154 | return $compiled . substr($fragment, $cursor); |
| 154 | return $compiled . substr($fragment, $cursor); |
| 155 | } |
| 178 | string $placeholder, |
| 179 | string $name, |
| 180 | string $path, |
| 181 | string $source, |
| 182 | int $offset, |
| 183 | ): RuntimeException { |
| 184 | [$line, $column] = $this->location($source, $offset); |
| 185 | |
| 186 | return new RuntimeException( |
| 187 | "Unknown static placeholder {$placeholder} in {$path}:{$line}:{$column} for driver \"{$this->driver}\".\n" |
| 188 | . "No value was configured for \"{$name}\".\n" |
| 189 | . "Add placeholders['all']['{$name}'] or placeholders['{$this->driver}']['{$name}'].\n" |
| 190 | . 'Static placeholders are raw SQL fragments. Use them only for trusted configuration, never for user input.', |
| 191 | ); |
| 192 | } |