Skip to content

Commit c93dff3

Browse files
committed
fix: PatchOperationList should decode patches as objects
Because although the top-level patch entries will be equivalent as arrays or objects, properties like the `value` key can contain any type of data.
1 parent 5cf0da3 commit c93dff3

2 files changed

Lines changed: 37 additions & 26 deletions

File tree

src/operations/PatchOperationList.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use blancks\JsonPatch\exceptions\InvalidPatchOperationException;
77
use blancks\JsonPatch\json\handlers\BasicJsonHandler;
88
use blancks\JsonPatch\json\handlers\JsonHandlerInterface;
9+
use stdClass;
910

1011
final class PatchOperationList implements \JsonSerializable
1112
{
@@ -25,7 +26,7 @@ public static function fromJson(
2526
JsonHandlerInterface $jsonHandler = new BasicJsonHandler(),
2627
array $customClasses = [],
2728
): self {
28-
$patches = $jsonHandler->decode($jsonOperations, ['associative' => true]);
29+
$patches = $jsonHandler->decode($jsonOperations);
2930
if (!(is_array($patches) && array_is_list($patches))) {
3031
throw new InvalidPatchException(
3132
sprintf('Invalid patch structure (expected list, got %s)', get_debug_type($patches)),
@@ -44,7 +45,10 @@ public static function fromJson(
4445

4546
return new PatchOperationList(
4647
...array_map(
47-
function (array $patch) use ($classes) {
48+
function (stdClass $patch) use ($classes) {
49+
// The top-level patch entries should be identical as objects or arrays, cast to array to allow
50+
// spreading the properties into the DTO constructors (which are assumed to match the JSON objects)
51+
$patch = (array) $patch;
4852
$op = $patch['op'];
4953
unset($patch['op']);
5054
if (!isset($classes[$op])) {

tests/operations/PatchOperationListTest.php

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPUnit\Framework\Attributes\DataProvider;
2626
use PHPUnit\Framework\Assert;
2727
use PHPUnit\Framework\Attributes\UsesClass;
28+
use stdClass;
2829
use Throwable;
2930

3031
#[CoversClass(PatchOperationList::class)]
@@ -51,6 +52,10 @@ class PatchOperationListTest extends JsonPatchCompliance
5152
*/
5253
public static function validEncodeDecodeProvider(): array
5354
{
55+
$objValue = new stdClass();
56+
$objValue->type = 'foo';
57+
$objValue->list = ['bar', 'baz'];
58+
5459
return [
5560
'empty list' => [
5661
[],
@@ -82,6 +87,20 @@ public static function validEncodeDecodeProvider(): array
8287
]
8388
JSON,
8489
],
90+
'operations with object values' => [
91+
[
92+
new Add(path: '/bar', value: $objValue),
93+
new Replace(path: '/bar', value: $objValue),
94+
new Test(path: '/bar', value: $objValue),
95+
],
96+
<<<'JSON'
97+
[
98+
{"op":"add","path":"/bar","value":{"type":"foo","list":["bar","baz"]}},
99+
{"op":"replace","path":"/bar","value":{"type":"foo","list":["bar","baz"]}},
100+
{"op":"test","path":"/bar","value":{"type":"foo","list":["bar","baz"]}}
101+
]
102+
JSON,
103+
]
85104
];
86105
}
87106

@@ -150,13 +169,13 @@ public function decode(string $json, array $options = []): mixed
150169
Assert::assertSame(
151170
[
152171
'json' => '[fake]',
153-
'options' => ['associative' => true],
172+
'options' => [],
154173
],
155174
get_defined_vars(),
156175
'JSONHandler should have been called with expected args',
157176
);
158177
return [
159-
['op' => 'remove', 'path' => '/some/path'],
178+
(object) ['op' => 'remove', 'path' => '/some/path'],
160179
];
161180
}
162181
},
@@ -170,23 +189,6 @@ public function decode(string $json, array $options = []): mixed
170189

171190
public function testItCanDecodeWithCustomOperationClasses(): void
172191
{
173-
$appendOperation = new class('/greeting', ' World') extends PatchOperation {
174-
public function __construct(
175-
public readonly string $path,
176-
public readonly string $suffix,
177-
) {
178-
parent::__construct('append');
179-
}
180-
};
181-
$customAddOperation = new class('/greeting', 'Hello') extends PatchOperation {
182-
public function __construct(
183-
public readonly string $path,
184-
public readonly mixed $value,
185-
) {
186-
parent::__construct('add');
187-
}
188-
};
189-
190192
$result = PatchOperationList::fromJson(
191193
<<<'JSON'
192194
[
@@ -196,15 +198,15 @@ public function __construct(
196198
]
197199
JSON,
198200
customClasses: [
199-
'add' => $customAddOperation::class,
200-
'append' => $appendOperation::class,
201+
'add' => CustomAdd::class,
202+
'append' => Append::class,
201203
],
202204
);
203205

204206
$this->assertEquals(
205207
new PatchOperationList(
206-
$customAddOperation,
207-
$appendOperation,
208+
new CustomAdd('/greeting', 'Hello'),
209+
new Append('/greeting', ' World'),
208210
new Copy(path: '/whatever', from: '/greeting'),
209211
),
210212
$result,
@@ -220,13 +222,18 @@ public static function invalidJsonProvider(): array
220222
'json is not a list (example 1)' => [
221223
'{"some": "field"}',
222224
InvalidPatchException::class,
223-
'Invalid patch structure (expected list, got array)',
225+
'Invalid patch structure (expected list, got stdClass)',
224226
],
225227
'json is not a list (example 2)' => [
226228
'true',
227229
InvalidPatchException::class,
228230
'Invalid patch structure (expected list, got bool)',
229231
],
232+
'json is not a list (example 3)' => [
233+
'{"2": {"op": "add", "path": "/some/path", "value": "World"}}',
234+
InvalidPatchException::class,
235+
'Invalid patch structure (expected list, got stdClass)',
236+
],
230237
'unknown operation' => [
231238
'[{"op": "scramble", "path": "/anywhere"}]',
232239
InvalidPatchOperationException::class,

0 commit comments

Comments
 (0)