Skip to content

Fix FakerStubResolver optional() wrapping: nullsafe chain with ?? exa…#113

Merged
cebe merged 3 commits into
masterfrom
faker-stub-optional-nullsafe
May 29, 2026
Merged

Fix FakerStubResolver optional() wrapping: nullsafe chain with ?? exa…#113
cebe merged 3 commits into
masterfrom
faker-stub-optional-nullsafe

Conversation

@siggi-k
Copy link
Copy Markdown

@siggi-k siggi-k commented May 28, 2026

Fix: optional() wrapping for faker stubs — nullsafe chain + ?? fallback

1. The bug: optional(weight, default)->dateTimeX()->format() fatal error

$faker->optional($weight, $default) is a proxy. On a miss (8 %), it returns $default
as the result of the next proxied method call — without actually calling it.

The previous strategy embedded $example as the default:

// generated (broken):
$faker->optional(0.92, '2020-03-14 21:42:17')->dateTimeThisCentury->format('Y-m-d')

On the 8 % miss, dateTimeThisCentury returns the string '2020-03-14 21:42:17',
then ->format() is called on it → Call to a member function format() on string.

2. The fix: nullsafe chain + ?? $example

optional() without a default returns null on a miss. All subsequent calls are made
nullsafe (?->) so the chain collapses to null, then ?? $example provides the fallback.

Two str_replace passes turn the raw stub into the wrapped form:

  1. all ->?-> (makes the whole chain nullsafe)
  2. $faker?->$faker->optional(0.92)-> (restores a normal -> to $faker, inserts optional)

Generated output — before / after:

// datetime column (e.g. updatedAt, nullable, no example in spec):
// before — no wrapping at all (too strict skip condition, see §3):
$model->updatedAt = $faker->dateTimeThisYear('now', 'UTC')->format('Y-m-d H:i:s');

// after:
$model->updatedAt = $faker->optional(0.92)?->dateTimeThisYear('now', 'UTC')?->format('Y-m-d H:i:s') ?? null;

// date column with example:
// before (broken — format() called on string on miss):
$model->archivedAt = $faker->optional(0.92, '2020-03-14')->dateTimeThisCentury->format('Y-m-d');

// after:
$model->archivedAt = $faker->optional(0.92)?->dateTimeThisCentury?->format('Y-m-d') ?? '2020-03-14';

// simple string column with example:
// before:
$model->name = $faker->optional(0.92, 'Acme Corp')->company;

// after:
$model->name = $faker->optional(0.92)?->company ?? 'Acme Corp';

3. Condition for skipping optional wrapping

Before: wrapping was skipped for any field without an example — including nullable ones.
After: wrapping is skipped only when a real value is mandatory:

if (
    (($this->attribute->isRequired() || $this->attribute->nullable === false) && !$this->property->hasAttr('example')) ||
    $this->property->getAttr('uniqueItems')
) {
    return $result; // no optional wrapping
}

Nullable fields without an example now generate null 8 % of the time (semantically correct).
uniqueItems fields are excluded because an optional fallback would break uniqueness.

@cebe cebe merged commit 1f73e2d into master May 29, 2026
4 checks passed
@cebe cebe deleted the faker-stub-optional-nullsafe branch May 29, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants