prisma-client-php 0.0.50 → 0.0.51

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.
@@ -1042,11 +1042,11 @@ final class PPHPUtility
1042
1042
  }
1043
1043
 
1044
1044
  public static function populateIncludedRelations(
1045
- array $records,
1046
- array $includes,
1047
- array $fields,
1048
- array $fieldsRelatedWithKeys,
1049
- PDO $pdo,
1045
+ array $records,
1046
+ array $includes,
1047
+ array $fields,
1048
+ array $fieldsRelatedWithKeys,
1049
+ PDO $pdo,
1050
1050
  string $dbType,
1051
1051
  ): array {
1052
1052
  $isSingle = !isset($records[0]) || !is_array($records[0]);
@@ -1054,200 +1054,286 @@ final class PPHPUtility
1054
1054
  $records = [$records];
1055
1055
  }
1056
1056
 
1057
- foreach ($records as $recordIndex => $singleRecord) {
1058
- foreach ($includes as $key => $value) {
1059
- if (!isset($fields[$key]) || !isset($fieldsRelatedWithKeys[$key])) {
1060
- continue;
1061
- }
1057
+ foreach ($includes as $relationName => $relationOpts) {
1058
+ if ($relationOpts === false) {
1059
+ continue;
1060
+ }
1061
+ if (!isset($fields[$relationName], $fieldsRelatedWithKeys[$relationName])) {
1062
+ continue;
1063
+ }
1062
1064
 
1063
- $relatedField = $fields[$key];
1064
- $relatedFieldKeys = $fieldsRelatedWithKeys[$key];
1065
+ $relatedField = $fields[$relationName];
1066
+ $relatedKeys = $fieldsRelatedWithKeys[$relationName];
1067
+ $relatedInstance = self::makeRelatedInstance($relatedField['type'], $pdo);
1065
1068
 
1066
- $relatedClass = "Lib\\Prisma\\Classes\\" . $relatedField['type'];
1067
- if (!class_exists($relatedClass)) {
1068
- throw new Exception("Class $relatedClass does not exist.");
1069
+ $instanceField = self::pickOppositeField(
1070
+ $relatedInstance->_fields,
1071
+ $relatedField['relationName'],
1072
+ $relatedField['isList']
1073
+ );
1074
+
1075
+ if ($relatedField['isList'] && !$instanceField['isList']) {
1076
+ $childFk = $instanceField['relationFromFields'][0] ?? null;
1077
+ $parentPk = $instanceField['relationToFields'][0] ?? null;
1078
+ if ($childFk === null || $parentPk === null) {
1079
+ goto PER_RECORD;
1069
1080
  }
1070
1081
 
1071
- $reflectionClass = new ReflectionClass($relatedClass);
1072
- $relatedInstance = $reflectionClass->newInstance($pdo);
1073
- $relatedInstanceField = $relatedInstance->_fieldByRelationName[$relatedField['relationName']];
1082
+ $parentIds = array_values(
1083
+ array_unique(
1084
+ array_filter(
1085
+ array_column($records, $parentPk),
1086
+ static fn($v) => $v !== null
1087
+ )
1088
+ )
1089
+ );
1090
+ if (!$parentIds) {
1091
+ foreach ($records as &$rec) {
1092
+ $rec[$relationName] = [];
1093
+ }
1094
+ unset($rec);
1095
+ continue;
1096
+ }
1074
1097
 
1075
- $whereConditions = [];
1076
- foreach ($relatedFieldKeys['relationFromFields'] as $index => $fromField) {
1077
- $toField = $relatedFieldKeys['relationToFields'][$index];
1098
+ [$base] = self::buildQueryOptions(
1099
+ [],
1100
+ $relationOpts,
1101
+ $relatedField,
1102
+ $relatedKeys,
1103
+ $instanceField
1104
+ );
1078
1105
 
1079
- if (!array_key_exists($toField, $singleRecord) && !array_key_exists($fromField, $singleRecord)) {
1080
- continue 2;
1081
- }
1106
+ $groups = self::loadOneToManyBatch($relatedInstance, $childFk, $parentIds, $base);
1082
1107
 
1083
- if (empty($relatedInstanceField['relationFromFields']) && empty($relatedInstanceField['relationToFields'])) {
1084
- if ($relatedInstanceField['relationName'] === $relatedField['relationName'] && array_key_exists($fromField, $singleRecord)) {
1085
- $whereConditions[$toField] = $singleRecord[$fromField];
1086
- } else {
1087
- $whereConditions[$toField] = $singleRecord[$toField];
1088
- }
1089
- } elseif ($relatedInstanceField['isList']) {
1090
- $whereConditions[$toField] = $singleRecord[$fromField];
1091
- } else {
1092
- $whereConditions[$fromField] = $singleRecord[$toField];
1093
- }
1108
+ foreach ($records as &$rec) {
1109
+ $rec[$relationName] = $groups[$rec[$parentPk]] ?? [];
1094
1110
  }
1111
+ unset($rec);
1112
+ continue;
1113
+ }
1095
1114
 
1096
- if (isset($value['where']) && is_array($value['where'])) {
1097
- $whereConditions = array_merge($whereConditions, $value['where']);
1098
- }
1115
+ PER_RECORD:
1116
+ foreach ($records as $idx => $singleRecord) {
1117
+ [$baseQuery, $where] = self::buildQueryOptions(
1118
+ $singleRecord,
1119
+ $relationOpts,
1120
+ $relatedField,
1121
+ $relatedKeys,
1122
+ $instanceField
1123
+ );
1099
1124
 
1100
- $selectFields = [];
1101
- if (isset($value['select']) && is_array($value['select'])) {
1102
- foreach ($value['select'] as $field => $subSelect) {
1103
- if (is_array($subSelect)) {
1104
- $selectFields[$field] = $subSelect;
1105
- } elseif ((bool) $subSelect === true) {
1106
- if (is_numeric($field)) {
1107
- $selectFields[$field] = $subSelect;
1108
- } else {
1109
- $selectFields[$field] = true;
1110
- }
1111
- }
1112
- }
1125
+ if ($relatedField['isList'] && $instanceField['isList']) {
1126
+ $result = ($relatedField['type'] === $instanceField['type'])
1127
+ ? self::loadExplicitMany($relatedInstance, $relatedField, $instanceField, $singleRecord, $baseQuery, $fields)
1128
+ : self::loadImplicitMany($relatedInstance, $relatedField, $instanceField, $singleRecord, $baseQuery, $where, $dbType, $pdo);
1129
+ } elseif ($relatedField['isList']) {
1130
+ $result = self::loadOneToMany($relatedInstance, $baseQuery);
1131
+ } else {
1132
+ $result = self::loadOneToOne($relatedInstance, $baseQuery);
1113
1133
  }
1114
1134
 
1115
- $includeFields = [];
1116
- if (isset($value['include']) && is_array($value['include'])) {
1117
- foreach ($value['include'] as $field => $subInclude) {
1118
- if (is_array($subInclude)) {
1119
- $includeFields[$field] = $subInclude;
1120
- } elseif ((bool) $subInclude === true) {
1121
- if (is_numeric($field)) {
1122
- $includeFields[$field] = $subInclude;
1123
- } else {
1124
- $includeFields[$field] = true;
1125
- }
1126
- }
1127
- }
1128
- }
1135
+ $records[$idx][$relationName] = $result;
1136
+ }
1137
+ }
1129
1138
 
1130
- $omitFields = [];
1131
- if (isset($value['omit']) && is_array($value['omit'])) {
1132
- foreach ($value['omit'] as $field => $shouldOmit) {
1133
- if ((bool) $shouldOmit === true) {
1134
- $omitFields[$field] = true;
1135
- }
1136
- }
1137
- }
1139
+ return $isSingle ? $records[0] : $records;
1140
+ }
1138
1141
 
1139
- $queryOptions = ['where' => $whereConditions];
1140
- if (!empty($selectFields)) {
1141
- $queryOptions['select'] = $selectFields;
1142
- }
1142
+ private static function pickOppositeField(array $allFields, string $relationName, bool $isListOnCaller): array
1143
+ {
1144
+ $candidates = array_values(array_filter(
1145
+ $allFields,
1146
+ static fn($f) => ($f['relationName'] ?? null) === $relationName
1147
+ ));
1143
1148
 
1144
- if (!empty($includeFields)) {
1145
- $queryOptions['include'] = $includeFields;
1146
- }
1149
+ if (count($candidates) === 1) {
1150
+ return $candidates[0];
1151
+ }
1147
1152
 
1148
- if (!empty($omitFields)) {
1149
- $queryOptions['omit'] = $omitFields;
1150
- }
1153
+ foreach ($candidates as $f) {
1154
+ if ($f['isList'] !== $isListOnCaller) {
1155
+ return $f;
1156
+ }
1157
+ }
1151
1158
 
1152
- if ($relatedField['isList'] && $relatedInstanceField['isList']) {
1153
- if ($relatedField['type'] === $relatedInstanceField['type']) {
1154
- if (isset($queryOptions['where']) && empty($queryOptions['where'])) {
1155
- unset($queryOptions['where']);
1156
- }
1159
+ return $candidates[0];
1160
+ }
1157
1161
 
1158
- if (empty($queryOptions)) {
1159
- $relatedFieldByRelationName = array_filter(
1160
- $fields,
1161
- fn($field) => isset($field['relationName'])
1162
- && $field['relationName'] === $relatedInstanceField['relationName']
1163
- && isset($relatedInstanceField['name'])
1164
- && $relatedInstanceField['name'] !== $field['name']
1165
- );
1162
+ private static function loadOneToManyBatch(
1163
+ object $relatedInstance,
1164
+ string $childFk,
1165
+ array $parentIds,
1166
+ array $baseQuery,
1167
+ ): array {
1168
+ if (!isset($baseQuery['where'][$childFk])) {
1169
+ $baseQuery['where'][$childFk] = ['in' => $parentIds];
1170
+ }
1166
1171
 
1167
- $whereConditions = [];
1168
- if (!empty($relatedFieldByRelationName)) {
1169
- $firstRelatedField = reset($relatedFieldByRelationName);
1170
- if (!empty($firstRelatedField['relationFromFields']) && isset($firstRelatedField['relationFromFields'][0])) {
1171
- $whereConditions[$firstRelatedField['relationFromFields'][0]] = $singleRecord[$firstRelatedField['relationToFields'][0]];
1172
- }
1173
- }
1172
+ $rows = $relatedInstance->findMany($baseQuery);
1174
1173
 
1175
- if (!empty($whereConditions)) {
1176
- $relatedFieldResult = $relatedInstance->findMany(
1177
- ['where' => $whereConditions]
1178
- );
1179
- } else {
1180
- $relatedFieldResult = $relatedInstance->findMany();
1181
- }
1182
- } else {
1183
- $relatedFieldResult = $relatedInstance->findMany($queryOptions);
1184
- }
1185
- } else {
1186
- $implicitModelInfo = PPHPUtility::compareStringsAlphabetically($relatedField['type'], $relatedInstanceField['type']);
1187
- $searchColumn = ($relatedField['type'] === $implicitModelInfo['A']) ? 'B' : 'A';
1188
- $returnColumn = ($searchColumn === 'A') ? 'B' : 'A';
1189
-
1190
- $idField = null;
1191
- foreach ($relatedField['relationFromFields'] as $index => $fromField) {
1192
- if (isset($singleRecord[$fromField])) {
1193
- $idField = $fromField;
1194
- break;
1195
- }
1196
- }
1174
+ $grouped = [];
1175
+ foreach ($rows as $row) {
1176
+ $key = $row->{$childFk};
1177
+ $grouped[$key][] = $row;
1178
+ }
1197
1179
 
1198
- if (!$idField) {
1199
- foreach ($relatedInstance->_fields as $field) {
1200
- if ($field['isId']) {
1201
- $idField = $field['name'];
1202
- break;
1203
- }
1204
- }
1205
- }
1180
+ return $grouped;
1181
+ }
1206
1182
 
1207
- if (!$idField || !isset($singleRecord[$idField])) {
1208
- throw new Exception("No valid ID field found for relation lookup.");
1209
- }
1183
+ private static function loadOneToOne(object $relatedInstance, array $query): array|object|null
1184
+ {
1185
+ return $relatedInstance->findUnique($query);
1186
+ }
1210
1187
 
1211
- $idValue = $singleRecord[$idField];
1212
-
1213
- $tableName = PPHPUtility::quoteColumnName($dbType, $implicitModelInfo['Name']);
1214
- $searchColumn = PPHPUtility::quoteColumnName($dbType, $searchColumn);
1215
- $returnColumn = PPHPUtility::quoteColumnName($dbType, $returnColumn);
1216
- $sql = "SELECT " . $returnColumn . " FROM " . $tableName . " WHERE " . $searchColumn . " = :id";
1217
- $stmt = $pdo->prepare($sql);
1218
- $stmt->execute(['id' => $idValue]);
1219
- $implicitRecords = $stmt->fetchAll(PDO::FETCH_COLUMN);
1220
-
1221
- if (!empty($implicitRecords)) {
1222
- $queryOptions['where'] = array_merge($queryOptions['where'] ?? [], [
1223
- 'id' => [
1224
- 'in' => $implicitRecords
1225
- ]
1226
- ]);
1227
- $queryOptions = array_filter($queryOptions, fn($value) => !empty($value));
1228
- $relatedFieldResult = $relatedInstance->findMany($queryOptions);
1229
- } else {
1230
- $relatedFieldResult = [];
1231
- }
1232
- }
1233
- } else {
1234
- if ($relatedField['isList']) {
1235
- $relatedFieldResult = $relatedInstance->findMany($queryOptions);
1236
- } else {
1237
- $relatedFieldResult = $relatedInstance->findUnique($queryOptions);
1238
- }
1239
- }
1188
+ private static function loadOneToMany(object $relatedInstance, array $query): array
1189
+ {
1190
+ return $relatedInstance->findMany($query);
1191
+ }
1192
+
1193
+ private static function loadExplicitMany(
1194
+ object $relatedInstance,
1195
+ array $relatedField,
1196
+ array $relatedInstanceField,
1197
+ array $singleRecord,
1198
+ array $queryOptions,
1199
+ array $parentFields,
1200
+ ): array {
1201
+ if (isset($queryOptions['where']) && $queryOptions['where'] === []) {
1202
+ unset($queryOptions['where']);
1203
+ }
1240
1204
 
1241
- $singleRecord[$key] = $relatedFieldResult;
1205
+ if ($queryOptions === []) {
1206
+ $opposites = array_values(array_filter(
1207
+ $parentFields,
1208
+ fn($f) => ($f['relationName'] ?? null) === $relatedInstanceField['relationName'] &&
1209
+ $f['name'] !== $relatedInstanceField['name']
1210
+ ));
1211
+
1212
+ if ($opposites && isset($opposites[0]['relationFromFields'][0])) {
1213
+ $src = $opposites[0]['relationFromFields'][0];
1214
+ $dst = $opposites[0]['relationToFields'][0];
1215
+ $queryOptions['where'][$src] = $singleRecord[$dst] ?? null;
1242
1216
  }
1217
+ }
1218
+
1219
+ return $relatedInstance->findMany($queryOptions);
1220
+ }
1243
1221
 
1244
- $records[$recordIndex] = $singleRecord;
1222
+ private static function loadImplicitMany(
1223
+ object $relatedInstance,
1224
+ array $relatedField,
1225
+ array $relatedInstanceField,
1226
+ array $singleRecord,
1227
+ array $baseQuery,
1228
+ array $whereConditions,
1229
+ string $dbType,
1230
+ PDO $pdo,
1231
+ ): array {
1232
+ $info = PPHPUtility::compareStringsAlphabetically($relatedField['type'], $relatedInstanceField['type']);
1233
+ $searchColumn = ($relatedField['type'] === $info['A']) ? 'B' : 'A';
1234
+ $returnColumn = $searchColumn === 'A' ? 'B' : 'A';
1235
+ $idField = self::detectIdField($singleRecord, $relatedField, $relatedInstance);
1236
+ $idValue = $singleRecord[$idField] ?? null;
1237
+
1238
+ if ($idValue === null) {
1239
+ return [];
1245
1240
  }
1246
1241
 
1247
- if ($isSingle) {
1248
- return $records[0];
1242
+ $table = PPHPUtility::quoteColumnName($dbType, $info['Name']);
1243
+ $search = PPHPUtility::quoteColumnName($dbType, $searchColumn);
1244
+ $return = PPHPUtility::quoteColumnName($dbType, $returnColumn);
1245
+ $sql = "SELECT {$return} FROM {$table} WHERE {$search} = :id";
1246
+ $stmt = $pdo->prepare($sql);
1247
+ $stmt->execute(['id' => $idValue]);
1248
+ $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
1249
+
1250
+ if (!$ids) {
1251
+ return [];
1252
+ }
1253
+
1254
+ $baseQuery['where'] = array_merge($whereConditions, ['id' => ['in' => $ids]]);
1255
+ return $relatedInstance->findMany($baseQuery);
1256
+ }
1257
+
1258
+ private static function makeRelatedInstance(string $model, PDO $pdo): object
1259
+ {
1260
+ $fqcn = "Lib\\Prisma\\Classes\\{$model}";
1261
+ if (!class_exists($fqcn)) {
1262
+ throw new Exception("Class {$fqcn} does not exist.");
1263
+ }
1264
+ return (new ReflectionClass($fqcn))->newInstance($pdo);
1265
+ }
1266
+
1267
+ private static function buildQueryOptions(
1268
+ array $singleRecord,
1269
+ mixed $relationOpts,
1270
+ array $relatedField,
1271
+ array $relatedFieldKeys,
1272
+ array $relatedInstanceField,
1273
+ ): array {
1274
+ if ($relationOpts === true) {
1275
+ $relationOpts = [];
1276
+ } elseif (!is_array($relationOpts)) {
1277
+ throw new Exception('include relation options must be array|true');
1278
+ }
1279
+
1280
+ $where = [];
1281
+ foreach ($relatedFieldKeys['relationFromFields'] as $i => $fromField) {
1282
+ $toField = $relatedFieldKeys['relationToFields'][$i];
1283
+
1284
+ if (!isset($singleRecord[$fromField]) && !isset($singleRecord[$toField])) {
1285
+ continue;
1286
+ }
1287
+
1288
+ if (empty($relatedInstanceField['relationFromFields']) && empty($relatedInstanceField['relationToFields'])) {
1289
+ $where[$toField] = $singleRecord[$fromField] ?? $singleRecord[$toField] ?? null;
1290
+ } elseif ($relatedInstanceField['isList']) {
1291
+ $where[$toField] = $singleRecord[$fromField];
1292
+ } else {
1293
+ $where[$fromField] = $singleRecord[$toField];
1294
+ }
1295
+ }
1296
+
1297
+ if (isset($relationOpts['where'])) {
1298
+ $where = array_merge($where, $relationOpts['where']);
1249
1299
  }
1250
1300
 
1251
- return $records;
1301
+ $query = ['where' => $where];
1302
+ foreach (['select', 'include', 'omit'] as $clause) {
1303
+ if (!isset($relationOpts[$clause])) {
1304
+ continue;
1305
+ }
1306
+ $query[$clause] = self::normaliseClause($relationOpts[$clause]);
1307
+ }
1308
+
1309
+ return [$query, $where];
1310
+ }
1311
+
1312
+ private static function normaliseClause(array $raw): array
1313
+ {
1314
+ $out = [];
1315
+ foreach ($raw as $k => $v) {
1316
+ if (is_array($v)) {
1317
+ $out[$k] = $v;
1318
+ } elseif ((bool)$v === true) {
1319
+ $out[is_numeric($k) ? $v : $k] = true;
1320
+ }
1321
+ }
1322
+ return $out;
1323
+ }
1324
+
1325
+ private static function detectIdField(array $singleRecord, array $relatedField, object $relatedInstance): string
1326
+ {
1327
+ foreach ($relatedField['relationFromFields'] as $from) {
1328
+ if (isset($singleRecord[$from])) {
1329
+ return $from;
1330
+ }
1331
+ }
1332
+ foreach ($relatedInstance->_fields as $f) {
1333
+ if ($f['isId']) {
1334
+ return $f['name'];
1335
+ }
1336
+ }
1337
+ throw new Exception('Unable to determine ID field for implicit many‑to‑many lookup.');
1252
1338
  }
1253
1339
  }
@@ -5,7 +5,7 @@ namespace Lib\Prisma\Model;
5
5
  interface IModel
6
6
  {
7
7
  public function aggregate(array $operation);
8
- // public function createMany(array $data);
8
+ public function createMany(array $data);
9
9
  // public function createManyAndReturn(array $data);
10
10
  public function create(array $data);
11
11
  // public function deleteMany(array $criteria);
@@ -20,5 +20,5 @@ interface IModel
20
20
  // public function updateManyAndReturn(array $data);
21
21
  // public function update(array $data);
22
22
  // public function upsert(array $data);
23
- public function count(array $criteria);
23
+ // public function count(array $criteria);
24
24
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prisma-client-php",
3
3
  "description": "Prisma Client PHP is an auto-generated query builder that enables type-safe database access in PHP.",
4
- "version": "0.0.50",
4
+ "version": "0.0.51",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {