Skip to content

[263]: fix ModelTestState to support custom Eloquent casts over JSON columns - #265

Merged
DenTray merged 47 commits into
masterfrom
263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns
Aug 26, 2026
Merged

[263]: fix ModelTestState to support custom Eloquent casts over JSON columns#265
DenTray merged 47 commits into
masterfrom
263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns

Conversation

@vitgrams

Copy link
Copy Markdown
Contributor

refs: #263

@vitgrams
vitgrams force-pushed the 263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns branch 4 times, most recently from ba44f54 to 1ec5a45 Compare March 24, 2026 11:22
@vitgrams vitgrams self-assigned this Mar 25, 2026
@vitgrams
vitgrams force-pushed the 263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns branch 3 times, most recently from 25e0251 to b57dbb6 Compare March 25, 2026 10:53
@vitgrams vitgrams assigned yburlakov and unassigned vitgrams Mar 26, 2026
@vitgrams
vitgrams requested a review from yburlakov March 26, 2026 08:13
@DenTray
DenTray requested a review from Copilot March 31, 2026 04:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 custom CastsAttributes casts 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.

Comment thread src/Testing/ModelTestState.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Testing/ModelTestState.php Outdated
Comment thread tests/support/Mock/Casts/CurrencyFormattedCast.php Outdated
@yburlakov

Copy link
Copy Markdown
Contributor

@vitgrams please resolve conflicts and copilot's comments.

@yburlakov yburlakov assigned vitgrams and unassigned yburlakov Apr 14, 2026
vitgrams added 10 commits April 21, 2026 14:27
…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
Comment thread src/Testing/ModelTestState.php Outdated
@vitgrams vitgrams self-assigned this Jun 15, 2026
@DenTray

DenTray commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

@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)

@DenTray DenTray removed their assignment Jun 15, 2026
@vitgrams
vitgrams force-pushed the 263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns branch from fa3eb4f to c7e3fc1 Compare June 18, 2026 08:40
@vitgrams
vitgrams force-pushed the 263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns branch from c7e3fc1 to 38c1dbd Compare June 19, 2026 06:20
@vitgrams

Copy link
Copy Markdown
Contributor Author

@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)

@DenTray The solution has been reworked, as discussed.

For native JSON casts (array, json, object, collection) defined as string literals, fixture preparation delegates to parent::prepareChanges, which handles them natively. For class-cast fields, the return type of the cast cannot be determined upfront — the caster may return anything. So instead we decode empirically via json_decode directly on the raw DB value, intentionally bypassing mutator/accessor logic. This gives us the human-readable representation of what's actually stored in the database, which is what fixture assertions care about.

@vitgrams vitgrams assigned DenTray and unassigned vitgrams Jun 19, 2026
Comment thread src/Testing/ModelTestState.php Outdated
{
$matching = array_filter(
array: $model->getCasts(),
callback: fn (string $definition) => $predicate(Str::before($definition, ':')),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' => [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

DenTray and others added 3 commits August 26, 2026 11:58
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>
@DenTray
DenTray merged commit 8169db7 into master Aug 26, 2026
12 checks passed
@DenTray
DenTray deleted the 263-fix-model-test-state-to-support-custom-eloquent-casts-over-json-columns branch August 26, 2026 06:25
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.

4 participants