Skip to content

Commit 47ab080

Browse files
authored
Merge pull request #112 from php-openapi/nonDbFakerStub
nonDbFakerStub on items with ref
2 parents 1f16eb6 + 206cbe0 commit 47ab080

3 files changed

Lines changed: 63 additions & 83 deletions

File tree

src/lib/FakerStubResolver.php

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/lib/openapi/PropertySchema.php

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -130,26 +130,20 @@ public function __construct(SpecObjectInterface $property, string $name, Compone
130130
}
131131
}
132132

133-
if (
134-
($onUpdate !== null || $onDelete !== null) &&
135-
($reference instanceof Reference)
136-
) {
137-
$this->onUpdateFkConstraint = $onUpdate;
138-
$this->onDeleteFkConstraint = $onDelete;
139-
$this->property = $reference;
140-
$property = $this->property;
141-
} elseif (
142-
($fkColName !== null) &&
143-
($reference instanceof Reference)
144-
) {
145-
$this->fkColName = $fkColName;
146-
$this->property = $reference;
147-
$property = $this->property;
148-
} elseif ($xFaker !== null && $reference instanceof Reference) {
149-
$this->xFaker = $xFaker;
150-
$this->property = $reference;
151-
$property = $this->property;
152-
} elseif ($xDbTypeFalse && $reference instanceof Reference) {
133+
if ($reference instanceof Reference) {
134+
if ($onUpdate !== null) {
135+
$this->onUpdateFkConstraint = $onUpdate;
136+
}
137+
if ($onDelete !== null) {
138+
$this->onDeleteFkConstraint = $onDelete;
139+
}
140+
if ($fkColName !== null) {
141+
$this->fkColName = $fkColName;
142+
}
143+
if ($xFaker !== null) {
144+
$this->xFaker = $xFaker;
145+
}
146+
// covers: fk constraints, fkColName, xFaker, xDbTypeFalse, and plain allOf[$ref]
153147
$this->property = $reference;
154148
$property = $this->property;
155149
}

tests/specs/issue_fix/20_consider_openapi_spec_examples_in_faker_code_generation/mysql/models/PetFaker.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,18 @@ public function generateModel($attributes = [])
7878
'id' => $uniqueFaker->numberBetween(0, 1000000),
7979
'name' => $faker->sentence,
8080
'age' => $faker->numberBetween(0, 200),
81-
'user' => $faker->randomElement(\app\models\User::find()->select("id")->column()),
81+
'user_id' => $faker->randomElement(\app\models\User::find()->select("id")->column()),
8282
'user_2' => array_map(function () use ($faker, $uniqueFaker) {
8383
return (new UserFaker)->generateModel()->attributes;
84-
},
85-
range(1, 4)),
84+
}, range(1, 4)),
8685
'tags' => array_map(function () use ($faker, $uniqueFaker) {
8786
return $uniqueFaker->sentence;
88-
},
89-
range(1, 4)),
87+
}, range(1, 4)),
9088
'arr_arr_int_2' => array_map(function () use ($faker, $uniqueFaker) {
91-
return array_map(
92-
function () use ($faker, $uniqueFaker) {
93-
return $faker->numberBetween(0, 1000000);
94-
},
95-
range(1, 11)
96-
);
97-
},
98-
range(1, 4)),
89+
return array_map(function () use ($faker, $uniqueFaker) {
90+
return $faker->numberBetween(0, 1000000);
91+
}, range(1, 11));
92+
}, range(1, 4)),
9993
'appearance' => [
10094
'height' => $faker->numberBetween(0, 20),
10195
'weight' => $faker->numberBetween(0, 1000000),

0 commit comments

Comments
 (0)