Update our override of Laravel's attributesToArray()#223
Merged
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
mjauvin
marked this pull request as draft
December 15, 2025 22:47
mjauvin
marked this pull request as ready for review
December 16, 2025 05:56
Storm overrides attributesToArray() so that it can fire the model.beforeGetAttribute / model.getAttribute events and decode $jsonable columns. None of that was covered: stripping both events and the $jsonable loop out of the method left the suite entirely green, so nothing guarded the only reason the override exists. Adds tests for both halves of the contract: - Winter specific behaviour that must survive future re-syncs with Laravel: $jsonable decoding (including invalid JSON, double decoding and mutator precedence), both attribute events, and the ordering guarantee that model.getAttribute observes fully serialized values. - The cast handling this branch fixes, each of which fails against the previous hand-rolled cast loops: pure and backed enum casts, casts implementing SerializesCastableAttributes, Arrayable casts, and the implicit primary key cast that made $model->id and toArray()['id'] disagree on drivers returning columns as strings. Also pins null cast values, mutator-over-cast precedence and appends. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mutator returned a non-JSON value, so json_decode() failed and the value survived whether or not the $jsonable loop skipped mutated attributes. The test passed with the in_array($key, $mutatedAttributes) guard removed, making it a tautology that only restated testMutatorsTakePrecedenceOverCasts. Return valid JSON from the mutator instead, so that dropping the guard decodes the string into an array and fails the assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LukeTowers
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Storm overrides
attributesToArray()so it can fire themodel.beforeGetAttribute/model.getAttributeevents and decode$jsonablecolumns. To do that, the override hand-copied Laravel's date / mutator / cast loops inline.That copy was taken from an old version of Laravel and never kept in sync. Laravel has since moved the cast handling into
addDateAttributesToArray()/addMutatedAttributesToArray()/addCastAttributesToArray()and added a whole post-cast serialization step that our copy simply doesn't have. The result is that several documented Laravel cast behaviours are silently broken — or outright fatal — on Winter models.This PR keeps the Winter-specific bits (both events,
$jsonable,$appends, and the existing ordering) and delegates the rest to the base model, deleting ~46 lines of stale duplication.What this actually fixes
Verified against a probe model exercising every cast type, before and after:
1. Pure (non-backed) enum casts crash
toJson()toArray()currently leaves the enum instance in the array, so encoding blows up:Laravel's
getStorableEnumValue()handles this (->valuefor backed,->namefor pure). After:"status": "Live".2.
SerializesCastableAttributes::serialize()is never calledA custom cast implementing this interface is a documented Laravel contract, and we ignore it — the raw value object leaks into
toArray()and JSON output becomes whatever the object's public properties happen to be ({"cents":1250}instead of the intended"SERIALIZED:1250").3.
date:<format>/datetime:<format>formats are silently ignoredCurrently emits the full ISO-8601 string in
toArray()/toJson()— the requested format is dropped without warning. After:2025/12/31.4.
AsCollection/AsArrayObject/AsEncryptedCollectionleak objectstoArray()returns aCollection/ArrayObjectnested inside the array, so the method doesn't honour its own contract of returning a plain array. Anything doingarray_walk_recursive(), strict comparison, or plain array traversal over the result hits an object. Laravel'sArrayablecheck flattens these.5.
toArray()['id']disagrees with$model->idgetAttribute()already applies the implicit primary-key cast viagetCasts(), but our loop iterated the raw$this->castsproperty, which doesn't include it. On any driver/config that returns column values as strings, that means:Two different types for the same attribute, which is a reliable source of strict-equality bugs in front-end code consuming the JSON.
6. Maintenance
Every future Laravel cast feature currently requires a manual re-port into our override, and the omissions above are exactly what happens when that doesn't occur. Delegating to the base model means we inherit them going forward.
Compatibility notes
SerializesCastableAttributes, custom date formats, andidon string-returning drivers).toArray()value types do change where a cast is declared:datetime/datecasts now yield the serialized string rather than anArgoninstance,AsCollectionyields an array rather than aCollection, and enum casts yield the scalar. This matches upstream Laravel. Code doing$model->toArray()['some_datetime_cast']->format(...)will need updating — worth an upgrade note.created_at/updated_atare covered bygetDates()and were already serialized to strings before this change.$castssee no change at all.Known edge case
When a column appears in both
getDates()and a custom-format cast,addDateAttributesToArray()serializes it to UTC ISO-8601 first, andaddCastAttributesToArray()then re-parses and formats that UTC value — so the custom format renders in UTC rather than the app timezone. This is inherited from Laravel rather than introduced here, but it's why the new test sets$timestamps = falsein order to usecreated_at/updated_atin$casts.Testing
testAddDatesCasts()coveringdate:<format>,datetime:<format>and baredatecasts.ArrayFileTest/ConfigWriterTestformatting failures ondevelopare unrelated).$castsoutside the model stub, so there is no internal exposure.Summary by CodeRabbit
Refactor
Tests
attributesToArray()behavior, covering JSON decoding rules, correct precedence between mutators and casts, enum/cast serialization, arrayable casts, primary-key casting, null preservation, and appended attributes.