[263]: fix ModelTestState to support custom Eloquent casts over JSON columns - #265
Conversation
ba44f54 to
1ec5a45
Compare
25e0251 to
b57dbb6
Compare
There was a problem hiding this comment.
Pull request overview
Updates RonasIT\Support\Testing\ModelTestState to correctly handle Eloquent models that use custom CastsAttributes casts on JSON-backed columns, aligning fixture assertions with the casted (model-level) values rather than raw JSON strings.
Changes:
- Split native JSON casts (
array/json/object/collection) from customCastsAttributescasts and process them separately. - Apply custom casts by invoking each cast class’s
get()method during change preparation. - Expand test coverage with new mock models/casts and updated fixtures to validate native JSON casts vs custom casts behavior.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/support/Traits/TableTestStateMockTrait.php | Allows mocking datasets for arbitrary table names to support new model/table scenarios in tests. |
| tests/support/Mock/Models/TestModelWithOnlyCustomCast.php | Adds a model containing only a custom cast field for targeted coverage. |
| tests/support/Mock/Models/TestModelWithModelAwareCast.php | Adds a model to exercise casts that depend on other model attributes. |
| tests/support/Mock/Models/TestModelWithCasts.php | Adds a model combining native JSON casts and a custom cast field to validate separation/processing. |
| tests/support/Mock/Models/TestModelWithAllNativeJsonCasts.php | Adds a model covering all supported native JSON casts. |
| tests/support/Mock/Models/TestModel.php | Removes JSON/cast fields from the generic test model to decouple unrelated tests from ModelTestState casting behavior. |
| tests/support/Mock/Casts/ModelAwareCast.php | Introduces a cast that reads another attribute (currency) to validate model-aware cast behavior. |
| tests/support/Mock/Casts/JSONCustomCast.php | Strengthens the custom JSON cast implementation and typing; maps raw JSON into a structured array. |
| tests/ModelTestStateTest.php | Reworks and expands tests to cover native JSON casts, custom casts, and model-aware casts with updated fixtures. |
| tests/fixtures/ModelTestStateTest/initialization/origin_records.json | Updates initialization fixtures to reflect new custom-cast JSON shapes. |
| tests/fixtures/ModelTestStateTest/initialization/dataset.json | Updates initialization fixtures to reflect new custom-cast JSON shapes. |
| tests/fixtures/ModelTestStateTest/get_without_changes/dataset.json | Updates “no changes” dataset fixture to reflect new custom-cast JSON shapes. |
| tests/fixtures/ModelTestStateTest/db_changes/test_models/assertion_fixture.json | Updates assertions to expect casted output for custom cast fields. |
| tests/fixtures/ModelTestStateTest/db_changes/test_model_with_only_custom_casts/assertion_fixture.json | Adds assertion fixture for the “only custom cast” model scenario. |
| tests/fixtures/ModelTestStateTest/db_changes/test_model_with_model_aware_casts/assertion_fixture.json | Adds assertion fixture for the model-aware cast scenario. |
| tests/fixtures/ModelTestStateTest/db_changes/test_model_with_all_native_json_casts/assertion_fixture.json | Adds assertion fixture for the “all native JSON casts” scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture/initial_dataset.json | Updates baseline dataset for mixed native/custom cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture/changed_dataset.json | Updates changed dataset for mixed native/custom cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_without_casts/initial_dataset.json | Adds dataset fixture for models without casts. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_without_casts/changed_dataset.json | Adds dataset fixture for models without casts. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_only_custom_cast/initial_dataset.json | Adds dataset fixture for only-custom-cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_only_custom_cast/changed_dataset.json | Adds dataset fixture for only-custom-cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_model_aware_cast/initial_dataset.json | Adds dataset fixture for model-aware cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_model_aware_cast/changed_dataset.json | Adds dataset fixture for model-aware cast scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_all_native_json_casts/initial_dataset.json | Adds dataset fixture for all-native-JSON-casts scenario. |
| tests/fixtures/ModelTestStateTest/changes_equals_fixture_with_all_native_json_casts/changed_dataset.json | Adds dataset fixture for all-native-JSON-casts scenario. |
| tests/BaseRequestTest.php | Updates expected orderable fields after removing cast fields from the generic TestModel. |
| src/Testing/ModelTestState.php | Implements native JSON cast detection, tracks custom cast fields, and applies custom casts via get() in prepareChanges(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 28 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@vitgrams please resolve conflicts and copilot's comments. |
…N casts and custom CastsAttributes
- Maintain existing handling for native JSON casts ('array', 'json', 'object', 'collection');
- Identify fields with custom casts on JSON columns and store them separately in a new property;
- Override prepareChanges to decode custom casts via its own get() method.
refs: #263
…->get() method calling when asserting model state changes; refs: #263
…e type error and be able to use model attributes refs: #263
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-eloquent-casts-over-json-columns
|
@vitgrams as discussed, applying all casts/getters/mutators is not the good approach for the ModelTestState as the main responsible of this class is to detect the raw dataset changes. The main idea of #263 was to allow user to see changes in the user friendly format (a php array instead of the huge JSON string). Let's find a way to apply only user-friendly representation casts instead of all model's. Please NOTE that we have #263 which has the similar reason (show binary fields content as normal) |
…s as raw stored values refs: #263
fa3eb4f to
c7e3fc1
Compare
c7e3fc1 to
38c1dbd
Compare
@DenTray The solution has been reworked, as discussed. For native JSON casts ( |
…-eloquent-casts-over-json-columns
| { | ||
| $matching = array_filter( | ||
| array: $model->getCasts(), | ||
| callback: fn (string $definition) => $predicate(Str::before($definition, ':')), |
There was a problem hiding this comment.
getCasts() may return a cast instance, not only a string — Eloquent supports it (HasAttributes::resolveCasterClass handles is_object($castType)). The string type hint then throws a TypeError in the constructor.
Fixed on the branch: the definition is normalized by a new resolveCastType() (is_string($definition) ? Str::before($definition, ':') : $definition::class), plus a cast defined as an instance data set that reproduces the crash without the fix.
| $modelTestState = new ModelTestState(TestModel::class); | ||
| $modelTestState->assertChangesEqualsFixture('assertion_fixture.json'); | ||
| return [ | ||
| 'primitive casts' => [ |
There was a problem hiding this comment.
Dropping the TestModel case removed the only coverage where a native array cast, a CastsAttributes cast and a Castable cast coexist on one model — the exact scenario #263 is about.
Fixed on the branch: mixed native, custom and castable casts data set restored, together with changes_equals_fixture/* and db_changes/test_models/assertion_fixture.json. The fixtures also pin that a non-JSON value under a class cast stays a raw string.
| "name": "name1", | ||
| "json_field": "{\"field1\": \"value\", \"field2\": [2, 3], \"field3\": {\"one\": 1, \"two\": 2}}", | ||
| "castable_field": "{\"field1\": \"value\", \"field2\": [2, 3], \"field3\": {\"one\": 1, \"two\": 2}}", | ||
| "settings": "{\"theme\": \"dark\", \"language\": \"en\", \"notifications_email\": true, \"notifications_sms\": false}", |
There was a problem hiding this comment.
TestModel has no settings column, so this fixture no longer contains any of the columns whose cast resolution the test asserts. It passes only because the dataset is mocked.
Fixed on the branch: json_field / custom_cast_field / castable_field restored here and in origin_records.json.
| "name": "name1", | ||
| "json_field": "{\"field1\": \"value\", \"field2\": [2, 3], \"field3\": {\"one\": 1, \"two\": 2}}", | ||
| "castable_field": "{\"field1\": \"value\", \"field2\": [2, 3], \"field3\": {\"one\": 1, \"two\": 2}}", | ||
| "castable_field": "{\"theme\": \"dark\", \"language\": \"en\", \"notifications_email\": true, \"notifications_sms\": false}", |
There was a problem hiding this comment.
custom_cast_field is gone from this dataset (its class-cast path is no longer exercised), and castable_field holds a UserSettingCast-shaped payload while TestModel casts it with JSONCastable. The content change isn't needed for the assertion.
Fixed on the branch: content restored and the trailing newline added back.
| "created_at": "2018-10-10 10:10:10", | ||
| "updated_at": "2018-10-10 10:10:10" | ||
| } | ||
| ] No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline, and the new dataset fixtures use 4-space indentation while the existing ones in this directory use 2.
Fixed on the branch: reindented to 2 spaces with trailing newlines, same for the custom_json_cast and custom_non_json_cast dataset dirs.
Eloquent allows a cast to be declared as an object instance, so getCasts() may return a non-string definition. The cast resolution closure was typed string and threw a TypeError in the constructor. Definitions are now normalized by resolveCastType() before the predicate is applied. Also restores test coverage and fixture consistency: - bring back the mixed native/custom/castable cast case for TestModel along with changes_equals_fixture/* and db_changes/test_models fixtures - add TestModelWithCastInstance and the cast-instance data set - make initialization and get_without_changes fixtures match TestModel columns again - reindent the new dataset fixtures to 2 spaces and add trailing newlines Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recent Laravel versions added an object branch to ensureCastsAreStringValues(), which requires a cast declared as an instance to implement Stringable and converts it to its class name during model initialization. The mock cast now implements it, so the data set works both on versions that keep the object in getCasts() and on those that stringify it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Also renames its second parameter to $callback and the local variable to $filtered to match the new name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refs: #263