prisma-client-php 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/index.js +32 -30
- package/dist/init.js +158 -0
- package/dist/prisma/schema.prisma +35 -0
- package/dist/prisma/seed.ts +74 -0
- package/dist/settings/prisma-schema.json +103 -0
- package/dist/settings/prisma-sdk.ts +30 -0
- package/dist/src/Lib/Prisma/Classes/PPHPUtility.php +856 -0
- package/dist/src/Lib/Prisma/Model/IModel.php +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Lib\Prisma\Classes;
|
|
4
|
+
|
|
5
|
+
use Lib\Validator;
|
|
6
|
+
use ReflectionClass;
|
|
7
|
+
use InvalidArgumentException;
|
|
8
|
+
use DateTime;
|
|
9
|
+
use Brick\Math\BigDecimal;
|
|
10
|
+
use Brick\Math\BigInteger;
|
|
11
|
+
use ReflectionUnionType;
|
|
12
|
+
use ReflectionNamedType;
|
|
13
|
+
use Exception;
|
|
14
|
+
|
|
15
|
+
enum ArrayType: string
|
|
16
|
+
{
|
|
17
|
+
case Associative = 'associative';
|
|
18
|
+
case Indexed = 'indexed';
|
|
19
|
+
case Value = 'value';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
final class PPHPUtility
|
|
23
|
+
{
|
|
24
|
+
/**
|
|
25
|
+
* Checks if the fields exist with references in the given selection.
|
|
26
|
+
*
|
|
27
|
+
* @param array $select The selection array containing fields to check.
|
|
28
|
+
* @param array &$relatedEntityFields Reference to an array where related entity fields will be stored.
|
|
29
|
+
* @param array &$primaryEntityFields Reference to an array where primary entity fields will be stored.
|
|
30
|
+
* @param array $relationName An array of relation names.
|
|
31
|
+
* @param array $fields An array of fields in the model.
|
|
32
|
+
* @param string $modelName The name of the model being checked.
|
|
33
|
+
* @param string $timestamp The timestamp field name to be ignored during the check.
|
|
34
|
+
*
|
|
35
|
+
* @throws Exception If a field does not exist in the model or if the selection format is incorrect.
|
|
36
|
+
*/
|
|
37
|
+
public static function checkFieldsExistWithReferences(
|
|
38
|
+
array $select,
|
|
39
|
+
array &$relatedEntityFields,
|
|
40
|
+
array &$primaryEntityFields,
|
|
41
|
+
array $relationName,
|
|
42
|
+
array $fields,
|
|
43
|
+
string $modelName,
|
|
44
|
+
string $timestamp
|
|
45
|
+
) {
|
|
46
|
+
if (isset($select) && is_array($select)) {
|
|
47
|
+
foreach ($select as $key => $value) {
|
|
48
|
+
if ($key === $timestamp) continue;
|
|
49
|
+
|
|
50
|
+
if (is_numeric($key) && is_string($value)) {
|
|
51
|
+
if (array_key_exists($value, $fields))
|
|
52
|
+
throw new Exception("The '$value' is indexed, waiting example: ['$value' => true]");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isset($value) && empty($value) || !is_bool($value)) {
|
|
56
|
+
if (is_string($key) && !array_key_exists($key, $fields)) {
|
|
57
|
+
throw new Exception("The field '$key' does not exist in the $modelName model.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (is_string($key) && array_key_exists($key, $fields)) {
|
|
61
|
+
if (!is_bool($value) && !is_array($value)) {
|
|
62
|
+
throw new Exception("The '$key' is indexed, waiting example: ['$key' => true]");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!is_array($value))
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (is_string($key) && is_array($value)) {
|
|
71
|
+
if (isset($value['select'])) {
|
|
72
|
+
$relatedEntityFields[$key] = $value['select'];
|
|
73
|
+
} else {
|
|
74
|
+
if (is_array($value) && empty($value)) {
|
|
75
|
+
$relatedEntityFields[$key] = [$key];
|
|
76
|
+
} else {
|
|
77
|
+
if (!is_bool($value) || empty($value)) {
|
|
78
|
+
throw new Exception("The '$key' is indexed, waiting example: ['$key' => true] or ['$key' => ['select' => ['field1' => true, 'field2' => true]]]");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
foreach (explode(',', $key) as $fieldName) {
|
|
84
|
+
if ($key === $timestamp || $fieldName === $timestamp) continue;
|
|
85
|
+
$fieldName = trim($fieldName);
|
|
86
|
+
|
|
87
|
+
if (!array_key_exists($fieldName, $fields)) {
|
|
88
|
+
$availableFields = implode(', ', array_keys($fields));
|
|
89
|
+
throw new Exception("The field '$fieldName' does not exist in the $modelName model. Available fields are: $availableFields");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
in_array($fieldName, $relationName) ||
|
|
94
|
+
(isset($fields[$fieldName]) && in_array($fields[$fieldName]['type'], $relationName))
|
|
95
|
+
) {
|
|
96
|
+
$relatedEntityFields[$fieldName] = [$fieldName];
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$isRelationalOrInverse = false;
|
|
101
|
+
if (isset($fields[$fieldName]['decorators'])) {
|
|
102
|
+
foreach ($fields[$fieldName]['decorators'] as $decoratorKey => $decoratorValue) {
|
|
103
|
+
if ($decoratorKey === 'relation' || $decoratorKey === 'inverseRelation') {
|
|
104
|
+
$isRelationalOrInverse = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!$isRelationalOrInverse) {
|
|
111
|
+
if (in_array($fieldName, $primaryEntityFields)) continue;
|
|
112
|
+
$primaryEntityFields[] = $fieldName;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Checks if the fields in the select array exist in the fields array for the given model.
|
|
122
|
+
*
|
|
123
|
+
* @param array $select The array of fields to select.
|
|
124
|
+
* @param array $fields The array of fields available in the model.
|
|
125
|
+
* @param string $modelName The name of the model being checked.
|
|
126
|
+
*
|
|
127
|
+
* @throws Exception If a field in the select array does not exist in the fields array.
|
|
128
|
+
*/
|
|
129
|
+
public static function checkFieldsExist(array $select, array $fields, string $modelName)
|
|
130
|
+
{
|
|
131
|
+
foreach ($select as $key => $value) {
|
|
132
|
+
if (is_numeric($key) && is_string($value)) {
|
|
133
|
+
if (self::fieldExists($key, $fields))
|
|
134
|
+
throw new Exception("The '$value' is indexed, waiting example: ['$value' => true]");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isset($value) && empty($value) || !is_bool($value)) {
|
|
138
|
+
if (is_string($key) && !self::fieldExists($key, $fields)) {
|
|
139
|
+
throw new Exception("The field '$key' does not exist in the $modelName model.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (is_array($value) && !empty($value)) {
|
|
143
|
+
|
|
144
|
+
$isRelatedModel = false;
|
|
145
|
+
|
|
146
|
+
foreach ($fields as $field) {
|
|
147
|
+
$isObject = $field['kind'] === 'object' ? true : false;
|
|
148
|
+
$fieldName = $field['name'];
|
|
149
|
+
|
|
150
|
+
if ($isObject && $fieldName === $key) {
|
|
151
|
+
$isRelatedModel = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ($isRelatedModel) continue;
|
|
156
|
+
|
|
157
|
+
$keys = array_keys($value);
|
|
158
|
+
foreach ($keys as $fieldName) {
|
|
159
|
+
$fieldName = trim($fieldName);
|
|
160
|
+
if (!self::fieldExists($fieldName, $fields)) {
|
|
161
|
+
throw new Exception("The field '$fieldName' does not exist in the $modelName model.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
foreach (explode(',', $key) as $fieldName) {
|
|
170
|
+
$fieldName = trim($fieldName);
|
|
171
|
+
if (!self::fieldExists($fieldName, $fields)) {
|
|
172
|
+
throw new Exception("The field '$fieldName' does not exist in the $modelName model.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static function fieldExists(string $key, array $fields): bool
|
|
179
|
+
{
|
|
180
|
+
foreach ($fields as $field) {
|
|
181
|
+
if (isset($field['name']) && $field['name'] === $key) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Checks the contents of an array and determines its type.
|
|
190
|
+
*
|
|
191
|
+
* This method iterates through the provided array and checks the type of its elements.
|
|
192
|
+
* It returns an `ArrayType` enum value indicating whether the array is associative,
|
|
193
|
+
* indexed, or contains a single value.
|
|
194
|
+
*
|
|
195
|
+
* @param array $array The array to check.
|
|
196
|
+
* @return ArrayType Returns `ArrayType::Associative` if the array is associative,
|
|
197
|
+
* `ArrayType::Indexed` if the array is indexed,
|
|
198
|
+
* or `ArrayType::Value` if the array contains a single value.
|
|
199
|
+
*/
|
|
200
|
+
public static function checkArrayContents(array $array): ArrayType
|
|
201
|
+
{
|
|
202
|
+
foreach ($array as $key => $value) {
|
|
203
|
+
if (is_array($value)) {
|
|
204
|
+
if (array_keys($value) !== range(0, count($value) - 1)) {
|
|
205
|
+
return ArrayType::Associative;
|
|
206
|
+
} else {
|
|
207
|
+
return ArrayType::Indexed;
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
return ArrayType::Value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Checks and processes the include array for related entity fields and includes.
|
|
217
|
+
*
|
|
218
|
+
* @param array $include The array of includes to be checked.
|
|
219
|
+
* @param array &$relatedEntityFields The array of related entity fields to be updated.
|
|
220
|
+
* @param array &$includes The array of includes to be updated.
|
|
221
|
+
* @param array $fields The array of fields in the model.
|
|
222
|
+
* @param string $modelName The name of the model being processed.
|
|
223
|
+
*
|
|
224
|
+
* @throws Exception If an include value is indexed incorrectly or if a field does not exist in the model.
|
|
225
|
+
*/
|
|
226
|
+
public static function checkIncludes(array $include, array &$relatedEntityFields, array &$includes, array $fields, string $modelName)
|
|
227
|
+
{
|
|
228
|
+
if (isset($include) && is_array($include)) {
|
|
229
|
+
foreach ($include as $key => $value) {
|
|
230
|
+
if (is_array($value) && array_key_exists('join.type', $value)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
self::processIncludeValue($key, $value, $relatedEntityFields, $fields, $modelName, $key);
|
|
235
|
+
|
|
236
|
+
if (is_numeric($key) && is_string($value)) {
|
|
237
|
+
throw new Exception("The '$value' is indexed, waiting example: ['$value' => true]");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isset($value) && empty($value) || !is_bool($value)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!array_key_exists($key, $fields)) {
|
|
245
|
+
throw new Exception("The field '$key' does not exist in the $modelName model.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$includes[$key] = $value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private static function processIncludeValue($key, $value, &$relatedEntityFields, $fields, $modelName, $parentKey)
|
|
254
|
+
{
|
|
255
|
+
if (isset($value['select']) || isset($value['where'])) {
|
|
256
|
+
$relatedEntityFields[$parentKey] = $value;
|
|
257
|
+
} elseif (is_array($value)) {
|
|
258
|
+
if (empty($value)) {
|
|
259
|
+
$relatedEntityFields[$parentKey] = [$parentKey];
|
|
260
|
+
} else {
|
|
261
|
+
foreach ($value as $k => $v) {
|
|
262
|
+
if (is_string($k) && (is_bool($v) || empty($v))) {
|
|
263
|
+
$relatedEntityFields[$parentKey]['include'] = [$k => $v];
|
|
264
|
+
} else {
|
|
265
|
+
self::processIncludeValue($k, $v, $relatedEntityFields, $fields, $modelName, $parentKey);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
if (!is_bool($value) || empty($value)) {
|
|
271
|
+
throw new Exception("The '$value' is indexed, waiting example: ['$value' => true] or ['$value' => ['select' => ['field1' => true, 'field2' => true]]]");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Processes an array of conditions and converts them into SQL conditions and bindings.
|
|
278
|
+
*
|
|
279
|
+
* @param array $conditions The array of conditions to process.
|
|
280
|
+
* @param array &$sqlConditions The array to store the resulting SQL conditions.
|
|
281
|
+
* @param array &$bindings The array to store the resulting bindings for prepared statements.
|
|
282
|
+
* @param string $dbType The type of the database (e.g., MySQL, PostgreSQL).
|
|
283
|
+
* @param string $tableName The name of the table to which the conditions apply.
|
|
284
|
+
* @param string $prefix The prefix to use for condition keys (used for nested conditions).
|
|
285
|
+
* @param int $level The current level of nesting for conditions (used for recursion).
|
|
286
|
+
*
|
|
287
|
+
* @return void
|
|
288
|
+
*/
|
|
289
|
+
public static function processConditions(array $conditions, &$sqlConditions, &$bindings, $dbType, $tableName, $prefix = '', $level = 0)
|
|
290
|
+
{
|
|
291
|
+
foreach ($conditions as $key => $value) {
|
|
292
|
+
if (in_array($key, ['AND', 'OR', 'NOT'])) {
|
|
293
|
+
$groupedConditions = [];
|
|
294
|
+
if ($key === 'NOT') {
|
|
295
|
+
self::processNotCondition($value, $groupedConditions, $bindings, $dbType, $tableName, $prefix . $key . '_', $level);
|
|
296
|
+
if (!empty($groupedConditions)) {
|
|
297
|
+
$conditionGroup = '(' . implode(" $key ", $groupedConditions) . ')';
|
|
298
|
+
$conditionGroup = 'NOT ' . $conditionGroup;
|
|
299
|
+
$sqlConditions[] = $conditionGroup;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
foreach ($value as $conditionKey => $subCondition) {
|
|
303
|
+
if (is_numeric($conditionKey)) {
|
|
304
|
+
self::processConditions($subCondition, $groupedConditions, $bindings, $dbType, $tableName, $prefix . $key . $conditionKey . '_', $level + 1);
|
|
305
|
+
} else {
|
|
306
|
+
self::processSingleCondition($conditionKey, $subCondition, $groupedConditions, $bindings, $dbType, $tableName, $prefix . $key . $conditionKey . '_', $level + 1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!empty($groupedConditions)) {
|
|
310
|
+
$conditionGroup = '(' . implode(" $key ", $groupedConditions) . ')';
|
|
311
|
+
$sqlConditions[] = $conditionGroup;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
self::processSingleCondition($key, $value, $sqlConditions, $bindings, $dbType, $tableName, $prefix, $level);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private static function processSingleCondition($key, $value, &$sqlConditions, &$bindings, $dbType, $tableName, $prefix, $level)
|
|
321
|
+
{
|
|
322
|
+
$fieldQuoted = self::quoteColumnName($dbType, $key);
|
|
323
|
+
$qualifiedField = $tableName . '.' . $fieldQuoted;
|
|
324
|
+
|
|
325
|
+
if (is_array($value)) {
|
|
326
|
+
foreach ($value as $condition => $val) {
|
|
327
|
+
$bindingKey = ":" . $prefix . $key . "_" . $condition . $level;
|
|
328
|
+
switch ($condition) {
|
|
329
|
+
case 'contains':
|
|
330
|
+
case 'startsWith':
|
|
331
|
+
case 'endsWith':
|
|
332
|
+
case 'equals':
|
|
333
|
+
case 'not':
|
|
334
|
+
if ($val === null) {
|
|
335
|
+
$sqlConditions[] = "$qualifiedField IS NOT NULL";
|
|
336
|
+
} elseif ($val === '') {
|
|
337
|
+
$sqlConditions[] = "$qualifiedField != ''";
|
|
338
|
+
} else {
|
|
339
|
+
$validatedValue = Validator::string($val);
|
|
340
|
+
$likeOperator = $condition === 'contains' ? ($dbType == 'pgsql' ? 'ILIKE' : 'LIKE') : '=';
|
|
341
|
+
if ($condition === 'startsWith') $validatedValue .= '%';
|
|
342
|
+
if ($condition === 'endsWith') $validatedValue = '%' . $validatedValue;
|
|
343
|
+
if ($condition === 'contains') $validatedValue = '%' . $validatedValue . '%';
|
|
344
|
+
$sqlConditions[] = $condition === 'not' ? "$qualifiedField != $bindingKey" : "$qualifiedField $likeOperator $bindingKey";
|
|
345
|
+
$bindings[$bindingKey] = $validatedValue;
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
case 'gt':
|
|
349
|
+
case 'gte':
|
|
350
|
+
case 'lt':
|
|
351
|
+
case 'lte':
|
|
352
|
+
if (is_float($val)) {
|
|
353
|
+
$validatedValue = Validator::float($val);
|
|
354
|
+
} elseif (is_int($val)) {
|
|
355
|
+
$validatedValue = Validator::int($val);
|
|
356
|
+
} elseif (strtotime($val) !== false) {
|
|
357
|
+
$validatedValue = date('Y-m-d H:i:s', strtotime($val));
|
|
358
|
+
} else {
|
|
359
|
+
$validatedValue = Validator::string($val);
|
|
360
|
+
}
|
|
361
|
+
$operator = $condition === 'gt' ? '>' : ($condition === 'gte' ? '>=' : ($condition === 'lt' ? '<' : '<='));
|
|
362
|
+
$sqlConditions[] = "$qualifiedField $operator $bindingKey";
|
|
363
|
+
$bindings[$bindingKey] = $validatedValue;
|
|
364
|
+
break;
|
|
365
|
+
case 'in':
|
|
366
|
+
case 'notIn':
|
|
367
|
+
$inPlaceholders = [];
|
|
368
|
+
foreach ($val as $i => $inVal) {
|
|
369
|
+
$inKey = $bindingKey . "_" . $i;
|
|
370
|
+
$validatedValue = Validator::string($inVal);
|
|
371
|
+
$inPlaceholders[] = $inKey;
|
|
372
|
+
$bindings[$inKey] = $validatedValue;
|
|
373
|
+
}
|
|
374
|
+
$inClause = implode(', ', $inPlaceholders);
|
|
375
|
+
$sqlConditions[] = "$qualifiedField " . ($condition === 'notIn' ? 'NOT IN' : 'IN') . " ($inClause)";
|
|
376
|
+
break;
|
|
377
|
+
default:
|
|
378
|
+
// Handle other conditions or log an error/warning for unsupported conditions
|
|
379
|
+
throw new Exception("Unsupported condition: $condition");
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
if ($value === null) {
|
|
385
|
+
$sqlConditions[] = "$qualifiedField IS NULL";
|
|
386
|
+
} elseif ($value === '') {
|
|
387
|
+
$sqlConditions[] = "$qualifiedField = ''";
|
|
388
|
+
} else {
|
|
389
|
+
$bindingKey = ":" . $prefix . $key . $level;
|
|
390
|
+
$validatedValue = Validator::string($value);
|
|
391
|
+
$sqlConditions[] = "$qualifiedField = $bindingKey";
|
|
392
|
+
$bindings[$bindingKey] = $validatedValue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private static function processNotCondition($conditions, &$sqlConditions, &$bindings, $dbType, $tableName, $prefix, $level = 0)
|
|
398
|
+
{
|
|
399
|
+
foreach ($conditions as $key => $value) {
|
|
400
|
+
self::processSingleCondition($key, $value, $sqlConditions, $bindings, $dbType, $tableName, $prefix . 'NOT_', $level);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Checks for invalid keys in the provided data array.
|
|
406
|
+
*
|
|
407
|
+
* This method iterates through the provided data array and checks if each key exists in the allowed fields array.
|
|
408
|
+
* If a key is found that does not exist in the allowed fields, an exception is thrown.
|
|
409
|
+
*
|
|
410
|
+
* @param array $data The data array to check for invalid keys.
|
|
411
|
+
* @param array $fields The array of allowed field names.
|
|
412
|
+
* @param string $modelName The name of the model being checked.
|
|
413
|
+
*
|
|
414
|
+
* @throws Exception If an invalid key is found in the data array.
|
|
415
|
+
*/
|
|
416
|
+
public static function checkForInvalidKeys(array $data, array $fields, string $modelName)
|
|
417
|
+
{
|
|
418
|
+
foreach ($data as $key => $value) {
|
|
419
|
+
if (!empty($key) && !in_array($key, $fields)) {
|
|
420
|
+
throw new Exception("The field '$key' does not exist in the $modelName model. Accepted fields: " . implode(', ', $fields));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Modifies the given SQL query based on the provided criteria.
|
|
427
|
+
*
|
|
428
|
+
* This function handles the following criteria:
|
|
429
|
+
* - _max: Adds MAX() aggregate functions for specified columns.
|
|
430
|
+
* - _min: Adds MIN() aggregate functions for specified columns.
|
|
431
|
+
* - _count: Adds COUNT() aggregate functions for specified columns.
|
|
432
|
+
* - _avg: Adds AVG() aggregate functions for specified columns.
|
|
433
|
+
* - _sum: Adds SUM() aggregate functions for specified columns.
|
|
434
|
+
* - orderBy: Adds ORDER BY clause based on specified columns and directions.
|
|
435
|
+
* - take: Adds LIMIT clause to restrict the number of rows returned.
|
|
436
|
+
* - skip: Adds OFFSET clause to skip a specified number of rows.
|
|
437
|
+
*
|
|
438
|
+
* @param array $criteria An associative array of criteria for modifying the query.
|
|
439
|
+
* @param string &$sql The SQL query string to be modified.
|
|
440
|
+
* @param string $dbType The type of the database (e.g., 'mysql', 'pgsql').
|
|
441
|
+
* @param string $tableName The name of the table being queried.
|
|
442
|
+
*/
|
|
443
|
+
public static function queryOptions(array $criteria, string &$sql, $dbType, $tableName)
|
|
444
|
+
{
|
|
445
|
+
// Handle _max, _min, _count, _avg, and _sum
|
|
446
|
+
$selectParts = [];
|
|
447
|
+
if (isset($criteria['_max'])) {
|
|
448
|
+
foreach ($criteria['_max'] as $column => $enabled) {
|
|
449
|
+
if ($enabled) {
|
|
450
|
+
$selectParts[] = "MAX($tableName." . self::quoteColumnName($dbType, $column) . ") AS max_$column";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (isset($criteria['_min'])) {
|
|
455
|
+
foreach ($criteria['_min'] as $column => $enabled) {
|
|
456
|
+
if ($enabled) {
|
|
457
|
+
$selectParts[] = "MIN($tableName." . self::quoteColumnName($dbType, $column) . ") AS min_$column";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (isset($criteria['_count'])) {
|
|
462
|
+
foreach ($criteria['_count'] as $column => $enabled) {
|
|
463
|
+
if ($enabled) {
|
|
464
|
+
$selectParts[] = "COUNT($tableName." . self::quoteColumnName($dbType, $column) . ") AS count_$column";
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (isset($criteria['_avg'])) {
|
|
469
|
+
foreach ($criteria['_avg'] as $column => $enabled) {
|
|
470
|
+
if ($enabled) {
|
|
471
|
+
$selectParts[] = "AVG($tableName." . self::quoteColumnName($dbType, $column) . ") AS avg_$column";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (isset($criteria['_sum'])) {
|
|
476
|
+
foreach ($criteria['_sum'] as $column => $enabled) {
|
|
477
|
+
if ($enabled) {
|
|
478
|
+
$selectParts[] = "SUM($tableName." . self::quoteColumnName($dbType, $column) . ") AS sum_$column";
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Prepend to SELECT if _max, _min, _count, _avg, or _sum is specified
|
|
484
|
+
if (!empty($selectParts)) {
|
|
485
|
+
$sql = str_replace('SELECT', 'SELECT ' . implode(', ', $selectParts) . ',', $sql);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Handle ORDER BY
|
|
489
|
+
if (isset($criteria['orderBy'])) {
|
|
490
|
+
$orderByParts = self::parseOrderBy($criteria['orderBy'], $dbType, $tableName);
|
|
491
|
+
if (!empty($orderByParts)) {
|
|
492
|
+
$sql .= " ORDER BY " . implode(', ', $orderByParts);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Handle LIMIT (take)
|
|
497
|
+
if (isset($criteria['take'])) {
|
|
498
|
+
$sql .= " LIMIT " . intval($criteria['take']);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Handle OFFSET (skip)
|
|
502
|
+
if (isset($criteria['skip'])) {
|
|
503
|
+
$sql .= " OFFSET " . intval($criteria['skip']);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private static function parseOrderBy(array $orderBy, $dbType, $tableName): array
|
|
508
|
+
{
|
|
509
|
+
$orderByParts = [];
|
|
510
|
+
|
|
511
|
+
foreach ($orderBy as $column => $direction) {
|
|
512
|
+
if (is_array($direction)) {
|
|
513
|
+
// Handle nested orderBy
|
|
514
|
+
foreach ($direction as $nestedColumn => $nestedDirection) {
|
|
515
|
+
$nestedDirection = strtolower($nestedDirection) === 'desc' ? 'desc' : 'asc';
|
|
516
|
+
$orderByParts[] = self::quoteColumnName($dbType, $column) . "." . self::quoteColumnName($dbType, $nestedColumn) . " $nestedDirection";
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
// Handle regular orderBy
|
|
520
|
+
$direction = strtolower($direction) === 'desc' ? 'desc' : 'asc';
|
|
521
|
+
$orderByParts[] = "$tableName." . self::quoteColumnName($dbType, $column) . " $direction";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return $orderByParts;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Quotes a column name based on the database type.
|
|
530
|
+
*
|
|
531
|
+
* This method adds appropriate quotes around the column name depending on the database type.
|
|
532
|
+
* For PostgreSQL and SQLite, it uses double quotes. For other databases, it uses backticks.
|
|
533
|
+
* If the column name is empty or null, it simply returns an empty string.
|
|
534
|
+
*
|
|
535
|
+
* @param string $dbType The type of the database (e.g., 'pgsql', 'sqlite', 'mysql').
|
|
536
|
+
* @param string|null $column The name of the column to be quoted.
|
|
537
|
+
* @return string The quoted column name or an empty string if the column is null or empty.
|
|
538
|
+
*/
|
|
539
|
+
public static function quoteColumnName(string $dbType, ?string $column): string
|
|
540
|
+
{
|
|
541
|
+
if (empty($column)) {
|
|
542
|
+
return '';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return ($dbType === 'pgsql' || $dbType === 'sqlite') ? "\"$column\"" : "`$column`";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Parses a column string into an array of segments.
|
|
550
|
+
*
|
|
551
|
+
* This method performs the following steps:
|
|
552
|
+
* 1. Replaces occurrences of '._.' with '._ARRAY_.' in the input string.
|
|
553
|
+
* 2. Splits the modified string on '.' to create an array of parts.
|
|
554
|
+
* 3. Converts '_ARRAY_' placeholders into special markers in the resulting array.
|
|
555
|
+
*
|
|
556
|
+
* @param string $column The column string to be parsed.
|
|
557
|
+
* @return array An array of segments derived from the input column string.
|
|
558
|
+
*/
|
|
559
|
+
public static function parseColumn(string $column): array
|
|
560
|
+
{
|
|
561
|
+
// Step 1: replace ._. with ._ARRAY_.
|
|
562
|
+
$column = str_replace('._.', '._ARRAY_.', $column);
|
|
563
|
+
|
|
564
|
+
// Step 2: split on '.'
|
|
565
|
+
$parts = explode('.', $column);
|
|
566
|
+
|
|
567
|
+
// Step 3: convert '_ARRAY_' placeholders into special markers in the array
|
|
568
|
+
$segments = [];
|
|
569
|
+
foreach ($parts as $part) {
|
|
570
|
+
if ($part === '_ARRAY_') {
|
|
571
|
+
$segments[] = '_ARRAY_';
|
|
572
|
+
} else {
|
|
573
|
+
$segments[] = $part;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return $segments;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Recursively builds SQL JOIN statements and SELECT fields for nested relations.
|
|
582
|
+
*
|
|
583
|
+
* @param array $include An array of relations to include, with optional nested includes.
|
|
584
|
+
* @param string $parentAlias The alias of the parent table in the SQL query.
|
|
585
|
+
* @param array &$joins An array to collect the generated JOIN statements.
|
|
586
|
+
* @param array &$selectFields An array to collect the generated SELECT fields.
|
|
587
|
+
* @param mixed $pdo The PDO instance for database connection.
|
|
588
|
+
* @param string $dbType The type of the database (e.g., 'mysql', 'pgsql').
|
|
589
|
+
* @param object|null $model The model object containing metadata about the relations.
|
|
590
|
+
*
|
|
591
|
+
* @throws Exception If relation metadata is not defined or if required fields/references are missing.
|
|
592
|
+
*/
|
|
593
|
+
public static function buildJoinsRecursively(
|
|
594
|
+
array $include,
|
|
595
|
+
string $parentAlias,
|
|
596
|
+
array &$joins,
|
|
597
|
+
array &$selectFields,
|
|
598
|
+
mixed $pdo,
|
|
599
|
+
string $dbType,
|
|
600
|
+
?object $model = null,
|
|
601
|
+
string $defaultJoinType = 'INNER JOIN',
|
|
602
|
+
string $pathPrefix = ''
|
|
603
|
+
) {
|
|
604
|
+
foreach ($include as $relationName => $relationOptions) {
|
|
605
|
+
$joinType = isset($relationOptions['join.type'])
|
|
606
|
+
? strtoupper($relationOptions['join.type']) . ' JOIN'
|
|
607
|
+
: $defaultJoinType;
|
|
608
|
+
|
|
609
|
+
if (!in_array($joinType, ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'], true)) {
|
|
610
|
+
throw new Exception("Invalid join type: $joinType (expected 'INNER JOIN', 'LEFT JOIN', or 'RIGHT JOIN')");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Extract nested includes
|
|
614
|
+
$nestedInclude = [];
|
|
615
|
+
if (is_array($relationOptions) && isset($relationOptions['include']) && is_array($relationOptions['include'])) {
|
|
616
|
+
$nestedInclude = $relationOptions['include'];
|
|
617
|
+
}
|
|
618
|
+
$isNested = !empty($nestedInclude);
|
|
619
|
+
|
|
620
|
+
// 1. Fetch metadata
|
|
621
|
+
if (!isset($model->fields[$relationName])) {
|
|
622
|
+
throw new Exception("Relation metadata not defined for '$relationName' in " . get_class($model));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 2. Identify related class
|
|
626
|
+
$relatedClassName = "Lib\\Prisma\\Classes\\" . $model->fields[$relationName]['type'] ?? null;
|
|
627
|
+
$relatedClass = new $relatedClassName($pdo);
|
|
628
|
+
if (!$relatedClass) {
|
|
629
|
+
throw new Exception("Could not instantiate class for relation '$relationName'.");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// 3. Determine DB table
|
|
633
|
+
$joinTable = $relatedClass->tableName ?? null;
|
|
634
|
+
if (!$joinTable) {
|
|
635
|
+
throw new Exception("No valid table name found for relation '$relationName'.");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
$newAliasQuoted = PPHPUtility::quoteColumnName($dbType, $relationName);
|
|
639
|
+
|
|
640
|
+
// 5. Build the ON condition
|
|
641
|
+
$joinConditions = [];
|
|
642
|
+
$fieldsRelatedWithKeys = $model->fieldsRelatedWithKeys[$relationName] ?? null;
|
|
643
|
+
if ($fieldsRelatedWithKeys) {
|
|
644
|
+
$relationToFields = $fieldsRelatedWithKeys['relationToFields'] ?? [];
|
|
645
|
+
$relationFromFields = $fieldsRelatedWithKeys['relationFromFields'] ?? [];
|
|
646
|
+
|
|
647
|
+
if (count($relationToFields) !== count($relationFromFields)) {
|
|
648
|
+
throw new Exception("Mismatched 'references' and 'fields' for '$relationName'.");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
foreach ($relationToFields as $index => $toField) {
|
|
652
|
+
$fromField = $relationFromFields[$index] ?? null;
|
|
653
|
+
if (!$toField || !$fromField) {
|
|
654
|
+
throw new Exception("Missing references/fields for '$relationName' at index $index.");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
$fromFieldExists = array_key_exists($fromField, $model->fields);
|
|
658
|
+
|
|
659
|
+
if ($fromFieldExists) {
|
|
660
|
+
$joinConditions[] = sprintf(
|
|
661
|
+
'%s.%s = %s.%s',
|
|
662
|
+
$parentAlias,
|
|
663
|
+
PPHPUtility::quoteColumnName($dbType, $fromField),
|
|
664
|
+
$newAliasQuoted,
|
|
665
|
+
PPHPUtility::quoteColumnName($dbType, $toField)
|
|
666
|
+
);
|
|
667
|
+
} else {
|
|
668
|
+
$joinConditions[] = sprintf(
|
|
669
|
+
'%s.%s = %s.%s',
|
|
670
|
+
$parentAlias,
|
|
671
|
+
PPHPUtility::quoteColumnName($dbType, $toField),
|
|
672
|
+
$newAliasQuoted,
|
|
673
|
+
PPHPUtility::quoteColumnName($dbType, $fromField)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
throw new Exception("Relation '$relationName' not properly defined.");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
$joinCondition = implode(' AND ', $joinConditions);
|
|
682
|
+
|
|
683
|
+
// 6. Add the JOIN statement
|
|
684
|
+
$joinTableQuoted = PPHPUtility::quoteColumnName($dbType, $joinTable);
|
|
685
|
+
$joins[] = sprintf(
|
|
686
|
+
'%s %s AS %s ON %s',
|
|
687
|
+
$joinType,
|
|
688
|
+
$joinTableQuoted,
|
|
689
|
+
$newAliasQuoted,
|
|
690
|
+
$joinCondition
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// 7. ADD COLUMNS (with the *full path prefix*).
|
|
694
|
+
// e.g. if pathPrefix="" and relationName="post", then childPathPrefix="post".
|
|
695
|
+
// if pathPrefix="post" and relationName="categories", => "post.categories".
|
|
696
|
+
$childPathPrefix = $pathPrefix
|
|
697
|
+
? $pathPrefix . '.' . $relationName
|
|
698
|
+
: $relationName;
|
|
699
|
+
|
|
700
|
+
$fieldsOnly = $relatedClass->fieldsOnly ?? [];
|
|
701
|
+
foreach ($fieldsOnly as $field) {
|
|
702
|
+
$quotedField = PPHPUtility::quoteColumnName($dbType, $field);
|
|
703
|
+
$columnAlias = $childPathPrefix . '.' . $field; // e.g. "post.categories.id"
|
|
704
|
+
$columnAliasQuoted = PPHPUtility::quoteColumnName($dbType, $columnAlias);
|
|
705
|
+
|
|
706
|
+
$selectFields[] = sprintf(
|
|
707
|
+
'%s.%s AS %s',
|
|
708
|
+
$newAliasQuoted,
|
|
709
|
+
$quotedField,
|
|
710
|
+
$columnAliasQuoted
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 8. Recurse for nested includes
|
|
715
|
+
if ($isNested) {
|
|
716
|
+
self::buildJoinsRecursively(
|
|
717
|
+
$nestedInclude,
|
|
718
|
+
$newAliasQuoted, // use this for the next level's JOIN
|
|
719
|
+
$joins,
|
|
720
|
+
$selectFields,
|
|
721
|
+
$pdo,
|
|
722
|
+
$dbType,
|
|
723
|
+
$relatedClass,
|
|
724
|
+
$defaultJoinType,
|
|
725
|
+
$childPathPrefix // pass down the updated path
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
public static function arrayToObjectRecursive($data)
|
|
732
|
+
{
|
|
733
|
+
// If it's not an array, there's nothing to convert; just return as-is
|
|
734
|
+
if (!is_array($data)) {
|
|
735
|
+
return $data;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Convert each item in the array and then convert the array itself
|
|
739
|
+
foreach ($data as $key => $value) {
|
|
740
|
+
$data[$key] = self::arrayToObjectRecursive($value);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return (object) $data;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
public static function arrayToClassRecursive(array $data, string $class)
|
|
747
|
+
{
|
|
748
|
+
if (!class_exists($class)) {
|
|
749
|
+
throw new InvalidArgumentException("Class {$class} does not exist.");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
$reflection = new ReflectionClass($class);
|
|
753
|
+
$instance = $reflection->newInstanceWithoutConstructor();
|
|
754
|
+
$properties = $reflection->getProperties();
|
|
755
|
+
|
|
756
|
+
foreach ($properties as $property) {
|
|
757
|
+
$propertyName = $property->getName();
|
|
758
|
+
|
|
759
|
+
if (array_key_exists($propertyName, $data)) {
|
|
760
|
+
$propertyType = $property->getType();
|
|
761
|
+
$typeNames = [];
|
|
762
|
+
|
|
763
|
+
if ($propertyType instanceof ReflectionUnionType) {
|
|
764
|
+
$typeNames = array_map(fn($t) => $t->getName(), $propertyType->getTypes());
|
|
765
|
+
} elseif ($propertyType instanceof ReflectionNamedType) {
|
|
766
|
+
$typeNames[] = $propertyType->getName();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (in_array(DateTime::class, $typeNames)) {
|
|
770
|
+
$instance->$propertyName = !empty($data[$propertyName])
|
|
771
|
+
? new DateTime($data[$propertyName])
|
|
772
|
+
: null;
|
|
773
|
+
} elseif (in_array(BigDecimal::class, $typeNames)) {
|
|
774
|
+
$instance->$propertyName = BigDecimal::of($data[$propertyName]);
|
|
775
|
+
} elseif (in_array(BigInteger::class, $typeNames)) {
|
|
776
|
+
$instance->$propertyName = BigInteger::of($data[$propertyName]);
|
|
777
|
+
} elseif (count(array_intersect($typeNames, ['int', 'float', 'string', 'bool'])) > 0) {
|
|
778
|
+
$instance->$propertyName = $data[$propertyName];
|
|
779
|
+
} elseif (in_array('array', $typeNames) && isset($data[$propertyName]) && is_array($data[$propertyName])) {
|
|
780
|
+
// Check array type
|
|
781
|
+
$arrayType = self::checkArrayContents($data[$propertyName]);
|
|
782
|
+
|
|
783
|
+
// Handle array-to-object conversion
|
|
784
|
+
$docComment = $property->getDocComment();
|
|
785
|
+
if ($docComment && preg_match('/@var\s+([^\s\[\]]+)\[]/', $docComment, $matches)) {
|
|
786
|
+
$elementType = $matches[1];
|
|
787
|
+
if (class_exists($elementType)) {
|
|
788
|
+
if ($arrayType === ArrayType::Indexed) {
|
|
789
|
+
$instance->$propertyName = array_map(
|
|
790
|
+
fn($item) => self::arrayToClassRecursive($item, $elementType),
|
|
791
|
+
$data[$propertyName]
|
|
792
|
+
);
|
|
793
|
+
} else {
|
|
794
|
+
// If associative, keep as array
|
|
795
|
+
$instance->$propertyName = $data[$propertyName];
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
$instance->$propertyName = $data[$propertyName]; // Default to raw array
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
$instance->$propertyName = $data[$propertyName]; // Default to raw array
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
foreach ($typeNames as $typeName) {
|
|
805
|
+
if (class_exists($typeName)) {
|
|
806
|
+
if (is_array($data[$propertyName])) {
|
|
807
|
+
$arrayType = self::checkArrayContents($data[$propertyName]);
|
|
808
|
+
|
|
809
|
+
if ($arrayType === ArrayType::Associative) {
|
|
810
|
+
$instance->$propertyName = self::arrayToClassRecursive($data[$propertyName], $typeName);
|
|
811
|
+
} elseif ($arrayType === ArrayType::Indexed) {
|
|
812
|
+
$instance->$propertyName = array_map(
|
|
813
|
+
fn($item) => self::arrayToClassRecursive($item, $typeName),
|
|
814
|
+
$data[$propertyName] ?? []
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} elseif ($data[$propertyName] instanceof $typeName) {
|
|
818
|
+
$instance->$propertyName = $data[$propertyName];
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return $instance;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Recursively sets a value in a multi-dimensional array
|
|
832
|
+
* based on an array of keys. E.g.:
|
|
833
|
+
* setNestedValue($arr, ['post','categories','id'], 'some-id')
|
|
834
|
+
* becomes
|
|
835
|
+
* $arr['post']['categories']['id'] = 'some-id';
|
|
836
|
+
*/
|
|
837
|
+
public static function setNestedValue(array &$array, array $keys, $value)
|
|
838
|
+
{
|
|
839
|
+
// Take the first key from the array
|
|
840
|
+
$key = array_shift($keys);
|
|
841
|
+
|
|
842
|
+
// If this key doesn't exist yet, initialize it
|
|
843
|
+
if (!isset($array[$key])) {
|
|
844
|
+
// Decide if you want an empty array or some default
|
|
845
|
+
$array[$key] = [];
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// If there are no more keys left, this is where we set the final value
|
|
849
|
+
if (empty($keys)) {
|
|
850
|
+
$array[$key] = $value;
|
|
851
|
+
} else {
|
|
852
|
+
// Otherwise, we recurse deeper
|
|
853
|
+
self::setNestedValue($array[$key], $keys, $value);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|