fix(state): support output DTOs on write operations with ObjectMapper - #8497
Open
dylan-rumble wants to merge 1 commit into
Open
Conversation
dylan-rumble
marked this pull request as ready for review
September 2, 2026 13:49
3 tasks
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.
Follow-up to #8420 (closed), which attempted to fix this with a one-liner in
ObjectMapperOutputProcessor. @soyuka's review was right: that diff alone silently degraded IRIs and headers. This PR addresses the whole write path instead, and answers each concern raised there. It comes with functional tests asserting onLocation,Content-Locationand@id, not just response bodies.Problem
Declaring
output:on a write operation with the ObjectMapper integration (stateOptions: new Options(entityClass: ...)) is currently broken before the output mapping is ever reached — it doesn't just return the wrong representation:ObjectMapperProvidermaps the entity to the output class on reads (fix(metadata): use operation output class for mapping instead of operation class #7601). On a PATCH, that mapped DTO becomesOBJECT_TO_POPULATE, but the deserializer discards it because it is not an instance of the resource class (ObjectToPopulateTrait::extractObjectToPopulate). A fresh resource instance is built from the request body alone, andObjectMapperInputProcessorthen maps it onto the entity — clobbering every field the client didn't send:Unable to generate an IRI for the item of type ...) when the resource IRI can't be generated from the write result — the exact use case that motivates a dedicated output DTO in the first place.What this PR changes
Four small, gated changes (each new path requires
canMap()and an explicitoutputclass, so non-ObjectMapper users and read operations are untouched):ObjectMapperProvider— on write methods, map the entity to the resource class, not the output class. The provided data is the deserialization target: it must be an instance of the resource class forobject_to_populateto work. This fixes the PATCH 500 and preserves merge semantics — the output class is only mapped to after persistence. (Covered by a dedicated test asserting a partial PATCH body leaves unsent fields untouched, guarding the ObjectMapper in 4.3.2 causes unintended validation failures on PATCH with input DTO #7886 regression class explicitly.)ObjectMapperOutputProcessor— map the persisted entity togetOutput()['class'] ?? getClass(), mirroring the read side (the feat(object-mapper): Honor the output class in ObjectMapperOutputProcessor on write operations #8420 one-liner, now safe).HttpResponseHeadersTrait— whenoriginal_datais a mapped non-resource output DTO, deriveLocation/Content-Locationfrom the operation's item URI template (itemUriTemplateonPost, the operation's ownuriTemplateonPatch/Put) via the existingitem_uri_templateIriConverter support. No template → previous behavior, no silent change.SerializerContextBuilder— setitem_uri_templatefor mapped-output item write operations, soPatch/Putresponses get a real@id(POST already gets it throughitemUriTemplate, the mechanism the docs'BookCreatedpattern relies on).Answering the concerns from #8420
Confirmed empirically — and fixed via (3). With
itemUriTemplatedeclared, a POST now returns:Without
itemUriTemplate, behavior is unchanged from today (no silent degradation is introduced; failing loudly at metadata time could be a follow-up if preferred).The processor maps entity → output, exactly like
ObjectMapperProviderhas done on reads since #7601 — one#[Map(source: MyEntity::class)]attribute on the output DTO, same contract as the read side. The serializer then receives an actual instance of the output class, so the output serialization path is taken naturally.ObjectMapperMetadataCollectionFactoryalready validates the output class mapping (@yceruto's objection from #7611 is resolved in current code).That revert concerned the provider mapping to the input class, pre-filling DTO properties before validation. This PR does the opposite: it makes the provider stop mapping to a non-resource class on writes (fixing the PATCH break described above), and only maps to the output class after persistence, where it can no longer affect what gets written.
BC note
The provider previously mapped the entity to the output class on write operations. As demonstrated above, that behavior was not usable (500 on PATCH, 400 on POST in the standard setup), but if someone's output DTO happened to be populate-compatible with their resource class, they would see changed behavior. Happy to discuss if this needs a changelog entry beyond the fix itself.
Tests
tests/Functional/MappedResourceOutputTest.php(new fixtures only): POST asserting response body is the DTO,@id/Location/Content-Locationare the item IRI; PATCH asserting the response DTO, a real@id, and — against the database — that unsent fields are preserved.tests/State/Provider/ObjectMapperProviderTest.php: one test updated, as it asserted the exact provider behavior shown broken above; a comment explains the new contract.MappingTest, ObjectMapper validation/unit and DTO/CRUD suites pass unchanged.Documentation
If the approach is accepted I'll open the docs PR:
output:+ ObjectMapper on writes, the#[Map(source: ...)]contract for output DTOs, and theitemUriTemplaterequirement for POST responses.