1919
2020class CSV extends Source
2121{
22+ private const ALLOWED_INTERNALS = [
23+ '$id ' => true ,
24+ '$permissions ' => true ,
25+ '$createdAt ' => true ,
26+ '$updatedAt ' => true ,
27+ ];
28+
2229 private string $ filePath ;
2330
2431 /**
@@ -120,7 +127,6 @@ protected function exportGroupDatabases(int $batchSize, array $resources): void
120127 */
121128 private function exportRows (int $ batchSize ): void
122129 {
123-
124130 $ columns = [];
125131 $ lastColumn = null ;
126132
@@ -147,7 +153,9 @@ private function exportRows(int $batchSize): void
147153 }
148154 }
149155
150- $ arrayKeys = [];
156+ $ arrayKeys = [
157+ '$permissions ' => true ,
158+ ];
151159 $ columnTypes = [];
152160 $ manyToManyKeys = [];
153161
@@ -182,7 +190,7 @@ private function exportRows(int $batchSize): void
182190
183191 $ this ->withCSVStream (function ($ stream , $ delimiter ) use ($ columnTypes , $ manyToManyKeys , $ arrayKeys , $ table , $ batchSize ) {
184192 $ headers = fgetcsv ($ stream );
185- if (! is_array ($ headers ) || count ($ headers ) === 0 ) {
193+ if (!is_array ($ headers ) || count ($ headers ) === 0 ) {
186194 return ;
187195 }
188196
@@ -206,8 +214,11 @@ private function exportRows(int $batchSize): void
206214 $ parsedValue = trim ($ value );
207215 $ type = $ columnTypes [$ key ] ?? null ;
208216
209- if (! isset ($ type )) {
210- continue ;
217+ if (!isset ($ type ) && $ key !== '$permissions ' ) {
218+ if (isset (self ::ALLOWED_INTERNALS [$ key ])) {
219+ continue ;
220+ }
221+ throw new \Exception ('Unexpected attribute in CSV: ' .$ key );
211222 }
212223
213224 if (isset ($ manyToManyKeys [$ key ])) {
@@ -216,7 +227,7 @@ private function exportRows(int $batchSize): void
216227 : array_values (
217228 array_filter (
218229 array_map (
219- ' trim ' ,
230+ trim (...) ,
220231 explode (', ' , $ parsedValue )
221232 )
222233 )
@@ -229,7 +240,12 @@ private function exportRows(int $batchSize): void
229240 $ parsedData [$ key ] = [];
230241 } else {
231242 $ arrayValues = str_getcsv ($ parsedValue );
232- $ arrayValues = array_map ('trim ' , $ arrayValues );
243+ $ arrayValues = array_map (trim (...), $ arrayValues );
244+
245+ // Special handling for permissions to unescape quotes
246+ if ($ key === '$permissions ' ) {
247+ $ arrayValues = array_map (stripslashes (...), $ arrayValues );
248+ }
233249
234250 $ parsedData [$ key ] = array_map (function ($ item ) use ($ type ) {
235251 return match ($ type ) {
@@ -254,11 +270,18 @@ private function exportRows(int $batchSize): void
254270 }
255271
256272 $ rowId = $ parsedData ['$id ' ] ?? 'unique() ' ;
273+ $ permissions = $ parsedData ['$permissions ' ] ?? [];
274+
257275
258- // `$id`, `$permissions` in the doc can cause issues!
259276 unset($ parsedData ['$id ' ], $ parsedData ['$permissions ' ]);
260277
261- $ row = new Row ($ rowId , $ table , $ parsedData );
278+ $ row = new Row (
279+ $ rowId ,
280+ $ table ,
281+ $ parsedData ,
282+ $ permissions ,
283+ );
284+
262285 $ buffer [] = $ row ;
263286
264287 if (count ($ buffer ) === $ batchSize ) {
@@ -350,25 +373,22 @@ private function withCsvStream(callable $callback): void
350373 */
351374 private function validateCSVHeaders (array $ headers , array $ columnTypes ): void
352375 {
353- $ expectedColumns = array_keys ($ columnTypes );
354-
355- // Ignore keys like $id, $permissions, etc.
356- $ filteredHeaders = array_filter ($ headers , fn ($ key ) => ! str_starts_with ($ key , '$ ' ));
357-
358- $ extraColumns = array_diff ($ filteredHeaders , $ expectedColumns );
359- $ missingColumns = array_diff ($ expectedColumns , $ filteredHeaders );
376+ $ internalColumns = ['$id ' , '$permissions ' , '$createdAt ' , '$updatedAt ' ];
377+ $ expectedColumns = \array_keys ($ columnTypes );
378+ $ extraColumns = \array_diff ($ headers , $ expectedColumns , $ internalColumns );
379+ $ missingColumns = \array_diff ($ expectedColumns , $ headers );
360380
361381 if (! empty ($ missingColumns ) || ! empty ($ extraColumns )) {
362382 $ messages = [];
363383
364384 if (! empty ($ missingColumns )) {
365- $ label = count ($ missingColumns ) === 1 ? 'Missing column ' : 'Missing columns ' ;
366- $ messages [] = "{ $ label} : ' " .implode ("', ' " , $ missingColumns )."' " ;
385+ $ label = count ($ missingColumns ) === 1 ? 'Missing attribute ' : 'Missing attributes ' ;
386+ $ messages [] = "$ label: ' " .implode ("', ' " , $ missingColumns )."' " ;
367387 }
368388
369389 if (! empty ($ extraColumns )) {
370- $ label = count ($ extraColumns ) === 1 ? 'Unexpected column ' : 'Unexpected columns ' ;
371- $ messages [] = "{ $ label} : ' " .implode ("', ' " , $ extraColumns )."' " ;
390+ $ label = count ($ extraColumns ) === 1 ? 'Unexpected attribute ' : 'Unexpected attributes ' ;
391+ $ messages [] = "$ label: ' " .implode ("', ' " , $ extraColumns )."' " ;
372392 }
373393
374394 throw new \Exception ('CSV header mismatch. ' .implode (' | ' , $ messages ));
0 commit comments