@@ -80,8 +80,8 @@ public function resolve(): ?string
8080 return null ;
8181 }
8282
83- // column name ends with `_id`/FK
84- if (substr ( $ this -> attribute -> columnName , - 3 ) === ' _id ' || !empty ($ this ->attribute ->fkColName )) {
83+ // FK: determined by a $ref / allOf[$ref] — not by column name convention
84+ if (!empty ($ this ->attribute ->reference )) {
8585 $ config = $ this ->config ;
8686 if (!$ config ) {
8787 $ config = new Config ;
@@ -313,7 +313,7 @@ private function fakeForArray(SpecObjectInterface $property, int $count = 4): st
313313 }
314314
315315 if ($ type === 'object ' ) {
316- $ result = $ this ->fakeForObject ($ items );
316+ $ result = $ this ->fakeForObject ($ items, 1 );
317317 if ($ result === '(object) [] ' ) {
318318 return '[] ' ;
319319 }
@@ -339,7 +339,7 @@ private function fakeForArray(SpecObjectInterface $property, int $count = 4): st
339339 * defined properties this is acceptable, as no schema is enforced.
340340 * @internal
341341 */
342- public function fakeForObject (SpecObjectInterface $ items , int $ depth = 1 ): string
342+ public function fakeForObject (SpecObjectInterface $ items , int $ depth = 0 ): string
343343 {
344344 if (!$ items ->properties ) {
345345 return '(object) [] ' ;
@@ -353,14 +353,15 @@ public function fakeForObject(SpecObjectInterface $items, int $depth = 1): strin
353353 /** @var SpecObjectInterface $prop */
354354
355355 if (!$ prop instanceof Reference && ($ prop ->type === 'object ' || !empty ($ prop ->properties ))) {
356+ $ key = $ name ;
356357 $ result = $ this ->fakeForObject ($ prop , $ depth + 1 );
357358 } else {
358- $ result = $ this ->aElementFaker (['items ' => $ prop ->getSerializableData ()], $ name );
359+ [ ' columnName ' => $ key , ' fakerStub ' => $ result] = $ this ->resolveElement (['items ' => $ prop ->getSerializableData ()], $ name );
359360 if (str_starts_with ($ result , 'array_map ' )) {
360361 $ result = $ this ->reindentArrayMapForObject ($ result , $ depth );
361362 }
362363 }
363- $ parts [] = $ indent . '\'' . $ name . '\' => ' . $ result . ', ' ;
364+ $ parts [] = $ indent . '\'' . $ key . '\' => ' . $ result . ', ' ;
364365 }
365366
366367 $ props = '[ ' . PHP_EOL . implode (PHP_EOL , $ parts ) . PHP_EOL . $ closingIndent . '] ' ;
@@ -371,8 +372,11 @@ public function fakeForObject(SpecObjectInterface $items, int $depth = 1): strin
371372 /**
372373 * Re-indents a compact wrapInArray() output string to match the correct depth inside fakeForObject().
373374 * 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().
375+ * property value inside a fakeForObject() output, the indentation must be adjusted.
376+ *
377+ * For a simple body (single return statement): shift = bodyIndent - 12, placing the return at bodyIndent.
378+ * For a nested body (return array_map(...)): shift = bodyIndent - 8, so the inner return lands at
379+ * bodyIndent+4 and the inner closing brace lands at bodyIndent — matching wrapInArray's 12/8 ratio.
376380 */
377381 private function reindentArrayMapForObject (string $ code , int $ depth ): string
378382 {
@@ -385,41 +389,17 @@ private function reindentArrayMapForObject(string $code, int $depth): string
385389 }
386390 [$ body , $ count ] = [$ m [1 ], $ m [2 ]];
387391
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 })) " ;
392+ // For nested array_map: wrapInArray places the inner return at 12 spaces and the inner closing
393+ // brace at 8 spaces. We want the inner return at bodyIndent+4 and the inner brace at bodyIndent,
394+ // so the shift is bodyIndent - 8. For simple (non-nested) bodies the shift is bodyIndent - 12.
395+ $ shift = strlen ($ bodyIndent ) - (str_starts_with ($ body , 'return array_map( ' ) ? 8 : 12 );
396+ if ($ shift > 0 ) {
397+ $ body = preg_replace ('/\n/ ' , "\n" . str_repeat (' ' , $ shift ), $ body );
395398 }
396399
397400 return "array_map(function () use ( \$faker, \$uniqueFaker) { \n"
398401 . $ 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 . ") " ;
402+ . $ closeIndent . "}, range(1, {$ count })) " ;
423403 }
424404
425405 /**
@@ -444,13 +424,10 @@ public function handleOneOf(SpecObjectInterface $items, int $count): string
444424
445425 $ inp = $ aDataType instanceof Reference ? $ aDataType : ['items ' => $ aDataType ->getSerializableData ()];
446426 $ 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 , '[ ' )) {
427+ // Shift all continuation lines by 4 spaces so that multi-line values
428+ // (array_map or object literals from fakeForObject) are indented one level
429+ // deeper than the $dataTypeN assignment (12 → 16 for body, 8 → 12 for closing).
430+ if (str_contains ($ aFaker , PHP_EOL )) {
454431 $ aFaker = str_replace (PHP_EOL , PHP_EOL . ' ' , $ aFaker );
455432 }
456433 if ($ result !== '' ) {
@@ -492,11 +469,22 @@ public function arbitraryArray(): string
492469 * @internal
493470 */
494471 public function aElementFaker ($ data , ?string $ columnName = null ): ?string
472+ {
473+ return $ this ->resolveElement ($ data , $ columnName )['fakerStub ' ];
474+ }
475+
476+ /**
477+ * Resolves the faker stub and the effective column key for a single element.
478+ * For FK properties (direct $ref or allOf[$ref]), the key uses the same '_id' suffix
479+ * logic as Attribute::asReference() — both for real DB columns and JSONB sub-properties.
480+ * @return array
481+ * @example ['columnName' => 'payment_method_id', 'fakerStub' => '$faker->randomElement(...)']
482+ */
483+ private function resolveElement ($ data , ?string $ columnName = null ): array
495484 {
496485 if ($ data instanceof Reference) {
497- $ class = str_replace ('#/components/schemas/ ' , '' , $ data ->getReference ());
498- $ class .= 'Faker ' ;
499- return '(new ' . $ class . ')->generateModel()->attributes ' ;
486+ $ class = str_replace ('#/components/schemas/ ' , '' , $ data ->getReference ()) . 'Faker ' ;
487+ return ['columnName ' => $ columnName ?? 'unknownColumn ' , 'fakerStub ' => '(new ' . $ class . ')->generateModel()->attributes ' ];
500488 }
501489
502490 $ inp = $ data instanceof SpecObjectInterface ? $ data ->getSerializableData () : $ data ;
@@ -523,7 +511,11 @@ public function aElementFaker($data, ?string $columnName = null): ?string
523511 $ schema ->setReferenceContext ($ rc );
524512 }
525513 $ dbModels = (new AttributeResolver ($ compo , $ cs , new JunctionSchemas ([]), $ this ->config ))->resolve ();
514+ $ attr = $ dbModels ->attributes [$ columnName ];
526515
527- return (new static ($ dbModels ->attributes [$ columnName ], $ cs ->getProperty ($ columnName ), $ this ->config ))->resolve ();
516+ return [
517+ 'columnName ' => $ attr ->columnName ,
518+ 'fakerStub ' => (new static ($ attr , $ cs ->getProperty ($ columnName ), $ this ->config ))->resolve (),
519+ ];
528520 }
529521}
0 commit comments