Skip to content

Commit 1f73e2d

Browse files
authored
Merge pull request #113 from php-openapi/faker-stub-optional-nullsafe
Fix FakerStubResolver optional() wrapping: nullsafe chain with ?? exa…
2 parents 47ab080 + d93b091 commit 1f73e2d

74 files changed

Lines changed: 458 additions & 376 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
23+
php-versions: ['8.0', '8.1', '8.2', '8.3']
2424

2525
steps:
2626
- uses: actions/checkout@v3
@@ -60,7 +60,7 @@ jobs:
6060
run: make UID=0 migrate
6161

6262
- name: Check style
63-
if: "!contains(matrix.php-versions, '8.')"
63+
if: "matrix.php-versions == '8.0'"
6464
run: make check-style-from-host
6565

6666
- name: Run tests

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"source": "https://github.com/cebe/yii2-openapi"
1919
},
2020
"require": {
21-
"php": "^7.4 || ^8.0",
21+
"php": "^8.0",
2222
"cebe/php-openapi": "^1.7.0",
2323
"yiisoft/yii2": "~2.0.48",
2424
"yiisoft/yii2-gii": "~2.0.0 | ~2.1.0 | ~2.2.0| ~2.3.0",

src/lib/FakerStubResolver.php

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,37 @@ public function resolve(): ?string
120120
return null;
121121
}
122122

123-
if (!$this->property->hasAttr('example') ||
123+
// No optional wrapping for required/non-nullable fields without an example (always generate a real value),
124+
// or for unique-items fields (optional fallback would break uniqueness).
125+
if (
126+
(($this->attribute->isRequired() || $this->attribute->nullable === false) && !$this->property->hasAttr('example')) ||
124127
$this->property->getAttr('uniqueItems')
125128
) {
129+
if ($this->attribute->phpType === 'string' && $this->attribute->size) {
130+
return 'substr(' . $result . ', 0, '.$this->attribute->size.')';
131+
}
126132
return $result;
127133
}
128134

129135
$example = $this->property->getAttr('example');
130136
$example = VarExporter::export($example);
131137
$example = preg_replace('/\n/', "\n ", $example);
132-
return str_replace('$faker->', '$faker->optional(0.92, ' . $example . ')->', $result);
138+
139+
/**
140+
* $example must be the exact value that goes into the DB column, e.g. '2020-03-14 21:42:17'
141+
* for a datetime column — not a DateTime object or ISO string with timezone offset.
142+
* optional() without a default returns null on miss; all -> are made nullsafe so the whole
143+
* chain collapses to null, then ?? $example inserts the ready-to-store fallback value.
144+
*
145+
* Negative lookbehind prevents turning an existing ?-> into ??->
146+
*/
147+
$wrapped = str_replace('$faker?->', '$faker->optional(0.92)->', preg_replace('/(?<!\?)->/', '?->', $result));
148+
149+
if ($this->attribute->phpType === 'string' && $this->attribute->size) {
150+
return 'is_string($s = ' . $wrapped . ') ? substr($s, 0, '.$this->attribute->size.') : ' . $example;
151+
}
152+
153+
return $wrapped . ' ?? ' . $example;
133154
}
134155

135156
private function fakeForString(): ?string
@@ -161,8 +182,7 @@ private function fakeForString(): ?string
161182
return '$faker->title';
162183
}
163184
if ($this->attribute->primary || $this->attribute->isReference()) {
164-
$size = $this->attribute->size ?? 255;
165-
return 'substr($uniqueFaker->sha256, 0, ' . $size . ')';
185+
return '$uniqueFaker->sha256';
166186
}
167187

168188
$patterns = [
@@ -197,22 +217,19 @@ private function fakeForString(): ?string
197217
'~(url|site|website|href)~i' => '$faker->url',
198218
'~(username|login)~i' => '$faker->userName',
199219
];
200-
$size = $this->attribute->size > 0 ? $this->attribute->size : null;
201220
foreach ($patterns as $pattern => $fake) {
202221
if (preg_match($pattern, $this->attribute->columnName)) {
203-
if ($size) {
204-
return 'substr(' . $fake . ', 0, ' . $size . ')';
205-
}
206222
return $fake;
207223
}
208224
}
209225

226+
$size = $this->attribute->size > 0 ? $this->attribute->size : null;
210227
if ($size) {
211228
$method = 'text';
212229
if ($size < 5) {
213230
$method = 'word';
214231
}
215-
return 'substr($faker->' . $method . '(' . $size . '), 0, ' . $size . ')';
232+
return '$faker->' . $method . '(' . $size . ')';
216233
}
217234
return '$faker->sentence';
218235
}
@@ -389,10 +406,8 @@ private function reindentArrayMapForObject(string $code, int $depth): string
389406
}
390407
[$body, $count] = [$m[1], $m[2]];
391408

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);
409+
// wrapInArray shifts nested array_map bodies by 4 spaces, so all bodies now start at 12 spaces.
410+
$shift = strlen($bodyIndent) - 12;
396411
if ($shift > 0) {
397412
$body = preg_replace('/\n/', "\n" . str_repeat(' ', $shift), $body);
398413
}
@@ -443,8 +458,14 @@ public function handleOneOf(SpecObjectInterface $items, int $count): string
443458
public function wrapInArray(string $aFaker, bool $uniqueItems, int $count, bool $oneOf = false): string
444459
{
445460
$ret = $oneOf ? '' : 'return ';
461+
$inner = $uniqueItems ? str_replace('$faker->', '$uniqueFaker->', $aFaker) : $aFaker;
462+
// Only shift when the inner value is itself a nested array_map;
463+
// handleOneOf and fakeForObject results already carry correct indentation
464+
if (str_starts_with($inner, 'array_map(') && str_contains($inner, PHP_EOL)) {
465+
$inner = str_replace(PHP_EOL, PHP_EOL . ' ', $inner);
466+
}
446467
return 'array_map(function () use ($faker, $uniqueFaker) {
447-
' . $ret . ($uniqueItems ? str_replace('$faker->', '$uniqueFaker->', $aFaker) : $aFaker) . ';
468+
' . $ret . $inner . ';
448469
}, range(1, ' . $count . '))';
449470
}
450471

src/lib/migrations/BaseMigrationBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ protected function buildRelations():void
380380
$this->migration->dependencies[] = $refTable;
381381
}
382382
}
383+
ksort($existedRelations);
383384
foreach ($existedRelations as $fkName => $relation) {
384385
['fkCol' => $fkCol, 'refCol' => $refCol, 'refTable' => $refTable] = $relation;
385386
$this->migration
@@ -414,6 +415,7 @@ protected function buildRelationsForJunction(ManyToManyRelation $relation):void
414415
$this->migration->dependencies[] = $refTable;
415416
}
416417
}
418+
ksort($existedRelations);
417419
foreach ($existedRelations as $fkName => $rel) {
418420
['fkCol' => $fkCol, 'refCol' => $refCol, 'refTable' => $refTable] = $rel;
419421
$this->migration

tests/fixtures/blog.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
->setDefault('reader')
2525
->setFakerStub('$faker->randomElement([\'admin\', \'editor\', \'reader\'])'),
2626
'flags' => (new Attribute('flags', ['phpType'=>'int', 'dbType'=>'integer']))->setDefault(0)->setFakerStub
27-
('$faker->numberBetween(0, 1000000)'),
27+
('$faker->optional(0.92)->numberBetween(0, 1000000) ?? null'),
2828
'created_at' => (new Attribute('created_at', ['phpType' => 'string', 'dbType' => 'datetime']))
29-
->setDefault(new \yii\db\Expression('(CURRENT_TIMESTAMP)'))->setFakerStub('$faker->dateTimeThisYear(\'now\', \'UTC\')->format(\'Y-m-d H:i:s\')'),
29+
->setDefault(new \yii\db\Expression('(CURRENT_TIMESTAMP)'))->setFakerStub('$faker->optional(0.92)->dateTimeThisYear(\'now\', \'UTC\')?->format(\'Y-m-d H:i:s\') ?? null'),
3030
],
3131
'relations' => [],
3232
'indexes' => [
@@ -66,7 +66,7 @@
6666
'title' => (new Attribute('title', ['phpType' => 'string', 'dbType' => 'string']))
6767
->setRequired()->setSize(255)->setFakerStub('substr($faker->sentence, 0, 255)'),
6868
'slug' => (new Attribute('slug', ['phpType' => 'string', 'dbType' => 'string']))
69-
->setSize(200)->setLimits(null, null, 1)->setFakerStub('substr($uniqueFaker->slug, 0, 200)'),
69+
->setSize(200)->setLimits(null, null, 1)->setFakerStub('is_string($s = $uniqueFaker?->slug) ? substr($s, 0, 200) : null'),
7070
'active' => (new Attribute('active', ['phpType' => 'bool', 'dbType' => 'boolean']))
7171
->setRequired()->setDefault(false)->setFakerStub('$faker->boolean'),
7272
'category' => (new Attribute('category', ['phpType' => 'int', 'dbType' => 'integer']))
@@ -75,7 +75,7 @@
7575
->setDescription('Category of posts')
7676
->setFakerStub('$faker->randomElement(\app\models\Category::find()->select("id")->column())'),
7777
'created_at' => (new Attribute('created_at', ['phpType' => 'string', 'dbType' => 'date']))
78-
->setFakerStub('$faker->dateTimeThisCentury->format(\'Y-m-d\')'),
78+
->setFakerStub('$faker->optional(0.92)->dateTimeThisCentury?->format(\'Y-m-d\') ?? null'),
7979
'created_by' => (new Attribute('created_by', ['phpType' => 'int', 'dbType' => 'integer']))
8080
->asReference('User')
8181
->setDescription('The User')

tests/specs/blog.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,17 @@ components:
236236
str_date:
237237
type: string
238238
format: date
239+
str_date_ex:
240+
type: string
241+
format: date
242+
example: '2020-03-14'
239243
str_datetime:
240244
type: string
241245
format: date-time
246+
str_datetime_ex:
247+
type: string
248+
format: date-time
249+
example: '2020-03-14 21:42:17'
242250
str_country:
243251
type: string
244252
Error:

tests/specs/blog/migrations/m200000_000003_create_table_fakerable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public function up()
2121
'str_text' => $this->text()->null(),
2222
'str_varchar' => $this->string(100)->null()->defaultValue(null),
2323
'str_date' => $this->date()->null()->defaultValue(null),
24+
'str_date_ex' => $this->date()->null()->defaultValue(null),
2425
'str_datetime' => $this->timestamp()->null()->defaultValue(null),
26+
'str_datetime_ex' => $this->timestamp()->null()->defaultValue(null),
2527
'str_country' => $this->text()->null(),
2628
]);
2729
}

tests/specs/blog/migrations_maria_db/m200000_000003_create_table_fakerable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public function up()
2121
'str_text' => $this->text()->null()->defaultValue(null),
2222
'str_varchar' => $this->string(100)->null()->defaultValue(null),
2323
'str_date' => $this->date()->null()->defaultValue(null),
24+
'str_date_ex' => $this->date()->null()->defaultValue(null),
2425
'str_datetime' => $this->timestamp()->null()->defaultValue(null),
26+
'str_datetime_ex' => $this->timestamp()->null()->defaultValue(null),
2527
'str_country' => $this->text()->null()->defaultValue(null),
2628
]);
2729
}

tests/specs/blog/migrations_mysql_db/m200000_000003_create_table_fakerable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public function up()
2121
'str_text' => $this->text()->null(),
2222
'str_varchar' => $this->string(100)->null()->defaultValue(null),
2323
'str_date' => $this->date()->null()->defaultValue(null),
24+
'str_date_ex' => $this->date()->null()->defaultValue(null),
2425
'str_datetime' => $this->timestamp()->null()->defaultValue(null),
26+
'str_datetime_ex' => $this->timestamp()->null()->defaultValue(null),
2527
'str_country' => $this->text()->null(),
2628
]);
2729
}

tests/specs/blog/migrations_pgsql_db/m200000_000003_create_table_fakerable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public function safeUp()
2121
'str_text' => $this->text()->null()->defaultValue(null),
2222
'str_varchar' => $this->string(100)->null()->defaultValue(null),
2323
'str_date' => $this->date()->null()->defaultValue(null),
24+
'str_date_ex' => $this->date()->null()->defaultValue(null),
2425
'str_datetime' => $this->timestamp()->null()->defaultValue(null),
26+
'str_datetime_ex' => $this->timestamp()->null()->defaultValue(null),
2527
'str_country' => $this->text()->null()->defaultValue(null),
2628
]);
2729
}

0 commit comments

Comments
 (0)