mikemccabe/json-patch-php
PHP library implementing JSON Patch (RFC 6902) and JSON Pointer (RFC 6901). Diff two JSON documents, apply patch operations, or read values via pointers. Works with json_decode(..., true) arrays; includes optional SimpleXML-style array handling.
Installation:
Add to composer.json:
"require": {
"mikemccabe/json-patch-php": "dev-master"
}
Run composer update.
First Use Case: Apply a patch to a Laravel Eloquent model:
use mikemccabe\JsonPatch\JsonPatch;
$user = User::find(1);
$userData = $user->toArray();
$patches = [
['op' => 'replace', 'path' => '/name', 'value' => 'New Name'],
['op' => 'add', 'path' => '/metadata/updated', 'value' => true]
];
$patchedData = JsonPatch::patch($userData, $patches);
$user->update($patchedData);
Key Entry Points:
JsonPatch::patch($doc, $patches)JsonPatch::diff($src, $dst)JsonPatch::get($doc, $pointer)// routes/api.php
Route::patch('/users/{id}', [UserPatchController::class, 'update']);
// app/Http/Controllers/UserPatchController.php
public function update(Request $request, User $user) {
$patches = json_decode($request->getContent(), true);
$userData = $user->toArray();
$patchedData = JsonPatch::patch($userData, $patches);
$user->update($patchedData);
return response()->json($patchedData);
}
Compare two Eloquent models and generate patches:
$source = User::find(1)->toArray();
$target = User::find(2)->toArray();
$patches = JsonPatch::diff($source, $target);
// Apply patches to source to match target
Enable for arrays from SimpleXML:
$simplexmlData = simplexml_load_string($xml)->jsonSerialize();
$patched = JsonPatch::patch($simplexmlData, $patches, true); // true = simplexml_mode
Ensure patches conform to RFC 6902:
// app/Http/Middleware/ValidatePatch.php
public function handle($request, Closure $next) {
$patches = json_decode($request->getContent(), true);
foreach ($patches as $patch) {
if (!in_array($patch['op'], ['add', 'remove', 'replace', 'move', 'copy', 'test'])) {
abort(400, 'Invalid patch operation');
}
}
return $next($request);
}
Form Request Validation:
// app/Http/Requests/PatchUserRequest.php
public function rules() {
return [
'*.op' => 'required|in:add,remove,replace,move,copy,test',
'*.path' => 'required|string',
];
}
Service Layer Abstraction:
// app/Services/PatchService.php
class PatchService {
public function apply(array $data, array $patches): array {
return JsonPatch::patch($data, $patches);
}
}
Testing with Factories:
// tests/Feature/PatchTest.php
public function test_patch_user() {
$user = User::factory()->create();
$patches = [['op' => 'replace', 'path' => '/name', 'value' => 'Patched']];
$patched = $this->patch("/users/{$user->id}", $patches);
$patched->assertJson(['name' => 'Patched']);
}
Batch Processing:
// Process patches in chunks for large datasets
$chunkSize = 100;
$chunks = array_chunk($patches, $chunkSize);
foreach ($chunks as $chunk) {
$data = JsonPatch::patch($data, $chunk);
}
Caching Diffs:
// Cache patch diffs for expensive operations
$cacheKey = "diff_{$sourceId}_{$targetId}";
$patches = Cache::remember($cacheKey, now()->addHours(1), function() use ($source, $target) {
return JsonPatch::diff($source, $target);
});
PHP 8.x Compatibility:
JsonPatch class:
// Before (PHP 5.6)
public function __construct() { ... }
// After (PHP 8.1)
public function __construct(
private bool $simplexmlMode = false
) { ... }
Simplexml Mode Quirks:
[] vs. objects {} behave identically, causing unexpected add operations.$data = array_filter($data, fn($v) => $v !== null, ARRAY_FILTER_USE_BOTH);
Circular References:
json_encode() + json_decode() to flatten data:
$flatData = json_decode(json_encode($data), true);
Path Resolution:
/foo/0/bar may fail if /foo is not an array.$pointer = new \mikemccabe\JsonPointer\JsonPointer($patch['path']);
if (!$pointer->get($data)) {
throw new \InvalidArgumentException("Invalid path: {$patch['path']}");
}
Laravel-Specific Gotchas:
date, encrypted) may corrupt patched values.$attributes = $model->getAttributes();
$patched = JsonPatch::patch($attributes, $patches);
$model->setRawAttributes($patched);
Validate Patches: Use the official test suite:
git submodule update --init --recursive
php runtests.php
Log Patch Operations:
// app/Services/PatchService.php
public function apply(array $data, array $patches): array {
$patched = JsonPatch::patch($data, $patches);
\Log::debug('Applied patches', [
'input' => $data,
'patches' => $patches,
'output' => $patched,
]);
return $patched;
}
Inspect Pointers: Manually test JSON Pointers:
$pointer = new \mikemccabe\JsonPointer\JsonPointer('/users/0/name');
$value = $pointer->get($data);
Handle Edge Cases:
[] vs. {}./foo/0 vs. /foo/1 are handled as arrays.Custom Patch Operations: Extend the core logic:
// app/Extensions/CustomPatch.php
class CustomPatch extends \mikemccabe\JsonPatch\JsonPatch {
public static function customOp($doc, $patch) {
// Implement custom operation (e.g., 'increment')
$pointer = new \mikemccabe\JsonPointer\JsonPointer($patch['path']);
$value = $pointer->get($doc);
$pointer->set($doc, $value + 1);
return $doc;
}
}
Laravel Event Integration: Trigger events after patching:
// app/Listeners/PatchApplied.php
public function handle($event) {
event(new PatchApplied($event->model, $event->patches));
}
Policy Integration: Restrict patch operations:
// app/Policies/UserPatchPolicy.php
public function applyPatch(User $user, array $patches) {
return $user->id === auth()->id();
}
Testing Utilities:
Add helper methods to JsonPatch:
// app/Extensions/TestHelpers.php
namespace mikemccabe\JsonPatch;
How can I help you explore Laravel packages today?