@@ -104,7 +104,13 @@ public function resolve(): ?string
104104 $ result = $ this ->fakeForFloat ($ limits ['min ' ], $ limits ['max ' ]);
105105 } elseif ($ this ->attribute ->phpType === 'array ' ||
106106 substr ($ this ->attribute ->phpType , -2 ) === '[] ' ) {
107- $ result = $ this ->fakeForArray ($ this ->property ->getProperty ());
107+ $ property = $ this ->property ->getProperty ();
108+ if ($ property ->type === 'object ' ) {
109+ // A JSONB/JSON column declared as type:object in the spec has phpType=array but must
110+ // be faked as an object, not as an array.
111+ return $ this ->fakeForObject ($ property );
112+ }
113+ $ result = $ this ->fakeForArray ($ property );
108114 if ($ result !== '$faker->words() ' ) { # example for array will only work with a list/`$faker->words()`
109115 return $ result ;
110116 }
@@ -122,6 +128,7 @@ public function resolve(): ?string
122128
123129 $ example = $ this ->property ->getAttr ('example ' );
124130 $ example = VarExporter::export ($ example );
131+ $ example = preg_replace ('/\n/ ' , "\n " , $ example );
125132 return str_replace ('$faker-> ' , '$faker->optional(0.92, ' . $ example . ')-> ' , $ result );
126133 }
127134
@@ -284,7 +291,8 @@ private function fakeForArray(SpecObjectInterface $property, int $count = 4): st
284291 $ items = $ property ->items ;
285292
286293 if (!$ items ) {
287- return $ this ->arbitraryArray ();
294+ // Required fields cannot use [] — Yii2's isEmpty() treats empty arrays as blank.
295+ return $ this ->attribute ->required ? $ this ->arbitraryArray () : '[] ' ;
288296 }
289297
290298 if ($ items instanceof Reference) {
@@ -299,46 +307,121 @@ private function fakeForArray(SpecObjectInterface $property, int $count = 4): st
299307 if ($ type === null ) {
300308 return $ this ->arbitraryArray ();
301309 }
302- $ aFaker = $ this ->aElementFaker ($ this ->property ->getProperty (), $ this ->attribute ->columnName );
303310 if (in_array ($ type , ['string ' , 'number ' , 'integer ' , 'boolean ' , 'array ' ])) {
311+ $ aFaker = $ this ->aElementFaker ($ this ->property ->getProperty (), $ this ->attribute ->columnName );
304312 return $ this ->wrapInArray ($ aFaker , $ uniqueItems , $ count );
305313 }
306314
307315 if ($ type === 'object ' ) {
308316 $ result = $ this ->fakeForObject ($ items );
317+ if ($ result === '(object) [] ' ) {
318+ return '[] ' ;
319+ }
309320 return $ this ->wrapInArray ($ result , $ uniqueItems , $ count );
310321 }
311322
312323 return '[] ' ;
313324 }
314325
315326 /**
327+ * Generates a PHP array literal string for an OpenAPI object property.
328+ * The output is embedded as PHP code in Faker fixture files, not as JSON.
329+ *
330+ * Flow: Faker assigns a PHP array to the model property → ActiveRecord passes it
331+ * to the DB driver → the driver JSON-encodes it before storing.
332+ * Result in DB:
333+ * PHP [] → json_encode([]) → [] (JSON array)
334+ * PHP ['key' => 'v'] → json_encode(['key' => 'v']) → {"key": "v"} (JSON object)
335+ *
336+ * A non-empty associative array correctly becomes a JSON object in the DB.
337+ * An empty PHP array always becomes [] in the DB, never {} — regardless of whether
338+ * the OpenAPI field is typed as "object" or "array". For test/faker data without
339+ * defined properties this is acceptable, as no schema is enforced.
316340 * @internal
317341 */
318- public function fakeForObject (SpecObjectInterface $ items ): string
342+ public function fakeForObject (SpecObjectInterface $ items, int $ depth = 1 ): string
319343 {
320344 if (!$ items ->properties ) {
321- return $ this -> arbitraryArray () ;
345+ return ' (object) [] ' ;
322346 }
323347
324- $ props = '[ ' . PHP_EOL ;
348+ $ indent = str_repeat (' ' , $ depth + 3 );
349+ $ closingIndent = str_repeat (' ' , $ depth + 2 );
350+ $ parts = [];
325351
326352 foreach ($ items ->properties as $ name => $ prop ) {
327353 /** @var SpecObjectInterface $prop */
328354
329- if (!empty ($ prop ->properties )) { // nested object
330- $ result = $ this ->{ __FUNCTION__ } ($ prop );
355+ if (!$ prop instanceof Reference && ( $ prop -> type === ' object ' || ! empty ($ prop ->properties ))) {
356+ $ result = $ this ->fakeForObject ($ prop, $ depth + 1 );
331357 } else {
332358 $ result = $ this ->aElementFaker (['items ' => $ prop ->getSerializableData ()], $ name );
359+ if (str_starts_with ($ result , 'array_map ' )) {
360+ $ result = $ this ->reindentArrayMapForObject ($ result , $ depth );
361+ }
333362 }
334- $ props .= '\'' . $ name . '\' => ' . $ result . ', ' . PHP_EOL ;
363+ $ parts [] = $ indent . '\'' . $ name . '\' => ' . $ result . ', ' ;
335364 }
336365
337- $ props .= '] ' ;
366+ $ props = ' [ ' . PHP_EOL . implode ( PHP_EOL , $ parts ) . PHP_EOL . $ closingIndent . '] ' ;
338367
339368 return $ props ;
340369 }
341370
371+ /**
372+ * Re-indents a compact wrapInArray() output string to match the correct depth inside fakeForObject().
373+ * wrapInArray() always uses hardcoded 12/8-space indentation; when its result is embedded as a
374+ * property value inside a fakeForObject() output at depth >= 1, the indentation must be adjusted.
375+ * For a nested array_map body the inner call is expanded to multi-line style via expandCompactArrayMap().
376+ */
377+ private function reindentArrayMapForObject (string $ code , int $ depth ): string
378+ {
379+ $ bodyIndent = str_repeat (' ' , $ depth + 4 );
380+ $ closeIndent = str_repeat (' ' , $ depth + 3 );
381+
382+ $ pat = '/^array_map\(function \(\) use \(\$faker, \$uniqueFaker\) \{\n (.*)\n \}, range\(1, (\d+)\)\)$/s ' ;
383+ if (!preg_match ($ pat , $ code , $ m )) {
384+ return $ code ;
385+ }
386+ [$ body , $ count ] = [$ m [1 ], $ m [2 ]];
387+
388+ if (str_starts_with ($ body , 'return array_map( ' )) {
389+ $ inner = substr ($ body , 7 , -1 ); // strip "return " prefix and trailing ";"
390+ $ expanded = $ this ->expandCompactArrayMap ($ inner , $ bodyIndent );
391+ return "array_map(function () use ( \$faker, \$uniqueFaker) { \n"
392+ . $ bodyIndent . "return {$ expanded }; \n"
393+ . $ closeIndent . "}, \n"
394+ . $ closeIndent . "range(1, {$ count })) " ;
395+ }
396+
397+ return "array_map(function () use ( \$faker, \$uniqueFaker) { \n"
398+ . $ bodyIndent . $ body . "\n"
399+ . $ closeIndent . "}, \n"
400+ . $ closeIndent . "range(1, {$ count })) " ;
401+ }
402+
403+ /**
404+ * Expands a compact wrapInArray() string (single-line function + range) into multi-line style,
405+ * using $baseIndent as the reference indentation level for the opening "array_map(" line.
406+ */
407+ private function expandCompactArrayMap (string $ code , string $ baseIndent ): string
408+ {
409+ $ pat = '/^array_map\(function \(\) use \(\$faker, \$uniqueFaker\) \{\n (.*)\n \}, range\(1, (\d+)\)\)$/s ' ;
410+ if (!preg_match ($ pat , $ code , $ m )) {
411+ return $ code ;
412+ }
413+ [$ body , $ count ] = [$ m [1 ], $ m [2 ]];
414+ $ funcIndent = $ baseIndent . ' ' ;
415+ $ innerIndent = $ baseIndent . ' ' ;
416+
417+ return "array_map( \n"
418+ . $ funcIndent . "function () use ( \$faker, \$uniqueFaker) { \n"
419+ . $ innerIndent . $ body . "\n"
420+ . $ funcIndent . "}, \n"
421+ . $ funcIndent . "range(1, {$ count }) \n"
422+ . $ baseIndent . ") " ;
423+ }
424+
342425 /**
343426 * This method must be only used incase of array
344427 * @param SpecObjectInterface $items
@@ -355,15 +438,28 @@ public function fakeForObject(SpecObjectInterface $items): string
355438 public function handleOneOf (SpecObjectInterface $ items , int $ count ): string
356439 {
357440 $ result = '' ;
441+ $ indent = str_repeat (' ' , 12 );
358442 foreach ($ items ->oneOf as $ key => $ aDataType ) {
359443 /** @var Schema|Reference $aDataType */
360444
361445 $ inp = $ aDataType instanceof Reference ? $ aDataType : ['items ' => $ aDataType ->getSerializableData ()];
362446 $ aFaker = $ this ->aElementFaker ($ inp , $ this ->attribute ->columnName );
447+ /**
448+ * Each $dataTypeN gets its own line (12-space indent = wrapInArray body level).
449+ * wrapInArray output (array_map) gets +4 spaces on continuation lines (12→16, 8→12).
450+ * fakeForObject output (starts with "[") is left as-is — depth=1 already gives 16/12.
451+ * return goes on its own line.
452+ */
453+ if (str_contains ($ aFaker , PHP_EOL ) && !str_starts_with ($ aFaker , '[ ' )) {
454+ $ aFaker = str_replace (PHP_EOL , PHP_EOL . ' ' , $ aFaker );
455+ }
456+ if ($ result !== '' ) {
457+ $ result .= PHP_EOL . $ indent ;
458+ }
363459 $ result .= '$dataType ' . $ key . ' = ' . $ aFaker . '; ' ;
364460 }
365461 $ ct = count ($ items ->oneOf ) - 1 ;
366- $ result .= 'return ${"dataType".rand(0, ' . $ ct . ')} ' ;
462+ $ result .= PHP_EOL . $ indent . 'return ${"dataType".rand(0, ' . $ ct . ')} ' ;
367463 return $ result ;
368464 }
369465
0 commit comments