Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan": "^2.2",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^12.0"
},
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon

parameters:
level: 9
level: 10
paths:
- src
- scripts
Expand Down
51 changes: 40 additions & 11 deletions src/JsonMergePatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class JsonMergePatch
*/
public static function apply(mixed $document, mixed $patch): mixed
{
return self::mergeValue(self::deepCopy($document), self::deepCopy($patch));
return self::mergeValue(self::import($document), self::import($patch));
}

/**
Expand All @@ -42,7 +42,9 @@ public static function applyJson(
$document = json_decode($documentJson, false, 512, JSON_THROW_ON_ERROR);
$patch = json_decode($patchJson, false, 512, JSON_THROW_ON_ERROR);

return json_encode(self::apply($document, $patch), $encodeFlags | JSON_THROW_ON_ERROR);
// Decoded JSON is structurally valid and unaliased; skip apply()'s
// validating isolation pass.
return json_encode(self::mergeValue($document, $patch), $encodeFlags | JSON_THROW_ON_ERROR);
}

/**
Expand All @@ -51,7 +53,7 @@ public static function applyJson(
private static function mergeValue(mixed $target, mixed $patch): mixed
{
if (! self::isJsonObject($patch)) {
return self::deepCopy($patch);
return $patch;
}

if (! self::isJsonObject($target)) {
Expand Down Expand Up @@ -127,10 +129,10 @@ private static function hasObjectMember(array|stdClass $object, string $member):
private static function readObjectMember(array|stdClass $object, string $member): mixed
{
if ($object instanceof stdClass) {
return self::deepCopy($object->{$member});
return $object->{$member};
}

return self::deepCopy($object[$member]);
return $object[$member];
}

/**
Expand All @@ -141,12 +143,12 @@ private static function readObjectMember(array|stdClass $object, string $member)
private static function writeObjectMember(array|stdClass &$object, string $member, mixed $value): void
{
if ($object instanceof stdClass) {
$object->{$member} = self::deepCopy($value);
$object->{$member} = $value;

return;
}

$object[$member] = self::deepCopy($value);
$object[$member] = $value;
}

/**
Expand All @@ -166,19 +168,46 @@ private static function unsetObjectMember(array|stdClass &$object, string $membe
}

/**
* Copies JSON-like PHP values while rejecting values JSON cannot encode.
* Validates a JSON-like value and returns a copy that is safe to hand out.
*
* Arrays already behave as values through engine copy-on-write, so they are
* shared instead of rebuilt. stdClass instances have reference semantics
* and are cloned, including clones of every array holding one.
*/
private static function import(mixed $value): mixed
{
$containsObject = false;

return self::importValue($value, $containsObject);
}

/**
* Recursive worker for import() that rebuilds only object-bearing subtrees.
*/
private static function deepCopy(mixed $value): mixed
private static function importValue(mixed $value, bool &$containsObject): mixed
{
if (is_array($value)) {
return array_map(static fn (mixed $child): mixed => self::deepCopy($child), $value);
$result = $value;

foreach ($value as $key => $child) {
$childContainsObject = false;
$copy = self::importValue($child, $childContainsObject);

if ($childContainsObject) {
$containsObject = true;
$result[$key] = $copy;
}
}

return $result;
}

if ($value instanceof stdClass) {
$containsObject = true;
$copy = new stdClass();

foreach (get_object_vars($value) as $name => $child) {
$copy->{$name} = self::deepCopy($child);
$copy->{$name} = self::import($child);
}

return $copy;
Expand Down
Loading
Loading