1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This file is part of Cycle ORM package. |
||
5 | * |
||
6 | * For the full copyright and license information, please view the LICENSE |
||
7 | * file that was distributed with this source code. |
||
8 | */ |
||
9 | |||
10 | declare(strict_types=1); |
||
11 | |||
12 | namespace Cycle\Database\Injection; |
||
13 | |||
14 | use Cycle\Database\Driver\CompilerInterface; |
||
15 | use Cycle\Database\Driver\Quoter; |
||
16 | use Cycle\Database\Exception\DriverException; |
||
17 | |||
18 | abstract class JsonExpression implements FragmentInterface |
||
19 | { |
||
20 | protected string $expression; |
||
21 | protected Quoter $quoter; |
||
22 | |||
23 | /** |
||
24 | * @var ParameterInterface[] |
||
25 | */ |
||
26 | protected array $parameters = []; |
||
27 | |||
28 | /** |
||
29 | * @psalm-param non-empty-string $statement |
||
30 | */ |
||
31 | public function __construct(string $statement, mixed ...$parameters) |
||
32 | { |
||
33 | $this->quoter = new Quoter('', $this->getQuotes()); |
||
34 | |||
35 | $this->expression = $this->compile($statement); |
||
36 | |||
37 | foreach ($parameters as $parameter) { |
||
38 | if ($parameter instanceof ParameterInterface) { |
||
39 | $this->parameters[] = $parameter; |
||
40 | } else { |
||
41 | $this->parameters[] = new Parameter($parameter); |
||
42 | } |
||
43 | } |
||
44 | } |
||
45 | |||
46 | public function getType(): int |
||
47 | { |
||
48 | return CompilerInterface::JSON_EXPRESSION; |
||
49 | } |
||
50 | |||
51 | public function getTokens(): array |
||
52 | { |
||
53 | return [ |
||
54 | 'expression' => $this->expression, |
||
55 | 'parameters' => $this->parameters, |
||
56 | ]; |
||
57 | } |
||
58 | |||
59 | public function __toString(): string |
||
60 | { |
||
61 | return 'exp:' . $this->expression; |
||
62 | } |
||
63 | |||
64 | public static function __set_state(array $an_array): self |
||
65 | { |
||
66 | return new static( |
||
67 | $an_array['expression'] ?? $an_array['statement'], |
||
68 | ...($an_array['parameters'] ?? []), |
||
69 | ); |
||
70 | } |
||
71 | |||
72 | /** |
||
73 | * @param non-empty-string $statement |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
74 | * |
||
75 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
76 | */ |
||
77 | abstract protected function compile(string $statement): string; |
||
78 | |||
79 | /** |
||
80 | * @param non-empty-string $statement |
||
0 ignored issues
–
show
|
|||
81 | * |
||
82 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
83 | */ |
||
84 | protected function getField(string $statement): string |
||
85 | { |
||
86 | $parts = \explode('->', $statement, 2); |
||
87 | |||
88 | return $this->quoter->quote($parts[0]); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * @param non-empty-string $statement |
||
0 ignored issues
–
show
|
|||
93 | * |
||
94 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
95 | */ |
||
96 | protected function getPath(string $statement): string |
||
97 | { |
||
98 | $parts = \explode('->', $statement, 2); |
||
99 | |||
100 | return \count($parts) > 1 ? ', ' . $this->wrapPath($parts[1]) : ''; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Parses a string with array access syntax (e.g., "field[array-key]") and extracts the field name and key. |
||
105 | * |
||
106 | * @param non-empty-string $path |
||
0 ignored issues
–
show
|
|||
107 | * |
||
108 | * @return array<non-empty-string> |
||
0 ignored issues
–
show
|
|||
109 | */ |
||
110 | protected function parseArraySyntax(string $path): array |
||
111 | { |
||
112 | if (\preg_match('/(\[[^\]]+\])+$/', $path, $parts)) { |
||
113 | $parsed = [\trim(\substr($path, 0, \strpos($path, $parts[0])))]; |
||
114 | |||
115 | \preg_match_all('/\[([^\]]+)\]/', $parts[0], $matches); |
||
116 | |||
117 | foreach ($matches[1] as $key) { |
||
118 | if (\trim($key) === '') { |
||
119 | throw new DriverException('Invalid JSON array path syntax. Array key must not be empty.'); |
||
120 | } |
||
121 | $parsed[] = $key; |
||
122 | } |
||
123 | |||
124 | return $parsed; |
||
125 | } |
||
126 | |||
127 | if (\str_contains($path, '[') && \str_contains($path, ']')) { |
||
128 | throw new DriverException( |
||
129 | 'Unable to parse array path syntax. Array key must be wrapped in square brackets.', |
||
130 | ); |
||
131 | } |
||
132 | |||
133 | return [$path]; |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
138 | */ |
||
139 | protected function getQuotes(): string |
||
140 | { |
||
141 | return '""'; |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Transforms a string like "options->languages" into a correct path like $."options"."languages". |
||
146 | * |
||
147 | * @param non-empty-string $value |
||
0 ignored issues
–
show
|
|||
148 | * @param non-empty-string $delimiter |
||
149 | * |
||
150 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
151 | */ |
||
152 | private function wrapPath(string $value, string $delimiter = '->'): string |
||
153 | { |
||
154 | $value = \preg_replace("/(\\+)?'/", "''", $value); |
||
155 | |||
156 | $segments = \explode($delimiter, $value); |
||
157 | $path = \implode('.', \array_map(fn(string $segment): string => $this->wrapPathSegment($segment), $segments)); |
||
158 | |||
159 | return "'$" . (\str_starts_with($path, '[') ? '' : '.') . $path . "'"; |
||
160 | } |
||
161 | |||
162 | /** |
||
163 | * @param non-empty-string $segment |
||
0 ignored issues
–
show
|
|||
164 | * |
||
165 | * @return non-empty-string |
||
0 ignored issues
–
show
|
|||
166 | */ |
||
167 | private function wrapPathSegment(string $segment): string |
||
168 | { |
||
169 | $parts = $this->parseArraySyntax($segment); |
||
170 | |||
171 | if (isset($parts[1])) { |
||
172 | return \sprintf('"%s"[%s]', $parts[0], $parts[1]); |
||
173 | } |
||
174 | |||
175 | return \sprintf('"%s"', $segment); |
||
176 | } |
||
177 | } |
||
178 |