Skip to content
LaravelBlog

framework #61823: [13.x] Fix `forceCreate()` ignoring `withAttributes()`

LaravelBlogBot
LaravelBlogBot

Contributed by xurshudyan

Part of the framework v13.35.0 release

forceCreate() now honors the attribute defaults registered via withAttributes(), closing an inconsistency with create(). Prior to this fix, calling forceCreate() on a relation or query that had withAttributes() applied would silently drop those defaults, persisting rows that violated the contract the relation declared. PR #61823 merges the pending attributes inside forceCreate() on the Eloquent builder and on HasOneOrMany and MorphOneOrMany relations, using the same approach BelongsToMany::create() already used. It ships as a Fix.

What changed

Three small, surgical edits were made:

  • Eloquent\Builder::forceCreate() now merges $this->pendingAttributes into the attributes passed to create() inside the existing unguarded callback:
    return $this->newModelInstance()->create(
        array_merge($this->pendingAttributes, $attributes)
    );
    
  • HasOneOrMany::forceCreate() merges the relation query's pending attributes before assigning the foreign key.
  • MorphOneOrMany::forceCreate() does the same, before assigning both the foreign key and the morph type.

The merge order is deliberate: pending attributes come first in array_merge(), so explicitly passed attributes still win over the defaults — exactly how create() and BelongsToMany::create() already behave.

Why it matters

withAttributes() exists to enforce invariants: every record created through this relation or query should carry certain values. The classic case is a scoped relation:

// featuredPosts() is hasMany(Post::class)->withAttributes(['featured' => true])
$user->featuredPosts()->create([...]);      // featured = true ✅
$user->featuredPosts()->forceCreate([...]); // featured = false ❌ before this fix

That second line is a data-integrity bug waiting to happen. Teams often reach for forceCreate() when mass-assignment protection is on, or when creating models with many attributes — and without realizing it, they bypassed the defaults and produced rows like unfeatured posts in a featured feed, inactive records in an "active" scope, or rows missing a required type discriminator. After the fix, both creation paths through the relation honor the same defaults, so the relation's declared contract holds no matter which method you call.

Before and after

Beyond relations, the builder-level behavior is now consistent too. This pattern from the test suite previously persisted is_admin as the column default and dropped the enum:

WithAttributesModel::query()
    ->withAttributes([
        'is_admin' => 1,
        'type' => WithAttributesEnum::internal,
    ])
    ->forceCreate(['first_name' => 'FIRST', 'last_name' => 'LAST']);

After upgrading, the created row has is_admin = true and type = internal, matching what create() would have produced with the same setup.

Overriding still works, since explicit arguments take precedence:

$user->featuredPosts()->forceCreate(['featured' => false]); // featured = false — explicit wins

And the relation's own bookkeeping stays authoritative: the foreign key and morph type are applied after the merge, so withAttributes() can never accidentally overwrite them.

Upgrade impact

Upgrading from v13.34.0 requires no code changes, no new methods, and no migrations — but you should review call sites for a behavioral shift:

  • New rows will now include the defaults. Any forceCreate() call made through a relation or query with withAttributes() will persist those attributes. If you were (perhaps unknowingly) relying on forceCreate() writing bare rows, audit those spots.
  • Explicit values still override defaults. The merge order guarantees your directly passed attributes win, so nothing is forced on you.
  • forceCreate() semantics are otherwise unchanged. It still runs unguarded (bypassing mass-assignment protection) — only the defaults handling changed.
  • Existing data is untouched. Only rows created after the upgrade are affected; there's no backfill or migration concern.

The new test coverage — testHasManyForceCreateAddsAttributes, testMorphManyForceCreateAddsAttributes, and testForceCreateAddsAttributesViaDb — locks in the corrected behavior for relations and the builder alike.

Takeaways

  • forceCreate() now applies withAttributes() defaults on the Eloquent builder, HasOneOrMany, and MorphOneOrMany, matching create() and BelongsToMany::create().
  • Explicitly passed attributes still override the defaults; foreign keys and morph types are set after the merge and remain authoritative.
  • Expect rows created via forceCreate() through attribute-defaulted relations to now include those defaults — audit any code that depended on the old, inconsistent behavior.
  • No API, schema, or configuration changes; this is a drop-in behavioral fix for data integrity.

Sources

More from this release

Related Articles