Skip to content

Commit 78e9aa2

Browse files
committed
Merge remote-tracking branch 'origin/master' into fix/190-alwaysinclude-fk-columns
# Conflicts: # tests/ModelControllerProjectionTest.php
2 parents 10b3aa2 + aaf96b0 commit 78e9aa2

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/Mvc/Controller/ModelController.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,36 @@ protected function eagerLoadPlan($projection)
405405
return $plan;
406406
}
407407

408+
/**
409+
* Strip eager-load helper keys (`<fk>_object`) from a response row (KYTE-#190).
410+
* Model::eagerLoadRelations attaches the fetched relation as a dynamic
411+
* `<fk>_object` property for getObject() to consume — it is an internal
412+
* expansion helper, not a response field. getAllParams() surfaces it, so
413+
* left in the payload it duplicates the related row and, for relations whose
414+
* columns carry binary data (e.g. bzip2-compressed section templates),
415+
* produces invalid UTF-8 that makes json_encode fail. Only non-struct keys
416+
* ending in `_object` are removed. Pure/static for unit testing.
417+
*
418+
* @param array<string,mixed> $response Row from getAllParams()
419+
* @param array<string,mixed> $struct Model struct
420+
* @return array<string,mixed>
421+
*/
422+
public static function stripEagerHelpers(array $response, $struct)
423+
{
424+
foreach (array_keys($response) as $rk) {
425+
if (is_string($rk) && substr($rk, -7) === '_object' && !isset($struct[$rk])) {
426+
unset($response[$rk]);
427+
}
428+
}
429+
return $response;
430+
}
431+
408432
protected function getObject($obj) {
409433
try {
410-
$response = $obj->getAllParams();
411-
434+
// Strip eager-load `<fk>_object` helpers; the object property itself
435+
// is kept so FK expansion below still uses the eager-loaded object.
436+
$response = self::stripEagerHelpers($obj->getAllParams(), $obj->kyte_model['struct']);
437+
412438
foreach ($response as $key => &$value) {
413439
if (!isset($obj->kyte_model['struct'][$key])) {
414440
continue; // Skip if key not found in struct

tests/ModelControllerProjectionTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,27 @@ public function testProjectionKeepsForeignKeysSoExpansionStillRuns(): void
117117
$this->assertContains('client', $p); // FK preserved even though unrequested
118118
$this->assertNotContains('body', $p); // large column still trimmed
119119
}
120+
121+
public function testStripEagerHelpersRemovesObjectKeysButKeepsRealFields(): void
122+
{
123+
// Eager-load attaches `<fk>_object` helper props that getAllParams
124+
// surfaces; they must not reach the response (KYTE-#190 — a bzip2
125+
// section-template `<fk>_object` carries binary that breaks json_encode).
126+
$struct = ['id' => [], 'parent' => ['fk' => []], 'name' => []];
127+
$row = [
128+
'id' => 1,
129+
'parent' => 5,
130+
'name' => 'x',
131+
'parent_object' => (object) ['name' => 'P'], // eager helper (object)
132+
'header_object' => "\xff\xfebinary", // eager helper (binary)
133+
];
134+
$out = ModelController::stripEagerHelpers($row, $struct);
135+
$this->assertArrayHasKey('id', $out);
136+
$this->assertArrayHasKey('parent', $out); // real FK field kept
137+
$this->assertArrayHasKey('name', $out);
138+
$this->assertArrayNotHasKey('parent_object', $out); // helper stripped
139+
$this->assertArrayNotHasKey('header_object', $out); // helper stripped
140+
// the survivors must be JSON-encodable (the whole point)
141+
$this->assertNotFalse(json_encode($out));
142+
}
120143
}

0 commit comments

Comments
 (0)