fix(client): keep an empty nested object out of the array flattener - #48
Conversation
toArray() cast every nested stdClass to an array, and (array) new stdClass()
is [], so a document attribute holding {} came back as []. That hit both the
insert()/insertMany() return value, which becomes the POST response body, and
every read that goes through this client. Measured on production against a
dedicated Mongo DocumentsDB: sent {} read back [], sent {"inner":{}} read back
{"inner":[]}, sent {"arr":[{},{"x":1}]} read back {"arr":[[],{"x":1}]}. Every
write returned 201 and nothing warned.
An empty BSON sub-document deserialises to a property-less stdClass under the
default typeMap this client uses, so leaving that instance in place is all it
takes for the value to re-encode as {}. Non-empty objects still become
associative arrays, so callers that iterate or key into the result see no
change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesEmpty object conversion
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR preserves nested empty
Confidence Score: 5/5The PR appears safe to merge with no actionable defects identified. The conversion now preserves the BSON/JSON distinction between empty objects and empty arrays, while the regression test covers the affected write and read paths and confirms surrounding conversion behavior remains intact. Important Files Changed
Reviews (1): Last reviewed commit: "fix(client): keep an empty nested object..." | Re-trigger Greptile |
What
An empty JSON object stored in a document attribute was served back as an empty array.
Client::toArray()recursively cast every nestedstdClassto an array, and(array) new \stdClass()is[]. So an attribute holding{}came back as[], an attribute holding{"inner": {}}came back as{"inner": []}, and{"arr": [{}, {"x": 1}]}came back as{"arr": [[], {"x": 1}]}.This is not only a read-path defect.
insert()(line 922) andinsertMany()(line 1000) both return$this->toArray($docObj), andinsertMany()is whatutopia-php/database'sAdapter/Mongo.phpcreateDocumentsreturns to the caller, so the POST response body was already wrong even when storage was correct. That is exactly what the production probe measured: the create response itself carried[], every write returned 201, and nothing warned.Measured on production 2026-08-12 against a dedicated Mongo-backed DocumentsDB and a Postgres-backed VectorsDB. Linear: https://linear.app/appwrite/issue/DAT-2310
The change
src/Client.php,toArray()(lines 1694-1729): a nested object with no properties is left as thestdClassit already is, so it re-encodes as{}. Every non-empty object still becomes an associative array, so callers that iterate or key into the result are unaffected. Only the empty case changes shape.Verified the BSON type before writing the condition rather than assuming it. This client decodes responses with
Document::fromBSON($bson)->toPHP()and configures no typeMap, so on the required extension version an empty sub-document is a plainstdClasswith zero properties, notBSONDocumentand not anArrayObjectsubclass:A BSON array still decodes to a PHP
array, so an empty array is not affected and must keep encoding as[]. The test pins that too.Same shape of solution as the
toAssociative()helper in utopia-php/monorepo#123, which fixes the framework-level root cause: convert non-empty objects to arrays, keep empty ones.Regression test, seen red
tests/MongoTest.php::testEmptyObjectSurvivesToArray. It drives the public API only, against a real MongoDB 8 over the real wire protocol. No reflection, no doubles, so nothing in it can answer its own assertion.It covers
insert(),insertMany(),find()+toArray()andlastDocument(), and asserts both directions of the contract: empty objects encode as{}, non-empty objects are still associative arrays, and empty arrays are still[].The test was written first and run against unmodified
origin/mainon PHP 8.3.7 withext-mongodb2.1.1 (the versioncomposer.jsonrequires), MongoDB 8.0.Red, unmodified
src/:phpunit.xmlsetsstopOnFailure="true", so only the first assertion is reported. To show the full breadth of the defect, here is the same set of shapes driven through the public API against unmodifiedsrc/, reproducing the production measurements exactly:Green, with the fix:
And the same shapes:
Full suite, lint, static analysis
Full suite against a real MongoDB 8, PHP 8.3.7,
ext-mongodb2.1.1, in the repo's ownphp83.Dockerfileimage:composer lint(Pint, PSR-12) andcomposer check(PHPStan level 4 oversrc) both clean, run inside the same PHP 8.3 image:Scope and seams
There are several flatteners stacked on top of each other. This PR fixes only the one site in this repo.
utopia-php/databaseis fixed in its own PR, and the HTTP framework root cause is utopia-php/monorepo#123, where nested empty JSON objects now decode to\stdClasswhile non-empty objects stay associative arrays, leaving the array-shaped contract unchanged.Checked the immediate consumer for an assumption this change could break:
Adapter/Mongo.php::replaceInternalIdsKeys()recurses onis_array($value)and otherwise passes the value through, so a property-lessstdClasssurvives it untouched. There are no keys in an empty object to rename.Release order:
utopia-php/databaseandutopia-php/mongofirst, thenutopia-php/http, then the cloud/CE bump. All three flatteners have to be out before the end-to-end shape is correct.No E2E test in this repo covers the cloud-to-edge-to-engine path this defect was measured on, so the end-to-end proof has to come from the cloud/CE side once all three releases land.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
{}when data is converted and re-encoded.Tests