alto / json-patch
A PHP JSON-Patch library based on RFC 6902 for generating smart diffs, applying patches, and rebuilding data structures.
Fund package maintenance!
smnandre
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
pkg:composer/alto/json-patch
Requires
- php: ^8.3
- ext-json: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
README
A strict, auditable JSON Patch implementation for PHP 8.3+. This library handles two concerns with precision:
- Apply: A deterministic RFC 6902 engine that replays patches exactly.
- Diff: A smart diff generator that produces stable, readable patches.
Built for systems where change history matters.
- Pure PHP: Tiny surface area, no heavy dependencies.
- Strict Types: Built for PHP 8.3+ with strict typing.
- Deterministic: Error model designed for auditability.
- Smart Diffing: Supports standard list replacement or smart "by-id" list diffing for readable patches.
Installation
composer require alto/json-patch
Why Alto JSON Patch?
For audit logs: Deterministic apply means you can verify patch integrity. Store the parent hash, the patch, and the result hash. Replaying the patch will always produce the same result.
For readable diffs: Generate clean patches that humans can review. Optional identity-based list diffing produces granular operations instead of replacing entire arrays.
For reliability: Pure PHP with strict types. No magic, no surprises.
Quick Start
use Alto\JsonPatch\JsonPatch; $document = [ 'user' => ['name' => 'Alice', 'role' => 'editor'], 'status' => 'draft', ]; $patch = [ ['op' => 'replace', 'path' => '/user/role', 'value' => 'admin'], ['op' => 'replace', 'path' => '/status', 'value' => 'published'], ]; $result = JsonPatch::apply($document, $patch); // ['user' => ['name' => 'Alice', 'role' => 'admin'], 'status' => 'published']
Generate Patches
Create patches automatically by diffing two states:
$before = ['version' => 1, 'status' => 'draft']; $after = ['version' => 2, 'status' => 'published', 'author' => 'Alice']; $patch = JsonPatch::diff($before, $after); // [ // ['op' => 'replace', 'path' => '/version', 'value' => 2], // ['op' => 'replace', 'path' => '/status', 'value' => 'published'], // ['op' => 'add', 'path' => '/author', 'value' => 'Alice'], // ]
Smart List Diffing
By default, lists are replaced entirely when they differ. For granular control, use identity-based diffing:
use Alto\JsonPatch\DiffOptions; $before = [ 'items' => [ ['id' => 'a', 'qty' => 1], ['id' => 'b', 'qty' => 2], ], ]; $after = [ 'items' => [ ['id' => 'b', 'qty' => 3], // Modified and reordered ['id' => 'c', 'qty' => 1], // Added ], ]; $options = new DiffOptions(['/items' => 'id']); $patch = JsonPatch::diff($before, $after, $options); // Generates move, add, remove, and replace operations for individual items
This produces readable patches where reviewers can see exactly which items changed.
Utility Methods
// Get a value at a JSON pointer path $name = JsonPatch::get($document, '/user/name'); // Test if a value matches (returns bool) $isAdmin = JsonPatch::test($document, '/user/role', 'admin'); // Validate patch structure without applying $errors = JsonPatch::validate($patch);
Audit Trail Example
class ChangeLog { public function recordChange(array $before, array $after): void { $patch = JsonPatch::diff($before, $after); $this->store([ 'parent_hash' => hash('sha256', json_encode($before)), 'patch' => $patch, 'result_hash' => hash('sha256', json_encode($after)), 'timestamp' => time(), ]); } public function verifyIntegrity(string $recordId): bool { $record = $this->fetch($recordId); $parent = $this->reconstructState($record['parent_hash']); $result = JsonPatch::apply($parent, $record['patch']); $computedHash = hash('sha256', json_encode($result)); return $computedHash === $record['result_hash']; } }
Supported Operations
All RFC 6902 operations:
add: Add a value at a pathremove: Remove a value at a pathreplace: Replace a value at a pathmove: Move a value from one path to anothercopy: Copy a value from one path to anothertest: Assert a value matches (useful for conditional patches)
Error Handling
Operations throw JsonPatchException with clear messages:
try { JsonPatch::apply($doc, $patch); } catch (JsonPatchException $e) { // "Operation 0 (replace): path '/missing/path' not found." // "Operation 1 (add): invalid path '/items/-1'." }
Advanced Usage
Float Comparison
JsonPatch uses strict equality (===) for values. Be aware that json_decode may treat numbers differently depending on flags.
For example, 1.0 (float) is not strictly equal to 1 (int). Ensure your input documents use consistent types if strict equality is required.
Limitations
applyJson: Empty Object vs Array
When using JsonPatch::applyJson(), the underlying json_decode converts empty JSON objects {} into empty PHP arrays
[].
Since PHP does not distinguish between empty associative arrays (objects) and empty indexed arrays (lists), an input of
{"key": {}} may result in {"key": []} after a round-trip.
If strictly preserving {} vs [] is critical, consider using apply() with pre-decoded structures where you can
control the object mapping (e.g. json_decode($json, false) for stdClass).
API Reference
JsonPatch
| Method | Description |
|---|---|
apply(array $doc, array $patch): array |
Apply a patch to a document |
applyJson(string $docJson, string $patchJson, int $flags = 0): string |
Apply patch to JSON string |
diff(array $from, array $to, ?DiffOptions $opts = null): array |
Generate patch from two states |
get(array $doc, string $path): mixed |
Get value at JSON pointer path |
test(array $doc, string $path, mixed $value): bool |
Test if value matches at path |
validate(array $patch): array |
Validate patch structure, returns errors |
DiffOptions
Configure identity-based list diffing:
$options = new DiffOptions([ '/users' => 'id', // Use 'id' field for /users array '/items' => 'sku', // Use 'sku' field for /items array ]);
License
This project is licensed under the MIT License - see the LICENSE file for details.