Skip to content

Data except()/only() ignored when saving via Eloquent class cast #1052

Open
@Yi-pixel

Description

@Yi-pixel

✏️ Describe the bug

When I call the except or only methods on a data object using the default Data cast, these filters do not persist.

In the first call to the cast, everything works as expected ✅:

{
  "address": {
    "country": "Greece",
    "city": "Lynchfurt",
    "street": "Vernice Stravenue",
    "postalCode": "58391-3431",
    "phone": "+1 (662) 664-1742",
    "email": "[email protected]",
    "comment": "Et omnis sit et asperiores voluptas. Architecto omnis ut amet velit quia id. Vero sit recusandae porro omnis incidunt molestiae ea."
  }
}
DataEloquentCast.php:78, Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast->set()
HasAttributes.php:1235, Illuminate\Database\Eloquent\Model->setClassCastableAttribute()
HasAttributes.php:1075, Illuminate\Database\Eloquent\Model->setAttribute()
Model.php:589, Illuminate\Database\Eloquent\Model->fill()
Model.php:683, Illuminate\Database\Eloquent\Model->newInstance()
Builder.php:1717, Illuminate\Database\Eloquent\Builder->newModelInstance()
Builder.php:1188, Illuminate\Database\Eloquent\Builder->create()
ForwardsCalls.php:23, Illuminate\Database\Eloquent\Model->forwardCallTo()
Model.php:2496, Illuminate\Database\Eloquent\Model->__call()
Model.php:2512, Illuminate\Database\Eloquent\Model::__callStatic()
web.php:27, Illuminate\Routing\RouteFileRegistrar->{closure:}()

But on the second call, the result is no longer as expected ❌:

{
  "profile": "Maverick Cartwright",
  "address": {
    "country": "Greece",
    "city": "Lynchfurt",
    "street": "Vernice Stravenue",
+   "houseNumber": "09-18-560",
    "postalCode": "58391-3431",
    "phone": "+1 (662) 664-1742",
    "email": "[email protected]",
    "comment": "Et omnis sit et asperiores voluptas. Architecto omnis ut amet velit quia id. Vero sit recusandae porro omnis incidunt molestiae ea."
  }
}
DataEloquentCast.php:78, Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast->set()
+ HasAttributes.php:1899, Illuminate\Database\Eloquent\Model->mergeAttributesFromClassCasts()
+ HasAttributes.php:1881, Illuminate\Database\Eloquent\Model->mergeAttributesFromCachedCasts()
Model.php:1191, Illuminate\Database\Eloquent\Model->save()
Builder.php:1189, Illuminate\Database\Eloquent\Builder->{closure}()
helpers.php:399, tap()
Builder.php:1188, Illuminate\Database\Eloquent\Builder->create()
ForwardsCalls.php:23, Illuminate\Database\Eloquent\Model->forwardCallTo()
Model.php:2496, Illuminate\Database\Eloquent\Model->__call()
Model.php:2512, Illuminate\Database\Eloquent\Model::__callStatic()
web.php:27, Illuminate\Routing\RouteFileRegistrar->{closure:}()

Then again, the third call still behaves incorrectly ❌:

{
  "profile": "Maverick Cartwright",
  "address": {
    "country": "Greece",
    "city": "Lynchfurt",
    "street": "Vernice Stravenue",
+   "houseNumber": "09-18-560",
    "postalCode": "58391-3431",
    "phone": "+1 (662) 664-1742",
    "email": "[email protected]",
    "comment": "Et omnis sit et asperiores voluptas. Architecto omnis ut amet velit quia id. Vero sit recusandae porro omnis incidunt molestiae ea."
  }
}
DataEloquentCast.php:78, Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast->set()
+ HasAttributes.php:1899, Illuminate\Database\Eloquent\Model->mergeAttributesFromClassCasts()
+ HasAttributes.php:1881, Illuminate\Database\Eloquent\Model->mergeAttributesFromCachedCasts()
HasAttributes.php:1950, Illuminate\Database\Eloquent\Model->getAttributes()
HasAttributes.php:2214, Illuminate\Database\Eloquent\Model->getDirty()
HasAttributes.php:2134, Illuminate\Database\Eloquent\Model->isDirty()
HasTimestamps.php:68, Illuminate\Database\Eloquent\Model->updateTimestamps()
Model.php:1366, Illuminate\Database\Eloquent\Model->performInsert()
Model.php:1214, Illuminate\Database\Eloquent\Model->save()
Builder.php:1189, Illuminate\Database\Eloquent\Builder->{closure}()
helpers.php:399, tap()
Builder.php:1188, Illuminate\Database\Eloquent\Builder->create()
ForwardsCalls.php:23, Illuminate\Database\Eloquent\Model->forwardCallTo()
Model.php:2496, Illuminate\Database\Eloquent\Model->__call()
Model.php:2512, Illuminate\Database\Eloquent\Model->__callStatic()
web.php:27, Illuminate\Routing\RouteFileRegistrar->{closure:}()

↪️ To Reproduce

UserProfileData.php

class UserProfileData extends Data
{
    public function __construct(
        public readonly string $profile,
        public readonly AddressData $address,
    )
    {
        $this->address->except('houseNumber'); // <-- here
    }
}

We are trying to exclude address.houseNumber here.

AddressData.php

class AddressData extends Data
{
    public function __construct(
        public readonly string $country,
        public readonly string $city,
        public readonly string $street,
        public readonly string $houseNumber,
        public readonly string $postalCode,
        public readonly string $phone,
        public readonly string $email,
        public readonly string $comment,
    ) {}
}

App\Models\User.php

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
+       'profile',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
+           'profile' => UserProfileData::class, // <-- here
        ];
    }
}

routes/web.php

Route::get('/', function () {
    $profile = \App\Data\UserProfileData::from([
        'profile' => fake()->name,
        'address' => [
            'country' => fake()->country,
            'city' => fake()->city,
            'street' => fake()->streetName,
            'houseNumber' => fake()->numerify('##-##-###'),
            'postalCode' => fake()->postcode,
            'phone' => fake()->phoneNumber,
            'email' => fake()->email,
            'comment' => fake()->text,
        ]
    ]);

    $user_data = [
        'email' => fake()->email,
        'name' => fake()->name,
        'password' => 123456,
    ];

    $user = \App\Models\User::create($user_data + ['profile' => $profile]);

    dump($user->toArray()); // Not Expected
    dump($user->fresh()->toArray()); // Expected
});

Migration (SQLite)

Schema::table('users', function (Blueprint $table) {
    $table->json('profile')->default('{}'); // using SQLite
});

✅ Expected Behavior

The inserted data should exclude address.houseNumber:

{
  "profile": "Miss Alysha Metz",
  "address": {
    "country": "Wallis and Futuna",
    "city": "New Juniusview",
    "street": "Breitenberg Flats",
    "postalCode": "47876",
    "phone": "1-458-927-1713",
    "email": "[email protected]",
    "comment": "Ex beatae minus ducimus animi. Odit ullam voluptatibus aliquam ducimus..."
  }
}

❌ Actual Behavior

Instead, houseNumber is still persisted:

{
  "profile": "Miss Alysha Metz",
  "address": {
    "country": "Wallis and Futuna",
    "city": "New Juniusview",
    "street": "Breitenberg Flats",
+   "houseNumber": "29-92-219",
    "postalCode": "47876",
    "phone": "1-458-927-1713",
    "email": "[email protected]",
    "comment": "Ex beatae minus ducimus animi. Odit ullam voluptatibus aliquam ducimus..."
  }
}

✅ However, when dumping the model (dump($user->fresh()->toArray())), the houseNumber is correctly excluded — indicating it's a problem at cast-to-database level, not in model access.


🖥️ Versions

  • Laravel: 12.19.3
  • Laravel Data: 4.17.0
  • PHP: 8.4.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions