diff --git a/.gitattributes b/.gitattributes index a35b443..ddb19af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,14 @@ -/.git* export-ignore -/.php-cs-fixer.dist.php export-ignore -/phpstan.dist.neon export-ignore -/phpunit.dist.xml export-ignore -/docs/ export-ignore -/tests/ export-ignore +* text=auto eol=lf + +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/docs export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore + +/docs/** linguist-documentation +/tests/fixtures/** -linguist-detectable diff --git a/.gitignore b/.gitignore index d029bb1..8346dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -/.phpunit.cache/ /vendor/ -/.php-cs-fixer.cache /composer.lock +/.phpunit.cache/ +/.phpunit.result.cache +/.php-cs-fixer.cache +/coverage/ /coverage.xml -/phpstan.neon -/phpunit.xml +/clover.xml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f468b6d..4bb65cd 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,26 +1,28 @@ in(__DIR__) -; +$finder = PhpCsFixer\Finder::create() + ->in([__DIR__.'/src', __DIR__.'/tests']) + ->exclude('fixtures'); return (new PhpCsFixer\Config()) - ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) - ->setFinder($finder) ->setRiskyAllowed(true) ->setRules([ - '@PER-CS' => true, - '@Symfony' => true, + '@PER-CS2.0' => true, 'declare_strict_types' => true, 'header_comment' => ['header' => $licence], + 'no_unused_imports' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const']], + 'phpdoc_line_span' => ['const' => 'multi', 'property' => 'multi', 'method' => 'multi'], ]) -; + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index f10b7a3..e700b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## [Unreleased] + +- Add documentation. + +## [1.0.1] - 2026-07-30 + +- Move to ALTO PHP. + ## [1.0.0] - 2026-01-17 -* Initial release +- Initial release. diff --git a/LICENSE b/LICENSE index 9310ffc..dc9b29f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Simon André +Copyright (c) 2026-present Simon André Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index fedaca9..b2b8b13 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,46 @@ -# ALTO \ JSON Patch +# ALTO JSON Patch -A strict, auditable [JSON Patch](https://en.wikipedia.org/wiki/JSON_Patch) implementation for PHP 8.3+. This library handles two concerns with precision: +Strict RFC 6902 patching and deterministic diffs for PHP. -1. **Apply**: A deterministic **[RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)** engine that replays patches exactly. -2. **Diff**: A smart diff generator that produces stable, readable patches. +  ![PHP Version](https://img.shields.io/badge/PHP-8.3%2B-00B7FF?logoColor=00B7FF&labelColor=050608) +  ![CI](https://img.shields.io/github/actions/workflow/status/altophp/json-patch/CI.yml?branch=main&label=Tests&labelColor=050608&color=00B7FF) +  [![Packagist](https://img.shields.io/packagist/v/alto/json-patch?label=Packagist&labelColor=050608&color=00B7FF)](https://packagist.org/packages/alto/json-patch) +  ![License](https://img.shields.io/github/license/altophp/json-patch?label=License&labelColor=050608&color=00B7FF) +  [![GitHub Sponsors](https://img.shields.io/github/sponsors/smnandre?logo=githubsponsors&logoColor=00B7FF&label=%20Sponsor&labelColor=050608&color=00B7FF)](https://github.com/sponsors/smnandre) -Built for systems where change history matters. +ALTO JSON Patch applies all six JSON Patch operations and generates stable patches between PHP +values. Its identity-aware list diffing can express moves and nested changes instead of replacing +complete lists, keeping generated patches compact and readable. ---- +```php +use Alto\JsonPatch\JsonPatch; + +$before = ['status' => 'draft', 'tags' => ['php']]; +$after = ['status' => 'published', 'tags' => ['php', 'json']]; -  [![PHP Version](https://img.shields.io/badge/PHP-8.3+-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/json-patch) -  [![CI](https://img.shields.io/github/actions/workflow/status/altophp/json-patch/CI.yml?branch=main&label=Tests&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](https://github.com/altophp/json-patch/actions) -  [![Packagist Version](https://img.shields.io/packagist/v/alto/json-patch?label=Stable&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](https://packagist.org/packages/alto/json-patch) -  [![PHP Version](https://img.shields.io/badge/PHPUnit-100%25-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/json-patch) -  [![PHP Version](https://img.shields.io/badge/PHPStan-LVL%2010-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/json-patch) -  [![License](https://img.shields.io/github/license/altophp/json-patch?label=License&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](./LICENSE) +$patch = JsonPatch::diff($before, $after); +$result = JsonPatch::apply($before, $patch); + +assert($after === $result); +``` -* **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. +The package has no runtime dependencies beyond PHP's JSON extension. Its test suite includes the +RFC 6902 compliance corpus, and the codebase is analyzed at PHPStan level 10. ## Installation +Install ALTO JSON Patch with Composer: + ```bash 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. +ALTO JSON Patch requires PHP 8.3 or later and the JSON extension. The extension ships with PHP. ## Quick Start +Apply a sequence of operations to an in-memory value: + ```php use Alto\JsonPatch\JsonPatch; @@ -53,162 +55,97 @@ $patch = [ ]; $result = JsonPatch::apply($document, $patch); -// ['user' => ['name' => 'Alice', 'role' => 'admin'], 'status' => 'published'] ``` -## Generate Patches +The original value is unchanged. Operations run in order, and each operation sees the result of +the preceding one. -Create patches automatically by diffing two states: +## Applying Patches -```php -$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 +`JsonPatch::apply()` supports every RFC 6902 operation: -By default, lists are replaced entirely when they differ. For granular control, use identity-based diffing: +| Operation | Effect | +| --- | --- | +| `add` | Insert or replace a value | +| `remove` | Delete an existing value | +| `replace` | Replace an existing value | +| `move` | Move a value to another path | +| `copy` | Copy a value to another path | +| `test` | Assert that a value matches | -```php -use Alto\JsonPatch\DiffOptions; +Use `JsonPatch::applyJson()` to work directly with JSON strings. Read +[Applying patches](docs/applying.md) for path rules, validation, JSON handling, and failures. -$before = [ - 'items' => [ - ['id' => 'a', 'qty' => 1], - ['id' => 'b', 'qty' => 2], - ], -]; +## Generating Patches -$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 +Generate the operations needed to transform one state into another: ```php -// Get a value at a JSON pointer path -$name = JsonPatch::get($document, '/user/name'); +$patch = JsonPatch::diff( + ['version' => 1, 'status' => 'draft'], + ['version' => 2, 'status' => 'published'], +); +``` -// Test if a value matches (returns bool) -$isAdmin = JsonPatch::test($document, '/user/role', 'admin'); +Object keys are compared recursively. Lists use a longest common subsequence by default, producing +stable `add` and `remove` operations while preserving unchanged items. -// Validate patch structure without applying -$errors = JsonPatch::validate($patch); -``` +## Identity-aware Lists -## Audit Trail Example +Configure an identity key to express item moves and nested changes: ```php -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']; - } -} -``` +use Alto\JsonPatch\DiffOptions; -## Supported Operations +$options = new DiffOptions( + listIdentityByPointer: ['/items' => 'id'], +); -All RFC 6902 operations: +$patch = JsonPatch::diff($before, $after, $options); +``` -- `add`: Add a value at a path -- `remove`: Remove a value at a path -- `replace`: Replace a value at a path -- `move`: Move a value from one path to another -- `copy`: Copy a value from one path to another -- `test`: Assert a value matches (useful for conditional patches) +Read [Generating patches](docs/diffing.md) for list strategies and their fallback behavior. -## Error Handling +## JSON Pointers -Operations throw `JsonPatchException` with clear messages: +Patch paths follow RFC 6901. Use `JsonPatch::get()` and `JsonPatch::test()` to inspect values at a +path, or `Pointer` when another component needs to parse and compose paths. ```php -try { - JsonPatch::apply($doc, $patch); -} catch (JsonPatchException $e) { - // "Operation 0 (replace): path '/missing/path' not found." - // "Operation 1 (add): invalid path '/items/-1'." -} +$name = JsonPatch::get($document, '/user/name'); +$isAdmin = JsonPatch::test($document, '/user/role', 'admin'); ``` -## 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. +Read [JSON Pointers](docs/pointers.md) for root paths, list indices, and escaping. The +[complete guide](docs/index.md) also covers installation and a first end-to-end patch. -## Limitations +## Contributing -### `applyJson`: Empty Object vs Array +Contributions of all kinds are welcome. Visit the +[project on GitHub](https://github.com/altophp/json-patch) to +[report a bug](https://github.com/altophp/json-patch/issues/new), +[suggest a feature](https://github.com/altophp/json-patch/issues/new), or +[open a pull request](https://github.com/altophp/json-patch/pulls). -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`). +Before submitting code, run: -## API Reference - -### `JsonPatch` +```bash +# Runs PHP CS Fixer, PHPStan, and PHPUnit +composer qa +``` -| 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 | +Changes to public behavior should include tests and documentation. -### `DiffOptions` +## Support -Configure identity-based list diffing: +ALTO JSON Patch is open source. You can support its continued development through +[GitHub Sponsors](https://github.com/sponsors/smnandre). -```php -$options = new DiffOptions([ - '/users' => 'id', // Use 'id' field for /users array - '/items' => 'sku', // Use 'sku' field for /items array -]); -``` +Sharing this package with others or +[starring it on GitHub](https://github.com/altophp/json-patch) is also much +appreciated. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -```` +ALTO JSON Patch is released by [ALTO PHP](https://altophp.com) under the +[MIT License](LICENSE). diff --git a/composer.json b/composer.json index 8bcb899..5d4a194 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "alto/json-patch", - "description": "A PHP JSON-Patch library based on RFC 6902 for generating smart diffs, applying patches, and rebuilding data structures.", + "description": "Strict RFC 6902 patching and deterministic diffs for PHP.", "license": "MIT", "type": "library", "keywords": [ @@ -21,10 +21,11 @@ "email": "smn.andre@gmail.com" } ], - "homepage": "https://github.com/altophp/json-patch", + "homepage": "https://altophp.com/json-patch", "support": { "issues": "https://github.com/altophp/json-patch/issues", - "docs": "https://github.com/altophp/json-patch#readme" + "source": "https://github.com/altophp/json-patch", + "docs": "https://altophp.com/json-patch" }, "funding": [ { @@ -37,9 +38,9 @@ "ext-json": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.0", + "friendsofphp/php-cs-fixer": "^3.94", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5" }, "autoload": { "psr-4": { @@ -52,19 +53,17 @@ } }, "config": { - "optimize-autoloader": true, - "preferred-install": "dist", "sort-packages": true }, "scripts": { - "check": [ - "@cs-check", - "@phpstan", + "cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --sequential", + "cs:fix": "vendor/bin/php-cs-fixer fix --sequential", + "qa": [ + "@cs", + "@sa", "@test" ], - "cs-check": "php-cs-fixer fix --dry-run --diff", - "cs-fix": "php-cs-fixer fix", - "phpstan": "phpstan analyse --level=10", + "sa": "vendor/bin/phpstan analyse --memory-limit=-1", "test": "phpunit" } } diff --git a/docs/applying.md b/docs/applying.md new file mode 100644 index 0000000..b9c769b --- /dev/null +++ b/docs/applying.md @@ -0,0 +1,76 @@ +# Applying patches + +`JsonPatch::apply()` accepts any PHP value as the document and a list of RFC +6902 operation arrays. It returns the transformed value. + +## Operations + +```php +use Alto\JsonPatch\JsonPatch; + +$document = [ + 'name' => 'Draft', + 'tags' => ['php'], + 'metadata' => ['owner' => 'alice'], +]; + +$patch = [ + ['op' => 'test', 'path' => '/name', 'value' => 'Draft'], + ['op' => 'replace', 'path' => '/name', 'value' => 'Published'], + ['op' => 'add', 'path' => '/tags/-', 'value' => 'json'], + ['op' => 'copy', 'from' => '/metadata/owner', 'path' => '/author'], + ['op' => 'move', 'from' => '/metadata/owner', 'path' => '/owner'], + ['op' => 'remove', 'path' => '/metadata'], +]; + +$result = JsonPatch::apply($document, $patch); +``` + +- `add` inserts or replaces an object member and inserts into a list. The `-` + index appends to a list. +- `remove` deletes an existing value. The document root cannot be removed. +- `replace` requires an existing target, except that the empty root pointer + replaces the complete document. +- `move` removes an existing value and adds it elsewhere. A parent cannot move + into its own descendant. +- `copy` reads an existing value and adds it elsewhere. +- `test` uses strict recursive equality and aborts on mismatch. + +## Inspect and validate + +```php +$name = JsonPatch::get($result, '/name'); +$isPublished = JsonPatch::test($result, '/name', 'Published'); +$errors = JsonPatch::validate($patch); +``` + +`validate()` checks operation structure without reading a document. An empty +result means structurally valid; paths may still be missing when applied. + +## Handle failures + +```php +use Alto\JsonPatch\Exception\JsonPatchException; + +try { + $result = JsonPatch::apply($document, $patch); +} catch (JsonPatchException $error) { + // Invalid operation, path, container type, or failed test. +} +``` + +Specific subclasses distinguish invalid operations, missing paths, container +type mismatches, and failed `test` operations. + +## JSON strings + +```php +$json = JsonPatch::applyJson( + '{"status":"draft"}', + '[{"op":"replace","path":"/status","value":"published"}]', +); +``` + +`applyJson()` decodes JSON into associative arrays and returns compact JSON +with unescaped Unicode and slashes. Invalid JSON and a non-list patch raise +`JsonPatchException`. diff --git a/docs/diffing.md b/docs/diffing.md new file mode 100644 index 0000000..440ff96 --- /dev/null +++ b/docs/diffing.md @@ -0,0 +1,64 @@ +# Generating patches + +`JsonPatch::diff()` returns operations that transform one value into another. + +```php +use Alto\JsonPatch\JsonPatch; + +$before = ['version' => 1, 'status' => 'draft']; +$after = ['version' => 2, 'status' => 'published', 'author' => 'Alice']; + +$patch = JsonPatch::diff($before, $after); +$result = JsonPatch::apply($before, $patch); + +assert($after === $result); +``` + +Object keys are removed, added, or recursively changed. Scalar values and +changes between object and list shapes produce `replace` operations. + +## Lists + +Lists use a longest common subsequence by default. This produces stable +`remove` and `add` operations while preserving unchanged elements. + +```php +use Alto\JsonPatch\DiffOptions; + +$patch = JsonPatch::diff( + ['tags' => ['php', 'json']], + ['tags' => ['php', 'api']], + new DiffOptions(useLcs: true), +); +``` + +Set `useLcs` to `false` to replace a changed list as one value. + +## Lists with identities + +Identity-based diffing can express moves and nested item changes: + +```php +$before = ['items' => [ + ['id' => 'a', 'quantity' => 1], + ['id' => 'b', 'quantity' => 2], +]]; + +$after = ['items' => [ + ['id' => 'b', 'quantity' => 3], + ['id' => 'c', 'quantity' => 1], +]]; + +$options = new DiffOptions( + listIdentityByPointer: ['/items' => 'id'], +); + +$patch = JsonPatch::diff($before, $after, $options); +``` + +Every item in both lists must be an object-like associative array with a +unique string or integer identity. Otherwise, diffing falls back to the normal +list strategy. + +Diffing stops at a nesting depth of 512 and raises `JsonPatchException` rather +than recursing without a bound. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..b6346ad --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,35 @@ +# Getting started + +Apply a list of operations to an in-memory document: + +```php +use Alto\JsonPatch\JsonPatch; + +$before = [ + 'user' => ['name' => 'Alice', 'role' => 'editor'], + 'status' => 'draft', +]; + +$patch = [ + ['op' => 'replace', 'path' => '/user/role', 'value' => 'admin'], + ['op' => 'replace', 'path' => '/status', 'value' => 'published'], +]; + +$after = JsonPatch::apply($before, $patch); +``` + +The original value is unchanged. Operations run in order, and each subsequent +operation sees the result of the preceding one. + +## Generate the reverse transformation + +```php +$generated = JsonPatch::diff($before, $after); +$replayed = JsonPatch::apply($before, $generated); + +assert($after === $replayed); +``` + +Generated operations use JSON Pointer paths. Continue with [Applying](applying.md) +for all operations, [Diffing](diffing.md) for list strategies, and +[Pointers](pointers.md) for path escaping. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..71d9094 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +# Alto JSON Patch + +Alto JSON Patch applies RFC 6902 operations and generates deterministic +patches between PHP values. + +```php +use Alto\JsonPatch\JsonPatch; + +$document = ['status' => 'draft']; +$patch = [ + ['op' => 'replace', 'path' => '/status', 'value' => 'published'], +]; + +$result = JsonPatch::apply($document, $patch); +``` + +## Introduction + +- [Installation](installation.md): install the package and verify the runtime. +- [Getting started](getting-started.md): apply and generate a first patch. + +## Patching + +- [Applying](applying.md): use the six RFC 6902 operations and handle failures. +- [Diffing](diffing.md): generate stable object and list changes. +- [Pointers](pointers.md): address document values with RFC 6901 paths. + +The package transforms in-memory values and JSON strings. Persistence, +authorization, version storage, and conflict resolution remain application +responsibilities. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a3137d6 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,27 @@ +# Installation + +Alto JSON Patch requires PHP 8.3 or later and the JSON extension. + +```bash +composer require alto/json-patch +``` + +## Verify the installation + +```php + false], + [['op' => 'replace', 'path' => '/enabled', 'value' => true]], +); + +var_export($result); +``` + +The script prints `array ('enabled' => true,)` with PHP's normal multiline +formatting. diff --git a/docs/pointers.md b/docs/pointers.md new file mode 100644 index 0000000..913f453 --- /dev/null +++ b/docs/pointers.md @@ -0,0 +1,50 @@ +# JSON Pointers + +JSON Patch paths follow RFC 6901. The empty string addresses the document root; +every other pointer starts with `/`. + +```php +use Alto\JsonPatch\JsonPatch; + +$document = [ + 'users' => [ + ['name' => 'Alice'], + ], +]; + +echo JsonPatch::get($document, '/users/0/name'); +``` + +List indices are decimal integers without leading zeros. The `-` segment is +valid only for appending with an `add` operation. + +## Escape property names + +Within one segment, `~1` represents `/` and `~0` represents `~`: + +```php +$document = [ + 'a/b' => ['~key' => 'value'], +]; + +echo JsonPatch::get($document, '/a~1b/~0key'); +``` + +Invalid escape sequences raise `JsonPatchException`. + +## Inspect a pointer + +```php +use Alto\JsonPatch\Pointer; + +$pointer = Pointer::parse('/users/0/name'); + +$pointer->segments(); // ['users', '0', 'name'] +$pointer->parent()->toString(); // /users/0 +$pointer->last(); // name +$pointer->isRoot(); // false +``` + +Parsed pointers are cached internally. Application code normally uses +`JsonPatch` methods directly; `Pointer` is useful when another component must +inspect or compose the same path model. diff --git a/phpstan.dist.neon b/phpstan.neon.dist similarity index 100% rename from phpstan.dist.neon rename to phpstan.neon.dist diff --git a/phpunit.dist.xml b/phpunit.xml.dist similarity index 100% rename from phpunit.dist.xml rename to phpunit.xml.dist diff --git a/src/DiffOptions.php b/src/DiffOptions.php index 44cce70..d88b1c9 100644 --- a/src/DiffOptions.php +++ b/src/DiffOptions.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -25,8 +25,7 @@ public function __construct( public array $listIdentityByPointer = [], public bool $useLcs = true, - ) { - } + ) {} public function identityKeyFor(string $pointer): ?string { diff --git a/src/Exception/InvalidOperationException.php b/src/Exception/InvalidOperationException.php index 84c5707..110928a 100644 --- a/src/Exception/InvalidOperationException.php +++ b/src/Exception/InvalidOperationException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -13,6 +13,7 @@ namespace Alto\JsonPatch\Exception; -final class InvalidOperationException extends JsonPatchException -{ -} +/** + * @author Simon André + */ +final class InvalidOperationException extends JsonPatchException {} diff --git a/src/Exception/JsonPatchException.php b/src/Exception/JsonPatchException.php index aece723..22481f1 100644 --- a/src/Exception/JsonPatchException.php +++ b/src/Exception/JsonPatchException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -18,6 +18,4 @@ * * @author Simon André */ -class JsonPatchException extends \RuntimeException -{ -} +class JsonPatchException extends \RuntimeException {} diff --git a/src/Exception/PathNotFoundException.php b/src/Exception/PathNotFoundException.php index e25fd94..39669d1 100644 --- a/src/Exception/PathNotFoundException.php +++ b/src/Exception/PathNotFoundException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -13,6 +13,7 @@ namespace Alto\JsonPatch\Exception; -final class PathNotFoundException extends JsonPatchException -{ -} +/** + * @author Simon André + */ +final class PathNotFoundException extends JsonPatchException {} diff --git a/src/Exception/TestFailedException.php b/src/Exception/TestFailedException.php index 219a62c..bbd1712 100644 --- a/src/Exception/TestFailedException.php +++ b/src/Exception/TestFailedException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -16,6 +16,4 @@ /** * @author Simon André */ -final class TestFailedException extends JsonPatchException -{ -} +final class TestFailedException extends JsonPatchException {} diff --git a/src/Exception/TypeMismatchException.php b/src/Exception/TypeMismatchException.php index 7b17d28..76678c1 100644 --- a/src/Exception/TypeMismatchException.php +++ b/src/Exception/TypeMismatchException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -13,6 +13,7 @@ namespace Alto\JsonPatch\Exception; -final class TypeMismatchException extends JsonPatchException -{ -} +/** + * @author Simon André + */ +final class TypeMismatchException extends JsonPatchException {} diff --git a/src/JsonPatch.php b/src/JsonPatch.php index 055b1d2..0bd4f13 100644 --- a/src/JsonPatch.php +++ b/src/JsonPatch.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -58,12 +58,12 @@ public static function applyJson(string $documentJson, string $patchJson, int $j { $doc = json_decode($documentJson, true, flags: $jsonDecodeFlags); if (JSON_ERROR_NONE !== json_last_error()) { - throw new JsonPatchException('Invalid document JSON: '.json_last_error_msg()); + throw new JsonPatchException('Invalid document JSON: ' . json_last_error_msg()); } $patch = json_decode($patchJson, true, flags: $jsonDecodeFlags); if (JSON_ERROR_NONE !== json_last_error()) { - throw new JsonPatchException('Invalid patch JSON: '.json_last_error_msg()); + throw new JsonPatchException('Invalid patch JSON: ' . json_last_error_msg()); } if (!is_array($patch) || !array_is_list($patch)) { @@ -304,7 +304,7 @@ private static function opMove(mixed $document, Pointer $path, array $op, int $i // RFC 6902 Section 4.4: "from" must not be a proper prefix of "path" $pathStr = $path->toString(); - if ($pathStr !== $from && str_starts_with($pathStr, $from.'/')) { + if ($pathStr !== $from && str_starts_with($pathStr, $from . '/')) { throw new InvalidOperationException(sprintf('Operation %d (move): \'from\' cannot be a proper prefix of \'path\'.', $index)); } @@ -350,7 +350,7 @@ private static function opTest(mixed $document, Pointer $path, array $op, int $i $actual = $path->isRoot() ? $document : self::getAt($document, $path); if (!self::deepEquals($actual, $expected)) { - throw new TestFailedException('Test failed at path: '.$path->toString()); + throw new TestFailedException('Test failed at path: ' . $path->toString()); } return $document; @@ -417,7 +417,7 @@ private static function getAt(mixed $document, Pointer $pointer): mixed private static function readChild(mixed $current, string $segment, string $fullPath): mixed { if (!is_array($current)) { - throw new TypeMismatchException('Non-container encountered at: '.$fullPath); + throw new TypeMismatchException('Non-container encountered at: ' . $fullPath); } if (array_is_list($current)) { @@ -427,7 +427,7 @@ private static function readChild(mixed $current, string $segment, string $fullP } if (!array_key_exists($segment, $current)) { - throw new PathNotFoundException('Path does not exist: '.$fullPath); + throw new PathNotFoundException('Path does not exist: ' . $fullPath); } return $current[$segment]; @@ -840,9 +840,9 @@ private static function join(string $base, string $segment): string $seg = str_replace(['~', '/'], ['~0', '~1'], $segment); if ('' === $base) { - return '/'.$seg; + return '/' . $seg; } - return $base.'/'.$seg; + return $base . '/' . $seg; } } diff --git a/src/Pointer.php b/src/Pointer.php index 4001b6a..2706425 100644 --- a/src/Pointer.php +++ b/src/Pointer.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -123,7 +123,7 @@ public function toString(): string $encoded = array_map(self::encodeSegment(...), $this->segments); - return '/'.implode('/', $encoded); + return '/' . implode('/', $encoded); } private static function decodeSegment(string $segment): string diff --git a/tests/DiffOptionsTest.php b/tests/DiffOptionsTest.php index c61da3b..da57f42 100644 --- a/tests/DiffOptionsTest.php +++ b/tests/DiffOptionsTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/JsonPatchTest.php b/tests/JsonPatchTest.php index 61a4619..bd929c4 100644 --- a/tests/JsonPatchTest.php +++ b/tests/JsonPatchTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/PointerTest.php b/tests/PointerTest.php index fea750a..e5b3613 100644 --- a/tests/PointerTest.php +++ b/tests/PointerTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -138,7 +138,7 @@ public function testClearsOldestCacheEntryWhenMaxSizeExceeded(): void $maxCacheSize = (new \ReflectionClassConstant(Pointer::class, 'MAX_CACHE_SIZE'))->getValue(); for ($i = 0; $i < $maxCacheSize; ++$i) { - Pointer::parse('/entry'.$i); + Pointer::parse('/entry' . $i); } $this->assertCount($maxCacheSize, (new \ReflectionClass(Pointer::class))->getStaticPropertyValue('cache')); diff --git a/tests/Rfc6902ComplianceTest.php b/tests/Rfc6902ComplianceTest.php index 1e1c768..935e2b8 100644 --- a/tests/Rfc6902ComplianceTest.php +++ b/tests/Rfc6902ComplianceTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -56,7 +56,7 @@ public function testRfc6902Compliance( */ public static function provideSpecTests(): iterable { - yield from self::loadTestFile(__DIR__.'/fixtures/json-patch-tests/spec_tests.json', 'spec'); + yield from self::loadTestFile(__DIR__ . '/fixtures/json-patch-tests/spec_tests.json', 'spec'); } /** @@ -64,7 +64,7 @@ public static function provideSpecTests(): iterable */ public static function provideMainTests(): iterable { - yield from self::loadTestFile(__DIR__.'/fixtures/json-patch-tests/tests.json', 'main'); + yield from self::loadTestFile(__DIR__ . '/fixtures/json-patch-tests/tests.json', 'main'); } /**