Full Code of SoftCreatR/JSONPath for AI

main a07f7e4290cb cached
45 files
131.3 KB
33.9k tokens
144 symbols
1 requests
Download .txt
Repository: SoftCreatR/JSONPath
Branch: main
Commit: a07f7e4290cb
Files: 45
Total size: 131.3 KB

Directory structure:
gitextract_xceowo_u/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.md
│   │   ├── documentation.md
│   │   └── feature.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── diff.json
│   ├── php-syntax.json
│   └── workflows/
│       ├── Test.yml
│       ├── UpdateContributors.yml
│       └── codestyle.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│   ├── AccessHelper.php
│   ├── Filters/
│   │   ├── AbstractFilter.php
│   │   ├── IndexFilter.php
│   │   ├── IndexesFilter.php
│   │   ├── QueryMatchFilter.php
│   │   ├── QueryResultFilter.php
│   │   ├── RecursiveFilter.php
│   │   └── SliceFilter.php
│   ├── JSONPath.php
│   ├── JSONPathException.php
│   ├── JSONPathLexer.php
│   ├── JSONPathToken.php
│   └── TokenType.php
└── tests/
    ├── AccessHelperTest.php
    ├── IndexFilterTest.php
    ├── IndexesFilterTest.php
    ├── JSONPathCoreTest.php
    ├── JSONPathIntegrationTest.php
    ├── JSONPathLexerTest.php
    ├── JSONPathTokenTest.php
    ├── QueryMatchFilterTest.php
    ├── QueryResultFilterTest.php
    ├── RecursiveFilterTest.php
    └── SliceFilterTest.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
*.php diff=php


================================================
FILE: .github/FUNDING.yml
================================================
github: softcreatr
custom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4']


================================================
FILE: .github/ISSUE_TEMPLATE/bug.md
================================================
---
name: 🐛 Bug Report
about: Submit a bug report, to help us improve.
labels: "bug"
---

## 🐛 Bug Report

(A clear and concise description of what the bug is)

## Have you spent some time to check if this issue has been raised before?

[ ] I have read googled for a similar issue or checked our older issues for a similar bug

### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?

[ ] I have read the Code of Conduct

## To Reproduce

(Write your steps here:)

## Expected behavior

<!--
  How did you expect your project to behave?
  It’s fine if you’re not sure your understanding is correct.
  Write down what you thought would happen.
-->

(Write what you thought would happen.)

## Actual Behavior

<!--
  Did something go wrong?
  Is something broken, or not behaving as you expected?
  Describe this section in detail, and attach screenshots if possible.
  Don't only say "it doesn't work"!
-->

(Write what happened. Add screenshots, if applicable.)

## Your Environment

<!-- Include as many relevant details about the environment you experienced the bug in -->

(Write Environment, Operating system and version etc.)


================================================
FILE: .github/ISSUE_TEMPLATE/documentation.md
================================================
---
name: 📚 Documentation
about: Report an issue related to documentation.
labels: "documentation"
---

## 📚 Documentation

(A clear and concise description of what the issue is.)

### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?

[ ] I have read the Code of Conduct


================================================
FILE: .github/ISSUE_TEMPLATE/feature.md
================================================
---
name: 💡 Feature / Idea
about: Submit a proposal for a new feature.
labels: "feature"
---

## 💡 Feature / Idea

(A clear and concise description of what the feature is.)

## Have you spent some time to check if this issue has been raised before?

[ ] I have read googled for a similar issue or checked our older issues for a similar idea

### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?

[ ] I have read the Code of Conduct

## Pitch

(Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.)


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--
Thank you for sending the PR! We appreciate you spending the time to work on these changes.

Help us understand your motivation by explaining why you decided to make this change.

Happy contributing!

-->

# 🔀 Pull Request

## What does this PR do?

(Provide a description of what this PR does.)

## Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

## Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)


================================================
FILE: .github/diff.json
================================================
{
  "problemMatcher": [
    {
      "owner": "diff",
      "pattern": [
        {
          "regexp": "--- a/(.*)",
          "file": 1,
          "message": 1
        }
      ]
    }
  ]
}


================================================
FILE: .github/php-syntax.json
================================================
{
  "problemMatcher": [
    {
      "owner": "php -l",
      "pattern": [
        {
          "regexp": "^\\s*(PHP\\s+)?([a-zA-Z\\s]+):\\s+(.*)\\s+in\\s+(\\S+)\\s+on\\s+line\\s+(\\d+)$",
          "file": 4,
          "line": 5,
          "message": 3
        }
      ]
    }
  ]
}


================================================
FILE: .github/workflows/Test.yml
================================================
name: Test

on:
  push:
    paths:
      - '**.php'
      - 'composer.json'
    branches:
      - 'main'
  pull_request:
    paths:
      - '**.php'
      - 'composer.json'
  workflow_dispatch:

jobs:
  run:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php:
        - '8.5'
    name: PHP ${{ matrix.php }} Test

    steps:
      - name: Git checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: dom, json, mbstring, xml, xmlwriter
          tools: phpcs
          coverage: pcov
        env:
          COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup problem matchers for PHP syntax check
        run: echo "::add-matcher::.github/php-syntax.json"

      - run: |
          ! find . -type f -name '*.php' -exec php -l '{}' \; 2>&1 |grep -v '^No syntax errors detected'

      - name: Setup problem matchers for PHPUnit
        run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Run phpcs
        run: composer cs

      - name: Run PHPStan
        run: composer phpstan

      - name: Execute tests
        run: |
          set +e
          output=$(composer test -- --coverage-clover=coverage.xml 2>&1)
          exit_code=$?
          echo "$output"
          # If the only issue is the cache directory warning, ignore the exit code.
          if echo "$output" | grep -q "No cache directory configured, result of static analysis for code coverage will not be cached"; then
            echo "Ignoring known PHPUnit warning about missing cache directory."
            exit 0
          else
            exit $exit_code
          fi

      - name: Run codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}


================================================
FILE: .github/workflows/UpdateContributors.yml
================================================
name: Update Contributors

on: [ push, workflow_dispatch]

jobs:
  Update:
    runs-on: ubuntu-latest

    steps:
      - name: Git checkout
        uses: actions/checkout@v4

      - name: Update Contributors
        uses: BobAnkh/add-contributors@master
        with:
          REPO_NAME: 'SoftCreatR/JSONPath'
          CONTRIBUTOR: '## Contributors ✨'
          ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/codestyle.yml
================================================
name: Code Style

on:
  push:
    paths:
    - '**.php'
  pull_request:
    paths:
    - '**.php'

jobs:
  php:
    name: PHP
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Setup PHP with tools
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.5'
        extensions: dom, json, mbstring, xml, xmlwriter
        tools: cs2pr, phpcs, php-cs-fixer

    - name: phpcs
      run: phpcs -n -q --report=checkstyle | cs2pr

    - name: php-cs-fixer
      run: php-cs-fixer fix --dry-run --diff


================================================
FILE: .gitignore
================================================
.idea
.phpunit.result.cache
vendor
.php_cs.cache
composer.lock
composer.phar
.phpcs-cache


================================================
FILE: .php-cs-fixer.dist.php
================================================
<?php

use PhpCsFixer\Config;
use PhpCsFixer\Finder;

$finder = Finder::create()
    ->in(__DIR__)
    ->notPath('vendor');

return new Config()
    ->setRiskyAllowed(true)
    ->setRules([
        '@PSR1' => true,
        '@PSR2' => true,

        'array_push' => true,
        'backtick_to_shell_exec' => true,
        'no_alias_language_construct_call' => true,
        'no_mixed_echo_print' => true,
        'pow_to_exponentiation' => true,
        'random_api_migration' => true,

        'array_syntax' => ['syntax' => 'short'],
        'no_multiline_whitespace_around_double_arrow' => true,
        'no_trailing_comma_in_singleline_array' => true,
        'no_whitespace_before_comma_in_array' => true,
        'normalize_index_brace' => true,
        'whitespace_after_comma_in_array' => true,

        'non_printable_character' => ['use_escape_sequences_in_strings' => true],

        'lowercase_static_reference' => true,
        'magic_constant_casing' => true,
        'magic_method_casing' => true,
        'native_function_casing' => true,
        'native_function_type_declaration_casing' => true,

        'cast_spaces' => ['space' => 'none'],
        'lowercase_cast' => true,
        'no_unset_cast' => true,
        'short_scalar_cast' => true,

        'class_attributes_separation' => true,
        'no_blank_lines_after_class_opening' => true,
        'no_null_property_initialization' => true,
        'self_accessor' => true,
        'single_class_element_per_statement' => true,
        'single_trait_insert_per_statement' => true,

        'no_empty_comment' => true,
        'single_line_comment_style' => ['comment_types' => ['hash']],

        'native_constant_invocation' => ['strict' => false],

        'no_alternative_syntax' => true,
        'no_trailing_comma_in_list_call' => true,
        'no_unneeded_control_parentheses' => [
            'statements' => [
                'break',
                'clone',
                'continue',
                'echo_print',
                'return',
                'switch_case',
                'yield',
                'yield_from',
            ],
        ],
        'no_unneeded_curly_braces' => ['namespaces' => true],
        'switch_continue_to_break' => true,
        'trailing_comma_in_multiline' => ['elements' => ['arrays']],

        'function_typehint_space' => true,
        'lambda_not_used_import' => true,
        'native_function_invocation' => ['include' => ['@internal']],
        'no_unreachable_default_argument_value' => true,
        'nullable_type_declaration_for_default_null_value' => true,
        'return_type_declaration' => true,
        'static_lambda' => true,

        'fully_qualified_strict_types' => ['leading_backslash_in_global_namespace' => true],
        'no_leading_import_slash' => true,
        'no_unused_imports' => true,
        'ordered_imports' => [
            'imports_order' => ['class', 'function', 'const'],
            'sort_algorithm' => 'alpha',
        ],
        'blank_line_between_import_groups' => true,

        'declare_equal_normalize' => true,
        'dir_constant' => true,
        'explicit_indirect_variable' => true,
        'function_to_constant' => true,
        'is_null' => true,
        'no_unset_on_property' => true,

        'list_syntax' => ['syntax' => 'short'],

        'clean_namespace' => true,
        'no_leading_namespace_whitespace' => true,
        'single_blank_line_before_namespace' => true,

        'no_homoglyph_names' => true,

        'binary_operator_spaces' => true,
        'concat_space' => ['spacing' => 'one'],
        'increment_style' => ['style' => 'post'],
        'logical_operators' => true,
        'object_operator_without_whitespace' => true,
        'operator_linebreak' => true,
        'standardize_increment' => true,
        'standardize_not_equals' => true,
        'ternary_operator_spaces' => true,
        'ternary_to_elvis_operator' => true,
        'ternary_to_null_coalescing' => true,
        'unary_operator_spaces' => true,

        'no_useless_return' => true,
        'return_assignment' => true,

        'multiline_whitespace_before_semicolons' => true,
        'no_empty_statement' => true,
        'no_singleline_whitespace_before_semicolons' => true,
        'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],

        'escape_implicit_backslashes' => true,
        'explicit_string_variable' => true,
        'heredoc_to_nowdoc' => true,
        'no_binary_string' => true,
        'simple_to_complex_string_variable' => true,

        'array_indentation' => true,
        'blank_line_before_statement' => ['statements' => ['return', 'exit']],
        'compact_nullable_typehint' => true,
        'method_chaining_indentation' => true,
        'no_extra_blank_lines' => [
            'tokens' => [
                'case',
                'continue',
                'curly_brace_block',
                'default',
                'extra',
                'parenthesis_brace_block',
                'square_brace_block',
                'switch',
                'throw',
                'use',
            ],
        ],
        'no_spaces_around_offset' => true,
    ])
    ->setFinder($finder);


================================================
FILE: CHANGELOG.md
================================================
# Changelog

### 1.0.2
- Fixed tokenizer handling for quoted bracket keys containing `$` so literals like `['[$the.size$]']` remain atomic and do not split into root tokens.

### 1.0.1
- Aligned the query runner and lexer with the JSONPath comparison suite: JSON documents are now decoded as objects to preserve `{}` vs `[]`, unsupported selectors no longer abort the runner, and dot-notation now accepts quoted keys with dots/spaces/leading `@`.
- Hardened filter parsing: boolean-only filters (`?(true|false|null)`), literal short-circuiting (`&& false`, `|| true`), and empty filters now return the expected collections instead of throwing.
- Slice filters gracefully skip non-countable objects.

### 1.0.0
- Rebuilt the test suite from scratch: removed bulky baseline fixtures and added compact unit/integration coverage for every filter (index, union, query, recursive, slice), lexer edge cases, and JSONPath core helpers. Runs reflection-free and deprecation-free.
- Achieved and enforced 100% code coverage across AccessHelper, all filters, lexer, tokens, and JSONPath core while keeping phpstan and coding standards clean.
- Added a lightweight manual query runner with curated examples to exercise selectors quickly without external datasets.
- Major compatibility push toward the unofficial JSONPath standard: unions support slices/queries/wildcards, trailing commas parse correctly, negative indexes and bracket-escaped keys (quotes, brackets, wildcards, special chars) are honored, filters compare path-to-path and root references, equality/deep-equality/regex/in/nin semantics align with expectations, and null existence/value handling follows RFC behavior.
- New feature highlights from this cycle:
  - Multi-key unions with and without quotes: `$[name,year]` and `$["name","year"]`.
  - Robust bracket notation for special/escaped keys, including `']'`, `'*'`, `$`, backslashes, and mixed punctuation.
  - Trailing comma support in unions/slices (e.g. `$..books[0,1,2,]`).
  - Negative index handling aligned with spec (short arrays return empty; -1 works where valid).
  - Filter improvements: path-to-path/root comparisons, deep equality across scalars/objects/arrays/null/empties, regex matching, `in`/`nin`/`!in`, tautological expressions, and `?@` existence behavior per RFC.
  - Unions combining slices/queries/wildcards now return complete results (e.g. `$[1:3,4]`, `$[*,1]`).

### 0.11.0
🔻 Breaking changes ahead:

- Dropped support for PHP < 8.5
- `JSONPathToken` now uses a `TokenType` enum and the constructor signature changed accordingly.
- `JSONPath` options flag is now an `int` bitmask (was `bool`), requiring callers to pass integer flags.
- `SliceFilter` returns an empty result for non-positive step values (previously iterated indefinitely).
- `QueryResultFilter` now throws a `JSONPathException` for unsupported operators instead of silently proceeding.
- Access helper behavior is stricter: `arrayValues` throws on invalid types; ArrayAccess lookups check `offsetExists` before reading; traversables and objects are handled distinctly.
- Adopted PHP 8.5 features: `TokenType` enum, readonly value object for tokens, typed flags/options, and `#[\Override]` usage.
- CI now runs on PHP 8.5 with required extensions; code style workflow updated accordingly.
- Added coverage for AccessHelper edge cases (magic getters, ArrayAccess, traversables, negative indexes), QueryResultFilter arithmetic branches, and SliceFilter negative/null bounds.
- Fixed empty-expression handling in lexer and improved safety in AccessHelper traversable lookups.
- Added PHPStan static analysis to the toolchain and addressed its findings.

### 0.10.1
- Fixed ignore whitespace after comparison value in filter expression

### 0.10.0
- Fixed query/selector Filter Expression With Current Object
- Fixed query/selector Filter Expression With Different Grouped Operators
- Fixed query/selector Filter Expression With equals_on_array_of_numbers
- Fixed query/selector Filter Expression With Negation and Equals
- Fixed query/selector Filter Expression With Negation and Less Than
- Fixed query/selector Filter Expression Without Value
- Fixed query/selector Filter Expression With Boolean AND Operator (#42)
- Fixed query/selector Filter Expression With Boolean OR Operator (#43)
- Fixed query/selector Filter Expression With Equals (#45)
- Fixed query/selector Filter Expression With Equals false (#46)
- Fixed query/selector Filter Expression With Equals null (#47)
- Fixed query/selector Filter Expression With Equals Number With Fraction (#48)
- Fixed query/selector Filter Expression With Equals true (#50)
- Fixed query/selector Filter Expression With Greater Than (#52)
- Fixed query/selector Filter Expression With Greater Than or Equal (#53)
- Fixed query/selector Filter Expression With Less Than (#54)
- Fixed query/selector Filter Expression With Less Than or Equal (#55)
- Fixed query/selector Filter Expression With Not Equals (#56)
- Fixed query/selector Filter Expression With Value (#57)
- Fixed query/selector script_expression (Expected test result corrected)
- Added additional NULL related query tests from JSONPath RFC

### 0.9.0
🔻 Breaking changes ahead:

- Dropped support for PHP < 8.1

### 0.8.3
- Change `getData()` so that it can be mixed instead of array

### 0.8.2
- AccessHelper & RecursiveFilter now return a plain `object`, rather than an `ArrayAccess` object

### 0.8.1
- Removed strict_types
- Applied some PSR-12 related changes
- Small code optimizations

### 0.8.0
🔻 Breaking changes ahead:

 - Dropped support for PHP < 8.0
 - Removed deprecated method `JSONPath->data()`

### 0.7.5
 - Added support for $.length
 - Added trim to explode to support both 1,2,3 and 1, 2, 3 inputs
 - Dropped in_array strict equality check to be in line with the other standard equality checks such as (== and !=)

### 0.7.4
 - Removed PHPUnit from conflicting packages

### 0.7.3
 - Fixed PHP 7.4+ compatibility issues

### 0.7.2
 - Fixed query/selector "Array Slice With Start Large Negative Number And Open End On Short Array" (#7)
 - Fixed query/selector "Union With Keys" (#22)
 - Fixed query/selector "Dot Notation After Union With Keys" (#15)
 - Fixed query/selector "Union With Keys After Array Slice" (#23)
 - Fixed query/selector "Union With Keys After Bracket Notation" (#24)
 - Fixed query/selector "Union With Keys On Object Without Key" (#25)

### 0.7.1
 - Fixed issues with empty tokens (`['']` and `[""]`)
 - Fixed TypeError in AccessHelper::keyExists 
 - Improved QueryTest

### 0.7.0
🔻 Breaking changes ahead:

 - Made JSONPath::__construct final
 - Added missing type hints
 - Partially reduced complexity
 - Performed some code optimizations
 - Updated composer.json for proper PHPUnit/PHP usage
 - Added support for regular expression operator (`=~`)
 - Added QueryTest to perform tests against all queries from https://cburgmer.github.io/json-path-comparison/
 - Switched Code Style from PSR-2 to PSR-12

### 0.6.4
 - Removed unnecessary type casting, that caused problems under certain circumstances
 - Added support for `nin` operator
 - Added support for greater than or equal operator (`>=`)
 - Added support for less or equal operator (`<=`)

### 0.6.3
 - Added support for `in` operator
 - Fixed evaluation on indexed object

### 0.6.x
 - Dropped support for PHP < 7.1
 - Switched from (broken) PSR-0 to PSR-4
 - Updated PHPUnit to 8.5 / 9.4
 - Updated tests
 - Added missing PHPDoc blocks
 - Added return type hints
 - Moved from Travis to GitHub actions
 - Set `strict_types=1`

### 0.5.0
 - Fixed the slice notation (e.g. [0:2:5] etc.). **Breaks code relying on the broken implementation**

### 0.3.0
 - Added JSONPathToken class as value object
 - Lexer clean up and refactor
 - Updated the lexing and filtering of the recursive token ("..") to allow for a combination of recursion
   and filters, e.g. $..[?(@.type == 'suburb')].name

### 0.2.1 - 0.2.5
 - Various bug fixes and clean up

### 0.2.0
 - Added a heap of array access features for more creative iterating and chaining possibilities

### 0.1.x
 - Init


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity, expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at hello@1-2.dev. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


================================================
FILE: LICENSE.md
================================================
MIT License

Original work - Copyright (c) 2018 Flow Communications
Modified work - Copyright (c) 2020 Sascha Greuel <hello@1-2.dev>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# JSONPath for PHP 8.5+

[![Build](https://img.shields.io/github/actions/workflow/status/SoftCreatR/JSONPath/.github/workflows/Test.yml?branch=main)](https://github.com/SoftCreatR/JSONPath/actions/workflows/Test.yml) [![Latest Release](https://img.shields.io/packagist/v/SoftCreatR/JSONPath?color=blue&label=Latest%20Release)](https://packagist.org/packages/softcreatr/jsonpath)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Plant Tree](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Plant%20Tree&query=%24.total&url=https%3A%2F%2Fpublic.ecologi.com%2Fusers%2Fsoftcreatr%2Ftrees)](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4)
[![Codecov branch](https://img.shields.io/codecov/c/github/SoftCreatR/JSONPath)](https://codecov.io/gh/SoftCreatR/JSONPath)

This is a [JSONPath](http://goessner.net/articles/JsonPath/) implementation for PHP that targets the de facto comparison suite/RFC semantics while keeping the API small, cached, and `eval`-free.

## Highlights

- PHP 8.5+ only, with enums/readonly tokens and no `eval`.
- Works with arrays, objects, and `ArrayAccess`/traversables in any combination.
- Unions cover slices/queries/wildcards/multi-key strings (quoted or unquoted); negative indexes and escaped bracket notation are supported.
- Filters support path-to-path/root comparisons, regex, `in`/`nin`/`!in`, deep equality, RFC-style null existence/value handling, and literal-only short-circuiting (e.g., `?(true)`, `?(false)`, `&& false`, `|| true`).
- Tokenized parsing with internal caching; lightweight manual runner to try bundled examples quickly.

## Installation

Requires PHP 8.5 or newer.

```bash
composer require softcreatr/jsonpath:"^1.0"
```

## Development

Useful commands:

```bash
composer exec phpunit
composer phpstan
composer cs
```

## JSONPath Examples

JSONPath                  | Result
--------------------------|-------------------------------------
`$.store.books[*].author` | the authors of all books in the store
`$..author`               | all authors
`$.store..price`          | the price of everything in the store.
`$..books[2]`             | the third book
`$..books[(@.length-1)]`  | the last book in order.
`$..books[-1:]`           | the last book in order.
`$..books[0,1]`           | the first two books
`$..books[title,year]`    | multiple keys in a union
`$..books[:2]`            | the first two books
`$..books[::2]`           | every second book starting from first one
`$..books[1:6:3]`         | every third book starting from 1 till 6
`$..books[?(@.isbn)]`     | filter all books with isbn number
`$..books[?(@.price<10)]` | filter all books cheaper than 10
`$..books.length`         | the amount of books
`$..*`                    | all elements in the data (recursively extracted)


Expression syntax
---

Symbol                | Description
----------------------|-------------------------
`$`                   | The root object/element (not strictly necessary)
`@`                   | The current object/element
`.` or `[]`           | Child operator
`..`                  | Recursive descent
`*`                   | Wildcard. All child elements regardless their index.
`[,]`                 | Array indices as a set
`[start:end:step]`    | Array slice operator borrowed from ES4/Python.
`?()`                 | Filters a result set by a comparison expression (constant expressions like `?(true)`/`?(false)` are allowed; unsupported/empty filters evaluate to an empty result)
`()`                  | Uses the result of a comparison expression as the index

## PHP Usage

#### Using arrays

```php
<?php
require_once __DIR__ . '/vendor/autoload.php';

$data = ['people' => [
    ['name' => 'Sascha'],
    ['name' => 'Bianca'],
    ['name' => 'Alexander'],
    ['name' => 'Maximilian'],
]];

print_r((new \Flow\JSONPath\JSONPath($data))->find('$.people.*.name')->getData());

/*
Array
(
    [0] => Sascha
    [1] => Bianca
    [2] => Alexander
    [3] => Maximilian
)
*/
```

#### Using objects

```php
<?php
require_once __DIR__ . '/vendor/autoload.php';

$data = json_decode('{"name":"Sascha Greuel","birthdate":"1987-12-16","city":"Gladbeck","country":"Germany"}', false);

print_r((new \Flow\JSONPath\JSONPath($data))->find('$')->getData()[0]);

/*
stdClass Object
(
    [name] => Sascha Greuel
    [birthdate] => 1987-12-16
    [city] => Gladbeck
    [country] => Germany
)
*/
```

### Magic method access

The options flag `JSONPath::ALLOW_MAGIC` will instruct JSONPath when retrieving a value to first check if an object
has a magic `__get()` method and will call this method if available. This feature is *iffy* and
not very predictable as:

-  wildcard and recursive features will only look at public properties and can't smell which properties are magically accessible
-  there is no `property_exists` check for magic methods so an object with a magic `__get()` will always return `true` when checking
   if the property exists
-   any errors thrown or unpredictable behavior caused by fetching via `__get()` is your own problem to deal with

```php
<?php

use Flow\JSONPath\JSONPath;

$myObject = (new Foo())->get('bar');
$jsonPath = new JSONPath($myObject, JSONPath::ALLOW_MAGIC);
```

## Script expressions

Script execution is intentionally **not** supported:

- It would require `eval`, which we avoid.
- Behavior would diverge across languages and defeat having a portable expression syntax.

Supported filter/query patterns (200+ cases covered in the comparison suite):

```
[?(@._KEY_ _OPERATOR_ _VALUE_)]
  Operators: ==, =, !=, <>, !==, <, >, <=, >=, =~, in, nin, !in

Examples:
[?(@.title == "A string")]      // equality
[?(@.title = "A string")]       // SQL-style equals
[?(@.price < 10)]               // numeric comparisons
[?(@.title =~ /^a(nother)?/i)]  // regex
[?(@.title in ["A","B"])]       // membership
[?(@.title nin ["A"])]          // not in
[?(@.title !in ["A"])]          // alternate not in
[?(@.key == @.other)]           // path-to-path comparison
[?(@.key == $.rootValue)]       // root reference
[?(@)] or [?(@==@)]             // truthy/tautology
[?(@.length)]                   // existence checks
[?(@['weird-key']=="ok")]       // bracket-escaped keys and negative indexes
```

A full list of (un)supported filter/query patterns can be found in the [JSONPath Comparison Cheatsheet](https://cburgmer.github.io/json-path-comparison/).
	
## Similar projects

[FlowCommunications/JSONPath](https://github.com/FlowCommunications/JSONPath) is the predecessor of this library by Stephen Frank

Other / Similar implementations can be found in the [Wiki](https://github.com/SoftCreatR/JSONPath/wiki/Other-Implementations).

## Changelog

A list of changes can be found in the [CHANGELOG.md](CHANGELOG.md) file. 

## License 🌳

[MIT](LICENSE.md) © [1-2.dev](https://1-2.dev)

This package is Treeware. If you use it in production, then we ask that you [**buy the world a tree**](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4) to thank us for our work. By contributing to the ecologi project, you’ll be creating employment for local families and restoring wildlife habitats.

## Contributors ✨

<table>
<tr>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/SoftCreatR>
            <img src=https://avatars.githubusercontent.com/u/81188?v=4 width="100;"  alt=Sascha Greuel/>
            <br />
            <sub style="font-size:14px"><b>Sascha Greuel</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/lucasnetau>
            <img src=https://avatars.githubusercontent.com/u/9331242?v=4 width="100;"  alt=James Lucas/>
            <br />
            <sub style="font-size:14px"><b>James Lucas</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/Schrank>
            <img src=https://avatars.githubusercontent.com/u/379680?v=4 width="100;"  alt=Fabian Blechschmidt/>
            <br />
            <sub style="font-size:14px"><b>Fabian Blechschmidt</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/mpesari>
            <img src=https://avatars.githubusercontent.com/u/11061725?v=4 width="100;"  alt=Mikko Pesari/>
            <br />
            <sub style="font-size:14px"><b>Mikko Pesari</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/warlof>
            <img src=https://avatars.githubusercontent.com/u/648753?v=4 width="100;"  alt=warlof/>
            <br />
            <sub style="font-size:14px"><b>warlof</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/SG5>
            <img src=https://avatars.githubusercontent.com/u/3931761?v=4 width="100;"  alt=Sergey G/>
            <br />
            <sub style="font-size:14px"><b>Sergey G</b></sub>
        </a>
    </td>
</tr>
<tr>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/drealecs>
            <img src=https://avatars.githubusercontent.com/u/209984?v=4 width="100;"  alt=Alexandru Pătrănescu/>
            <br />
            <sub style="font-size:14px"><b>Alexandru Pătrănescu</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/oleg-andreyev>
            <img src=https://avatars.githubusercontent.com/u/1244112?v=4 width="100;"  alt=Oleg Andreyev/>
            <br />
            <sub style="font-size:14px"><b>Oleg Andreyev</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/rcjsuen>
            <img src=https://avatars.githubusercontent.com/u/15629116?v=4 width="100;"  alt=Remy Suen/>
            <br />
            <sub style="font-size:14px"><b>Remy Suen</b></sub>
        </a>
    </td>
    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
        <a href=https://github.com/esomething>
            <img src=https://avatars.githubusercontent.com/u/64032?v=4 width="100;"  alt=esomething/>
            <br />
            <sub style="font-size:14px"><b>esomething</b></sub>
        </a>
    </td>
</tr>
</table>


================================================
FILE: composer.json
================================================
{
  "name": "softcreatr/jsonpath",
  "description": "JSONPath implementation for parsing, searching and flattening arrays",
  "license": "MIT",
  "version": "1.0.2",
  "authors": [
    {
      "name": "Stephen Frank",
      "email": "stephen@flowsa.com",
      "homepage": "https://prismaticbytes.com",
      "role": "Developer"
    },
    {
      "name": "Sascha Greuel",
      "email": "hello@1-2.dev",
      "homepage": "https://1-2.dev",
      "role": "Developer"
    }
  ],
  "support": {
    "email": "hello@1-2.dev",
    "issues": "https://github.com/SoftCreatR/JSONPath/issues",
    "forum": "https://github.com/SoftCreatR/JSONPath/discussions",
    "source": "https://github.com/SoftCreatR/JSONPath"
  },
  "require": {
    "php": "^8.5",
    "ext-json": "*"
  },
  "require-dev": {
    "friendsofphp/php-cs-fixer": "^3.92",
    "phpstan/phpstan": "^2.1",
    "phpunit/phpunit": "^12",
    "squizlabs/php_codesniffer": "^4.0"
  },
  "replace": {
    "flow/jsonpath": "*"
  },
  "minimum-stability": "stable",
  "autoload": {
    "psr-4": {
      "Flow\\JSONPath\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Flow\\JSONPath\\Test\\": "tests/"
    }
  },
  "config": {
    "optimize-autoloader": true,
    "preferred-install": "dist"
  },
  "scripts": {
    "cs": "phpcs",
    "cs-fix": "php-cs-fixer fix --config=.php-cs-fixer.dist.php",
    "phpstan": "phpstan analyse --no-progress -c phpstan.neon.dist",
    "test": "phpunit"
  }
}


================================================
FILE: phpcs.xml
================================================
<?xml version="1.0"?>
<ruleset
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd"
>
    <file>src/</file>
    <file>tests/</file>
    <arg name="extensions" value="php" />
    <arg value="p"/>
    <arg name="basepath" value="."/>

    <rule ref="PSR12">
        <!-- https://github.com/squizlabs/PHP_CodeSniffer/issues/3200 -->
        <exclude name="PSR12.Classes.AnonClassDeclaration.SpaceAfterKeyword"/>

        <!-- We have a large number of comments between the closing brace of an `if` and the `else`. -->
        <exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace"/>
    </rule>
</ruleset>


================================================
FILE: phpstan.neon.dist
================================================
parameters:
    treatPhpDocTypesAsCertain: false
    level: 6
    paths:
        - src
        - tests
    tmpDir: .phpstan-cache


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
  <testsuites>
    <testsuite name="Unit">
      <directory>./tests</directory>
    </testsuite>
  </testsuites>

  <source>
    <include>
      <directory>./src/</directory>
    </include>
  </source>
</phpunit>


================================================
FILE: src/AccessHelper.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

use ArrayAccess;
use Traversable;

class AccessHelper
{
    /**
     * @return array<int, int|string>
     */
    public static function collectionKeys(mixed $collection): array
    {
        if (\is_object($collection)) {
            return \array_keys(\get_object_vars($collection));
        }

        return \array_keys($collection);
    }

    public static function isCollectionType(mixed $collection): bool
    {
        return \is_array($collection) || \is_object($collection);
    }

    public static function keyExists(mixed $collection, int|string|null $key, bool $magicIsAllowed = false): bool
    {
        if ($magicIsAllowed && \is_object($collection) && \method_exists($collection, '__get')) {
            return true;
        }

        if (\is_array($collection)) {
            if (\is_int($key) && $key < 0) {
                $keys = \array_keys($collection);
                $index = \count($keys) + $key;

                return $index >= 0 && \array_key_exists($index, $keys);
            }

            return \array_key_exists($key ?? '', $collection);
        }

        if ($collection instanceof ArrayAccess) {
            return $collection->offsetExists($key);
        }

        if (\is_object($collection)) {
            return \property_exists($collection, (string)$key);
        }

        return false;
    }

    /**
     * @todo Optimize conditions
     */
    public static function getValue(mixed $collection, int|string|null $key, bool $magicIsAllowed = false): mixed
    {
        if (
            $magicIsAllowed
            && \is_object($collection)
            && !$collection instanceof ArrayAccess && \method_exists($collection, '__get')
        ) {
            $return = $collection->__get($key);
        } elseif (\is_int($key) && $collection instanceof Traversable && !$collection instanceof ArrayAccess) {
            $return = self::getValueByIndex($collection, $key);
        } elseif (\is_object($collection) && !$collection instanceof ArrayAccess) {
            $return = $collection->{$key};
        } elseif ($collection instanceof ArrayAccess) {
            $return = $collection->offsetExists($key) ? $collection->offsetGet($key) : null;
        } elseif (\is_array($collection)) {
            if (\is_int($key) && $key < 0) {
                $index = \count($collection) + $key;
                $return = $index >= 0 && \array_key_exists($index, $collection) ? $collection[$index] : null;
            } else {
                $return = $collection[$key] ?? null;
            }
        } else {
            $return = null;
        }

        return $return;
    }

    /**
     * Find item in php collection by index
     * Written this way to handle instances ArrayAccess or Traversable objects
     */
    private static function getValueByIndex(mixed $collection, int $key): mixed
    {
        $i = 0;

        foreach ($collection as $val) {
            if ($i === $key) {
                return $val;
            }

            $i++;
        }

        if ($key < 0) {
            $total = $i;
            $i = 0;

            foreach ($collection as $val) {
                if ($i - $total === $key) {
                    return $val;
                }

                $i++;
            }
        }

        return null;
    }

    public static function setValue(mixed &$collection, int|string|null $key, mixed $value): mixed
    {
        if (\is_object($collection) && !$collection instanceof ArrayAccess) {
            $collection->{$key} = $value;

            return $value;
        }

        if ($collection instanceof ArrayAccess) {
            $collection->offsetSet($key, $value);

            return $value;
        }

        $collection[$key] = $value;

        return $value;
    }

    public static function unsetValue(mixed &$collection, int|string|null $key): void
    {
        if (\is_object($collection) && !$collection instanceof ArrayAccess) {
            unset($collection->{$key});
        }

        if ($collection instanceof ArrayAccess) {
            $collection->offsetUnset($key);
        }

        if (\is_array($collection)) {
            unset($collection[$key]);
        }
    }

    /**
     * @throws JSONPathException
     */
    /**
     * @return array<int, mixed>
     * @throws JSONPathException
     */
    public static function arrayValues(mixed $collection): array
    {
        if (\is_array($collection)) {
            return \array_values($collection);
        }

        if (\is_object($collection)) {
            return \array_values((array)$collection);
        }

        throw new JSONPathException('Invalid variable type for arrayValues');
    }
}


================================================
FILE: src/Filters/AbstractFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathToken;

abstract class AbstractFilter
{
    protected bool $magicIsAllowed;

    protected mixed $rootData = null;

    public function __construct(protected JSONPathToken $token, int $options = 0)
    {
        $this->magicIsAllowed = ($options & JSONPath::ALLOW_MAGIC) === JSONPath::ALLOW_MAGIC;
    }

    public function setRootData(mixed $root): void
    {
        $this->rootData = $root;
    }

    /**
     * @param array<array-key, mixed>|object $collection
     * @return array<array-key, mixed>
     */
    abstract public function filter(array|object $collection): array;
}


================================================
FILE: src/Filters/IndexFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPathException;

class IndexFilter extends AbstractFilter
{
    /**
     * @throws JSONPathException
     * @inheritDoc
     */
    public function filter(array|object $collection): array
    {
        if (\is_array($this->token->value)) {
            $result = [];

            foreach ($this->token->value as $value) {
                if (AccessHelper::keyExists($collection, $value, $this->magicIsAllowed)) {
                    $result[] = AccessHelper::getValue($collection, $value, $this->magicIsAllowed);
                }
            }

            return $result;
        }

        if (AccessHelper::keyExists($collection, $this->token->value, $this->magicIsAllowed)) {
            return [
                AccessHelper::getValue($collection, $this->token->value, $this->magicIsAllowed),
            ];
        }

        if ($this->token->value === '*' && !$this->token->quoted) {
            return AccessHelper::arrayValues($collection);
        }

        if ($this->token->value === 'length') {
            return [
                \count($collection),
            ];
        }

        return [];
    }
}


================================================
FILE: src/Filters/IndexesFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;

class IndexesFilter extends AbstractFilter
{
    /**
     * @inheritDoc
     *
     * @throws JSONPathException
     */
    public function filter(array|object $collection): array
    {
        $return = [];

        foreach ($this->token->value as $index) {
            if (\is_array($index) && ($index['type'] ?? null) === 'slice') {
                $sliceToken = new JSONPathToken(TokenType::Slice, $index['value']);
                $sliceFilter = new SliceFilter(
                    $sliceToken,
                    $this->magicIsAllowed ? JSONPath::ALLOW_MAGIC : 0
                );
                $sliceFilter->setRootData($this->rootData ?? $collection);

                $return = \array_merge($return, $sliceFilter->filter($collection));

                continue;
            }

            if (\is_array($index) && ($index['type'] ?? null) === 'query') {
                $queryToken = new JSONPathToken(TokenType::QueryMatch, $index['value']);
                $queryFilter = new QueryMatchFilter(
                    $queryToken,
                    $this->magicIsAllowed ? JSONPath::ALLOW_MAGIC : 0
                );
                $queryFilter->setRootData($this->rootData ?? $collection);

                $return = \array_merge($return, $queryFilter->filter($collection));

                continue;
            }

            if ($index === '*' && !$this->token->quoted) {
                $return = \array_merge($return, AccessHelper::arrayValues($collection));

                continue;
            }

            if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) {
                $return[] = AccessHelper::getValue($collection, $index, $this->magicIsAllowed);
            }
        }

        return $return;
    }
}


================================================
FILE: src/Filters/QueryMatchFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use JsonException;
use RuntimeException;

use const JSON_THROW_ON_ERROR;
use const PREG_OFFSET_CAPTURE;
use const PREG_UNMATCHED_AS_NULL;

class QueryMatchFilter extends AbstractFilter
{
    protected const string MATCH_QUERY_NEGATION_WRAPPED = '^(?<negate>!)\((?<logicalexpr>.+)\)$';

    protected const string MATCH_QUERY_NEGATION_UNWRAPPED = '^(?<negate>!)(?<logicalexpr>.+)$';

    protected const string MATCH_QUERY_OPERATORS = '
      (@\.(?<key>[^\s<>!=]+)|@\[["\']?(?<keySquare>.*?)["\']?\]|(?<node>@)|(%group(?<group>\d+)%))
      (\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?<comparisonValue>.+?(?=\s*(?:&&|$|\|\||%))))?
      (\s*(?<logicalandor>&&|\|\|)\s*)?
    ';

    protected const string MATCH_GROUPED_EXPRESSION = '#\([^)(]*+(?:(?R)[^)(]*)*+\)#';

    /**
     * @throws JSONPathException
     * @inheritDoc
     */
    public function filter(array|object $collection): array
    {
        $filterExpression = $this->token->value;
        $isShorthand = $this->token->shorthand ?? false;

        if (\is_array($filterExpression)) {
            $isShorthand = $filterExpression['shorthand'] ?? $isShorthand;
            $filterExpression = $filterExpression['expression'] ?? '';
        }

        $negateFilter = false;

        if (
            \preg_match('/' . static::MATCH_QUERY_NEGATION_WRAPPED . '/x', $filterExpression, $negationMatches)
            || \preg_match('/' . static::MATCH_QUERY_NEGATION_UNWRAPPED . '/x', $filterExpression, $negationMatches)
        ) {
            $negateFilter = true;
            $filterExpression = $negationMatches['logicalexpr'];
        }

        $literalResult = $this->evaluateLiteralExpression($filterExpression, $collection);

        if ($literalResult !== null) {
            return $literalResult;
        }

        $shortCircuitResult = $this->evaluateExpressionWithTrailingLiteral($filterExpression, $collection);

        if ($shortCircuitResult !== null) {
            return $shortCircuitResult;
        }

        $filterGroups = [];

        if (
            \preg_match_all(
                static::MATCH_GROUPED_EXPRESSION,
                $filterExpression,
                $matches,
                PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL
            )
        ) {
            foreach ($matches[0] as $i => $matchesGroup) {
                $test = \substr($matchesGroup[0], 1, -1);

                //sanity check that our group is a group and not something within a string or regular expression
                if (\preg_match('/' . static::MATCH_QUERY_OPERATORS . '/x', $test)) {
                    $filterGroups[$i] = $test;
                    $filterExpression = \str_replace($matchesGroup[0], "%group{$i}%", $filterExpression);
                }
            }
        }

        $match = \preg_match_all(
            '/' . static::MATCH_QUERY_OPERATORS . '/x',
            $filterExpression,
            $matches,
            PREG_UNMATCHED_AS_NULL
        );

        if (
            $match === false
            || !isset($matches[1][0])
            || isset($matches['logicalandor'][\array_key_last($matches['logicalandor'])])
        ) {
            $constantResult = $this->evaluateConstantExpression($filterExpression);

            if ($constantResult !== null) {
                return $constantResult ? AccessHelper::arrayValues($collection) : [];
            }

            throw new RuntimeException('Malformed filter query');
        }

        $return = [];
        $matchCount = \count($matches[0]);

        for ($expressionPart = 0; $expressionPart < $matchCount; $expressionPart++) {
            $filteredCollection = $collection;
            $logicalJoin = $expressionPart > 0 ? $matches['logicalandor'][$expressionPart - 1] : null;

            if ($logicalJoin === '&&') {
                //Restrict the nodes we need to look at to those already meeting criteria
                $filteredCollection = $return;
                $return = [];
            }

            //Processing a group
            if ($matches['group'][$expressionPart] !== null) {
                $filter = '$[?(' . $filterGroups[$matches['group'][$expressionPart]] . ')]';
                $resolve = new JSONPath($filteredCollection)->find($filter)->getData();
                $return = $resolve;

                continue;
            }

            //Process a normal expression
            $key = $this->normalizeKey($matches['key'][$expressionPart] ?: $matches['keySquare'][$expressionPart]);

            $operator = $matches['operator'][$expressionPart] ?? null;
            $comparisonValue = $matches['comparisonValue'][$expressionPart] ?? null;
            $comparisonIsPath = $this->isPathComparison($comparisonValue);
            $canCompareMissing = \in_array($operator, ['=', '==', '!=', '!==', '<>'], true) && $comparisonIsPath;

            if (\is_string($comparisonValue)) {
                $comparisonValue = \preg_replace('/^\'/', '"', $comparisonValue);
                $comparisonValue = \preg_replace('/\'$/', '"', $comparisonValue);

                try {
                    $comparisonValue = \json_decode($comparisonValue, true, 512, JSON_THROW_ON_ERROR);
                } catch (JsonException) {
                    //Leave $comparisonValue as raw (e.g. regular express or non quote wrapped string)
                }
            }

            foreach ($filteredCollection as $nodeIndex => $node) {
                if ($logicalJoin === '||' && \array_key_exists($nodeIndex, $return)) {
                    //Short-circuit, node already exists in output due to previous test
                    continue;
                }

                $selectedNode = null;
                $notNothing = AccessHelper::keyExists($node, $key, $this->magicIsAllowed);

                if ($key) {
                    if ($notNothing) {
                        $selectedNode = AccessHelper::getValue($node, $key, $this->magicIsAllowed);
                    } elseif (\str_contains($key, '.')) {
                        $foundValue = new JSONPath($node)->find($key)->getData();

                        if ($foundValue) {
                            $selectedNode = $foundValue[0];
                            $notNothing = true;
                        }
                    } elseif ($canCompareMissing) {
                        $notNothing = true;
                    }
                } else {
                    //Node selection was plain @
                    $selectedNode = $node;
                    $notNothing = true;
                }

                $comparisonResult = null;

                if ($notNothing) {
                    $resolvedComparisonValue = $this->resolveComparisonValue($comparisonValue, $node);
                    $comparisonResult = false;

                    switch ($operator) {
                        case null:
                            if ($key === '' || $key === null) {
                                $comparisonResult = !$isShorthand || $this->isTruthy($selectedNode);
                            } else {
                                $comparisonResult = AccessHelper::keyExists($node, $key, $this->magicIsAllowed)
                                        || (!$key);
                            }
                            break;
                        case "=":
                        case "==":
                            $comparisonResult = $this->compareEquals($selectedNode, $resolvedComparisonValue);
                            break;
                        case "!=":
                        case "!==":
                        case "<>":
                            $comparisonResult = !$this->compareEquals($selectedNode, $resolvedComparisonValue);
                            break;
                        case '=~':
                            $comparisonResult = @\preg_match(
                                (string)$resolvedComparisonValue,
                                (string)$selectedNode
                            );
                            break;
                        case '<':
                            $comparisonResult = $this->compareLessThan($selectedNode, $resolvedComparisonValue);
                            break;
                        case '<=':
                            $comparisonResult = $this->compareLessThan($selectedNode, $resolvedComparisonValue)
                                || $this->compareEquals($selectedNode, $resolvedComparisonValue);
                            break;
                        case '>':
                            //rfc semantics
                            $comparisonResult = $this->compareLessThan($resolvedComparisonValue, $selectedNode);
                            break;
                        case '>=':
                            //rfc semantics
                            $comparisonResult = $this->compareLessThan($resolvedComparisonValue, $selectedNode)
                                || $this->compareEquals($selectedNode, $resolvedComparisonValue);
                            break;
                        case "in":
                            $comparisonResult = \is_array($resolvedComparisonValue)
                                && \in_array($selectedNode, $resolvedComparisonValue, true);
                            break;
                        case 'nin':
                        case "!in":
                            $comparisonResult = \is_array($resolvedComparisonValue)
                                && !\in_array($selectedNode, $resolvedComparisonValue, true);
                            break;
                    }
                }

                if ($negateFilter) {
                    $comparisonResult = !$comparisonResult;
                }

                if ($comparisonResult) {
                    $return[$nodeIndex] = $node;
                }
            }
        }

        //Keep out returned nodes in the same order they were defined in the original collection
        \ksort($return);

        return $return;
    }

    protected function isNumber(mixed $value): bool
    {
        return !\is_string($value) && \is_numeric($value);
    }

    /**
     * @throws JSONPathException
     */
    private function resolveComparisonValue(mixed $comparisonValue, mixed $node): mixed
    {
        if (!\is_string($comparisonValue)) {
            return $comparisonValue;
        }

        if (\str_starts_with($comparisonValue, '@')) {
            $path = \substr($comparisonValue, 1);

            if ($path === '' || $path === '.') {
                return $node;
            }

            $resolved = new JSONPath($node)->find($path)->getData();

            return \is_array($resolved) && \array_key_exists(0, $resolved) ? $resolved[0] : null;
        }

        if (\str_starts_with($comparisonValue, '$')) {
            $root = $this->rootData ?? $node;
            $resolved = new JSONPath($root)->find($comparisonValue)->getData();

            return \is_array($resolved) && \array_key_exists(0, $resolved) ? $resolved[0] : null;
        }

        return $comparisonValue;
    }

    private function normalizeKey(mixed $key): int|string|null
    {
        if (\is_string($key) && \preg_match('/^-?\d+$/', $key)) {
            return (int)$key;
        }

        return $key;
    }

    private function isPathComparison(mixed $comparisonValue): bool
    {
        return \is_string($comparisonValue) && \str_starts_with($comparisonValue, '@');
    }

    private function evaluateConstantExpression(string $expression): ?bool
    {
        $pattern = '/^\s*(?<left>[^&|]+?)\s*(?<operator>==|=|!=|!==|<>|<=|>=|<|>)\s*(?<right>[^&|]+?)\s*$/';

        if (!\preg_match($pattern, $expression, $matches)) {
            return null;
        }

        $left = $this->decodeLiteral($matches['left']);
        $right = $this->decodeLiteral($matches['right']);
        $operator = $matches['operator'];

        return match ($operator) {
            '==', '=' => $this->compareEquals($left, $right),
            '!=', '!==', '<>' => !$this->compareEquals($left, $right),
            '<' => $this->compareLessThan($left, $right),
            '<=' => $this->compareLessThan($left, $right) || $this->compareEquals($left, $right),
            '>' => $this->compareLessThan($right, $left),
            '>=' => $this->compareLessThan($right, $left) || $this->compareEquals($left, $right),
        };
    }

    /**
     * @param array<int, mixed>|object $collection
     * @return array<int, mixed>|null
     * @throws JSONPathException
     */
    private function evaluateLiteralExpression(string $expression, array|object $collection): ?array
    {
        $trimmed = \trim($expression);

        if ($trimmed === '') {
            return [];
        }

        $literalValue = $this->decodeLiteral($trimmed);
        $literalIsBool = \is_bool($literalValue);

        if (!$literalIsBool && $literalValue !== null) {
            return null;
        }

        return $this->isTruthy($literalValue) ? AccessHelper::arrayValues($collection) : [];
    }

    /**
     * @param array<int, mixed>|object $collection
     * @return array<int, mixed>|null
     * @throws JSONPathException
     */
    private function evaluateExpressionWithTrailingLiteral(
        string $expression,
        array|object $collection
    ): ?array {
        if (
            !\preg_match(
                '/^(?<left>.+?)\s*(?<op>&&|\|\|)\s*(?<literal>true|false|null)\s*$/i',
                $expression,
                $matches
            )
        ) {
            return null;
        }

        $leftFilter = '$[?(' . $matches['left'] . ')]';
        $leftResult = new JSONPath($collection)->find($leftFilter)->getData();
        $literalValue = $this->decodeLiteral($matches['literal']);
        $literalIsTrue = $this->isTruthy($literalValue);

        return match ($matches['op']) {
            '&&' => $literalIsTrue ? $leftResult : [],
            '||' => $literalIsTrue ? AccessHelper::arrayValues($collection) : $leftResult,
            default => [],
        };
    }

    private function decodeLiteral(string $literal): mixed
    {
        $literal = \trim($literal);

        try {
            return \json_decode($literal, true, 512, \JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            if (\is_numeric($literal)) {
                return $literal + 0;
            }

            return $literal;
        }
    }

    private function isTruthy(mixed $value): bool
    {
        return (bool)$value;
    }

    protected function compareEquals(mixed $a, mixed $b): bool
    {
        $type_a = \gettype($a);
        $type_b = \gettype($b);

        if ($type_a === $type_b || ($this->isNumber($a) && $this->isNumber($b))) {
            //Primitives or Numbers
            if ($a === null || \is_scalar($a)) {
                /** @noinspection TypeUnsafeComparisonInspection */
                return $a == $b;
            }

            if (\is_array($a) && \is_array($b)) {
                return $this->deepEqual($a, $b);
            }

            if (\is_object($a) && \is_object($b)) {
                return $this->deepEqual((array)$a, (array)$b);
            }
        }

        return false;
    }

    /**
     * @param array<array-key, mixed> $a
     * @param array<array-key, mixed> $b
     */
    private function deepEqual(array $a, array $b): bool
    {
        $aIsList = \array_is_list($a);
        $bIsList = \array_is_list($b);

        if ($aIsList !== $bIsList) {
            return false;
        }

        if (\count($a) !== \count($b)) {
            return false;
        }

        if ($aIsList) {
            return \array_all($a, fn ($value, $index) => \array_key_exists($index, $b)
                && $this->compareEquals($value, $b[$index]));
        }

        return \array_all($a, fn ($value, $key) => \array_key_exists($key, $b)
            && $this->compareEquals($value, $b[$key]));
    }

    protected function compareLessThan(mixed $a, mixed $b): bool
    {
        if ((\is_string($a) && \is_string($b)) || ($this->isNumber($a) && $this->isNumber($b))) {
            //numerical and string comparison supported only
            return $a < $b;
        }

        return false;
    }
}


================================================
FILE: src/Filters/QueryResultFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPathException;

class QueryResultFilter extends AbstractFilter
{
    /**
     * @throws JSONPathException
     * @inheritDoc
     */
    public function filter(array|object $collection): array
    {
        if (!\preg_match('/@\.(?<key>\w+)\s*(?<operator>[-+*\/])\s*(?<numeric>\d+)/', $this->token->value, $matches)) {
            throw new JSONPathException('Unsupported operator in expression');
        }

        $matchKey = $matches['key'];

        if (AccessHelper::keyExists($collection, $matchKey, $this->magicIsAllowed)) {
            $value = AccessHelper::getValue($collection, $matchKey, $this->magicIsAllowed);
        } elseif ($matches['key'] === 'length') {
            $value = \count($collection);
        } else {
            return [];
        }

        $resultKey = match ($matches['operator']) {
            '+' => $value + $matches['numeric'],
            '*' => $value * $matches['numeric'],
            '-' => $value - $matches['numeric'],
            '/' => $value / $matches['numeric'],
        };

        $result = [];

        if (AccessHelper::keyExists($collection, $resultKey, $this->magicIsAllowed)) {
            $result[] = AccessHelper::getValue($collection, $resultKey, $this->magicIsAllowed);
        }

        return $result;
    }
}


================================================
FILE: src/Filters/RecursiveFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPathException;

class RecursiveFilter extends AbstractFilter
{
    /**
     * @inheritDoc
     *
     * @throws JSONPathException
     */
    public function filter(array|object $collection): array
    {
        $result = [];

        $this->recurse($result, $collection);

        return $result;
    }

    /**
     * @param array<int, array<array-key, mixed>> $result
     * @param array<array-key, mixed>|object $data
     *
     * @throws JSONPathException
     */
    private function recurse(array &$result, array|object $data): void
    {
        $result[] = (array)$data;

        if (AccessHelper::isCollectionType($data)) {
            foreach (AccessHelper::arrayValues($data) as $value) {
                if (AccessHelper::isCollectionType($value)) {
                    $this->recurse($result, $value);
                }
            }
        }
    }
}


================================================
FILE: src/Filters/SliceFilter.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Filters;

use Flow\JSONPath\AccessHelper;

class SliceFilter extends AbstractFilter
{
    /**
     * @inheritDoc
     */
    public function filter(array|object $collection): array
    {
        if (
            !\is_array($collection)
            && !$collection instanceof \Countable
            && !$collection instanceof \ArrayAccess
        ) {
            return [];
        }

        $length = \count($collection);
        $start = $this->token->value['start'];
        $end = $this->token->value['end'];
        $step = $this->token->value['step'] ?? 1;
        $result = [];

        if ($step === 0) {
            return $result;
        }

        if ($step > 0) {
            [$start, $end] = $this->normalizeForPositiveStep($length, $start, $end);

            for ($i = $start; $i < $end; $i += $step) {
                if (AccessHelper::keyExists($collection, $i, $this->magicIsAllowed)) {
                    $result[] = $collection[$i];
                }
            }

            return $result;
        }

        [$start, $end] = $this->normalizeForNegativeStep($length, $start, $end);

        for ($i = $start; $i > $end; $i += $step) {
            if (AccessHelper::keyExists($collection, $i, $this->magicIsAllowed)) {
                $result[] = $collection[$i];
            }
        }

        return $result;
    }

    /**
     * @return array{0: int, 1: int}
     */
    private function normalizeForPositiveStep(int $length, ?int $start, ?int $end): array
    {
        if ($start === null) {
            $start = 0;
        } elseif ($start < 0) {
            $start += $length;
        }

        if ($start < 0) {
            $start = 0;
        } elseif ($start > $length) {
            $start = $length;
        }

        if ($end === null) {
            $end = $length;
        } elseif ($end < 0) {
            $end += $length;
        }

        if ($end < 0) {
            $end = 0;
        } elseif ($end > $length) {
            $end = $length;
        }

        return [$start, $end];
    }

    /**
     * @return array{0: int, 1: int}
     */
    private function normalizeForNegativeStep(int $length, ?int $start, ?int $end): array
    {
        if ($start === null) {
            $start = $length - 1;
        } else {
            if ($start < 0) {
                $start += $length;
            }

            if ($start < 0) {
                $start = -1;
            } elseif ($start >= $length) {
                $start = $length - 1;
            }
        }

        if ($end === null) {
            $end = -1;
        } else {
            if ($end < 0) {
                $end += $length;
            }

            if ($end < 0) {
                $end = -1;
            } elseif ($end >= $length) {
                $end = $length - 1;
            }
        }

        return [$start, $end];
    }
}


================================================
FILE: src/JSONPath.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
use Override;

/**
 * @implements ArrayAccess<int|string, mixed>
 * @implements Iterator<int|string, mixed>
 */
class JSONPath implements ArrayAccess, Iterator, JsonSerializable, Countable
{
    public const int ALLOW_MAGIC = 1;

    /** @var array<int, list<JSONPathToken>> */
    protected static array $tokenCache = [];

    protected mixed $data = [];

    protected int $options = 0;

    final public function __construct(mixed $data = [], int $options = 0)
    {
        $this->data = $data;
        $this->options = $options;
    }

    /**
     * Evaluate an expression
     *
     * @throws JSONPathException
     *
     * @return static
     */
    public function find(string $expression): self
    {
        $tokens = $this->parseTokens($expression);
        $collectionData = [$this->data];

        foreach ($tokens as $token) {
            $filter = $token->buildFilter($this->options);
            $filter->setRootData($this->data);
            $filteredDataList = [];

            foreach ($collectionData as $value) {
                if (AccessHelper::isCollectionType($value)) {
                    $filteredDataList[] = $filter->filter($value);
                }
            }

            if (!empty($filteredDataList)) {
                $collectionData = \array_merge(...$filteredDataList);
            } else {
                $collectionData = [];
            }
        }

        return new static($collectionData, $this->options);
    }

    public function first(): mixed
    {
        $keys = AccessHelper::collectionKeys($this->data);

        if (empty($keys)) {
            return null;
        }

        $value = $this->data[$keys[0]] ?? null;

        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
    }

    /**
     * Evaluate an expression and return the last result
     */
    public function last(): mixed
    {
        $keys = AccessHelper::collectionKeys($this->data);

        if (empty($keys)) {
            return null;
        }

        $value = $this->data[\end($keys)] ?? null;

        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
    }

    /**
     * Evaluate an expression and return the first key
     */
    public function firstKey(): string|int|null
    {
        $keys = AccessHelper::collectionKeys($this->data);

        if (empty($keys)) {
            return null;
        }

        return $keys[0];
    }

    /**
     * Evaluate an expression and return the last key
     */
    public function lastKey(): string|int|null
    {
        $keys = AccessHelper::collectionKeys($this->data);

        if (empty($keys)) {
            return null;
        }

        return \end($keys);
    }

    /**
     * @return list<JSONPathToken>
     * @throws JSONPathException
     */
    public function parseTokens(string $expression): array
    {
        $cacheKey = \crc32($expression);

        if (isset(static::$tokenCache[$cacheKey])) {
            return static::$tokenCache[$cacheKey];
        }

        $lexer = new JSONPathLexer($expression);
        $tokens = $lexer->parseExpression();

        static::$tokenCache[$cacheKey] = $tokens;

        return $tokens;
    }

    public function getData(): mixed
    {
        return $this->data;
    }

    /**
     * @noinspection MagicMethodsValidityInspection
     */
    public function __get(string|int $key): mixed
    {
        return $this->offsetExists($key) ? $this->offsetGet($key) : null;
    }

    #[Override]
    public function offsetExists(mixed $offset): bool
    {
        return AccessHelper::keyExists($this->data, $offset);
    }

    #[Override]
    public function offsetGet(mixed $offset): mixed
    {
        $value = AccessHelper::getValue($this->data, $offset);

        return AccessHelper::isCollectionType($value)
            ? new static($value, $this->options)
            : $value;
    }

    #[Override]
    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($offset === null) {
            $this->data[] = $value;
        } else {
            AccessHelper::setValue($this->data, $offset, $value);
        }
    }

    #[Override]
    public function offsetUnset(mixed $offset): void
    {
        AccessHelper::unsetValue($this->data, $offset);
    }

    #[Override]
    public function jsonSerialize(): mixed
    {
        return $this->getData();
    }

    #[Override]
    public function current(): mixed
    {
        $value = \current($this->data);

        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
    }

    #[Override]
    public function next(): void
    {
        \next($this->data);
    }

    #[Override]
    public function key(): string|int|null
    {
        return \key($this->data);
    }

    #[Override]
    public function valid(): bool
    {
        return \key($this->data) !== null;
    }

    #[Override]
    public function rewind(): void
    {
        \reset($this->data);
    }

    #[Override]
    public function count(): int
    {
        return \count($this->data);
    }
}


================================================
FILE: src/JSONPathException.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

use Exception;

class JSONPathException extends Exception
{
    // does nothing
}


================================================
FILE: src/JSONPathLexer.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

class JSONPathLexer
{
    /*
     * Match within bracket groups
     * Matches are whitespace insensitive
     */

    // e.g.: foo or 40f35757-2563-4790-b0b1-caa904be455f or $
    public const string MATCH_INDEX = '(?!-)[\-\w]+ | \\$ | \\*';

    // Eg. 0,1,2 or *,1 or 0,1,2,
    public const string MATCH_INDEXES = '\s* (?:-?\d+|\*) (?: \s* , \s* (?:-?\d+|\*) )+ \s* ,? \s*';

    // Eg. [0:2:1] or [-1]
    public const string MATCH_SLICE = '(?:-?\d*:-?\d*(?::-?\d*)?|-\\d+)';

    // Eg. ?(@.length - 1)
    public const string MATCH_QUERY_RESULT = '\s* \( .+? \) \s*';

    // Eg. ?(@.foo = "bar")
    public const string MATCH_QUERY_MATCH = '\s* \?\(.+?\) \s*';

    // Eg. 'bar'
    public const string MATCH_INDEX_IN_SINGLE_QUOTES = '\s* \' (.+?)? \' \s*';

    // Eg. "bar"
    public const string MATCH_INDEX_IN_DOUBLE_QUOTES = '\s* " (.+?)? " \s*';

    private readonly string $expression;

    private readonly int $expressionLength;

    public function __construct(string $expression)
    {
        $expression = \trim($expression);
        $len = \strlen($expression);

        if ($len === 0) {
            $this->expression = '';
            $this->expressionLength = 0;

            return;
        }

        if ($expression[0] === '$' || $expression[0] === '@') {
            $expression = \substr($expression, 1);
        }

        if ($expression === '') {
            $this->expression = '';
            $this->expressionLength = 0;

            return;
        }

        if ($expression[0] !== '.' && $expression[0] !== '[') {
            $expression = '.' . $expression;
        }

        $this->expression = $expression;
        $this->expressionLength = \strlen($expression);
    }

    /**
     * @return list<JSONPathToken>
     * @throws JSONPathException
     */
    public function parseExpressionTokens(): array
    {
        $squareBracketDepth = 0;
        $tokenValue = '';
        $tokens = [];
        $inBracketQuote = null;
        $inQuote = null;

        for ($i = 0; $i < $this->expressionLength; $i++) {
            $char = $this->expression[$i];

            if ($squareBracketDepth === 0 && ($char === "'" || $char === '"')) {
                $escaped = $this->isEscaped($tokenValue);
                $inQuote = $inQuote === $char && !$escaped ? null : ($inQuote ?? $char);
            }

            if (($squareBracketDepth === 0) && $inQuote === null && $char === '.') {
                if ($this->lookAhead($i) === '.') {
                    $tokens[] = new JSONPathToken(TokenType::Recursive, null);
                }

                continue;
            }

            if ($char === '[' && $inBracketQuote === null) {
                $squareBracketDepth++;

                if ($squareBracketDepth === 1) {
                    $inBracketQuote = null;

                    continue;
                }
            }

            if ($char === ']' && $squareBracketDepth > 0 && $inBracketQuote === null) {
                $squareBracketDepth--;

                if ($squareBracketDepth === 0) {
                    $tokens[] = $this->createToken($tokenValue);
                    $tokenValue = '';

                    continue;
                }
            }

            /*
             * Within square brackets
             */
            if ($squareBracketDepth > 0) {
                if (($char === "'" || $char === '"')) {
                    $escaped = $this->isEscaped($tokenValue);

                    if ($inBracketQuote === null && !$escaped) {
                        $inBracketQuote = $char;
                    } elseif ($inBracketQuote === $char && !$escaped) {
                        $inBracketQuote = null;
                    }
                }

                $tokenValue .= $char;

                continue;
            }

            /*
             * Outside square brackets
             */
            $tokenValue .= $char;

            if (
                $inQuote === null
                && ($this->atEnd($i) || \in_array($this->lookAhead($i), ['.', '['], true))
            ) {
                $tokens[] = $this->createToken($tokenValue);
                $tokenValue = '';
            }
        }

        if ($tokenValue !== '') {
            $tokens[] = $this->createToken($tokenValue);
        }

        return $tokens;
    }

    protected function lookAhead(int $pos, int $forward = 1): ?string
    {
        return $this->expression[$pos + $forward] ?? null;
    }

    protected function atEnd(int $pos): bool
    {
        return $pos === ($this->expressionLength - 1);
    }

    /**
     * @return list<JSONPathToken>
     * @throws JSONPathException
     */
    public function parseExpression(): array
    {
        return $this->parseExpressionTokens();
    }

    /**
     * @throws JSONPathException
     */
    protected function createToken(string $value): JSONPathToken
    {
        // The IDE doesn't like, what we do with $value, so let's
        // move it to a separate variable, to get rid of any IDE warnings
        $tokenValue = \trim($value);

        /** @var JSONPathToken|null $ret */
        $ret = null;

        if (\str_contains($tokenValue, ',')) {
            $parts = \array_values(\array_filter(
                \array_map('trim', \explode(',', $tokenValue)),
                static fn (string $part): bool => $part !== ''
            ));

            if ($parts !== []) {
                $union = [];

                $hasSlice = false;
                $hasQuery = false;

                foreach ($parts as $part) {
                    if (
                        \preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/xu', $part, $matches)
                        || \preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/xu', $part, $matches)
                    ) {
                        $union[] = $this->decodeQuotedIndex($matches[1] ?? '', $matches[0][0]);

                        continue;
                    }

                    if (\preg_match('/^-\\d+$/', $part)) {
                        $union[] = (int)$part;

                        continue;
                    }

                    if (\preg_match('/^' . static::MATCH_SLICE . '$/u', $part)) {
                        $union[] = [
                            'type' => 'slice',
                            'value' => $this->parseSlice($part),
                        ];
                        $hasSlice = true;

                        continue;
                    }

                    if (\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $part)) {
                        $union[] = \preg_match('/^-?\d+$/', $part) ? (int)$part : $part;

                        continue;
                    }

                    if (\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $part)) {
                        $union[] = [
                            'type' => 'query',
                            'value' => \substr($part, 2, -1),
                        ];
                        $hasQuery = true;
                    }
                }

                if (\count($union) === \count($parts)) {
                    $quotedPattern = '/^(' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '|'
                        . static::MATCH_INDEX_IN_DOUBLE_QUOTES . ')$/xu';

                    $quotedCallback = static function (string $part) use ($quotedPattern): bool {
                        return \preg_match($quotedPattern, $part) === 1;
                    };

                    $quotedParts = \array_filter($parts, $quotedCallback);

                    $allQuoted = \count($quotedParts) === \count($parts);

                    $tokenType = ($hasSlice || $hasQuery || !$allQuoted) ? TokenType::Indexes : TokenType::Index;

                    return new JSONPathToken($tokenType, $union, $allQuoted);
                }
            }
        }

        if (\preg_match('/^-\\d+$/', $tokenValue)) {
            return new JSONPathToken(TokenType::Index, (int)$tokenValue);
        }

        if ($tokenValue === '') {
            return new JSONPathToken(TokenType::Indexes, []);
        }

        if (
            ($tokenValue[0] === "'" || $tokenValue[0] === '"')
            && $tokenValue[\strlen($tokenValue) - 1] === $tokenValue[0]
        ) {
            $tokenValue = $this->decodeQuotedIndex(\substr($tokenValue, 1, -1), $tokenValue[0]);

            return new JSONPathToken(TokenType::Index, $tokenValue, true);
        }

        if (\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $tokenValue, $matches)) {
            if (\preg_match('/^-?\d+$/', $tokenValue)) {
                $tokenValue = (int)$tokenValue;
            }

            $ret = new JSONPathToken(TokenType::Index, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_SLICE . '$/xu', $tokenValue, $matches)) {
            $tokenValue = $this->parseSlice($tokenValue);

            $ret = new JSONPathToken(TokenType::Slice, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_QUERY_RESULT . '$/xu', $tokenValue)) {
            $tokenValue = \substr($tokenValue, 1, -1);

            $ret = new JSONPathToken(TokenType::QueryResult, $tokenValue);
        } elseif ($tokenValue === '?()') {
            $ret = new JSONPathToken(TokenType::QueryMatch, '', shorthand: false);
        } elseif ($tokenValue === '?') {
            $ret = new JSONPathToken(TokenType::QueryMatch, '@', shorthand: true);
        } elseif (\preg_match('/^\\?@/', $tokenValue)) {
            $expr = \substr($tokenValue, 1);
            $expr = $expr === '' ? '@' : $expr;

            $ret = new JSONPathToken(TokenType::QueryMatch, $expr, shorthand: true);
        } elseif (\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $tokenValue)) {
            $tokenValue = \substr($tokenValue, 2, -1);

            $ret = new JSONPathToken(TokenType::QueryMatch, $tokenValue);
        }

        if ($ret !== null) {
            return $ret;
        }

        throw new JSONPathException("Unable to parse token {$tokenValue} in expression: {$this->expression}");
    }

    /**
     * @return array{start: int|null, end: int|null, step: int|null}
     */
    private function parseSlice(string $tokenValue): array
    {
        $parts = \explode(':', $tokenValue);

        return [
            'start' => $parts[0] !== '' ? (int)$parts[0] : null,
            'end' => isset($parts[1]) && $parts[1] !== '' ? (int)$parts[1] : null,
            'step' => isset($parts[2]) && $parts[2] !== '' ? (int)$parts[2] : null,
        ];
    }

    private function isEscaped(string $tokenValue): bool
    {
        $len = \strlen($tokenValue);
        if ($len === 0) {
            return false;
        }

        $backslashCount = 0;

        for ($i = $len - 1; $i >= 0; $i--) {
            if ($tokenValue[$i] === '\\') {
                $backslashCount++;
                continue;
            }

            break;
        }

        return ($backslashCount % 2) === 1;
    }

    private function decodeQuotedIndex(string $tokenValue, string $quote): string
    {
        // Unescape backslashes first, then the quote type used
        $tokenValue = \str_replace('\\\\', '\\', $tokenValue);

        if ($quote === "'") {
            $tokenValue = \str_replace("\\'", "'", $tokenValue);
        } elseif ($quote === '"') {
            $tokenValue = \str_replace('\\"', '"', $tokenValue);
        }

        return $tokenValue;
    }
}


================================================
FILE: src/JSONPathToken.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

use Flow\JSONPath\Filters\AbstractFilter;
use Flow\JSONPath\Filters\IndexesFilter;
use Flow\JSONPath\Filters\IndexFilter;
use Flow\JSONPath\Filters\QueryMatchFilter;
use Flow\JSONPath\Filters\QueryResultFilter;
use Flow\JSONPath\Filters\RecursiveFilter;
use Flow\JSONPath\Filters\SliceFilter;

readonly class JSONPathToken
{
    public function __construct(
        public TokenType $type,
        public mixed $value,
        public bool $quoted = false,
        public bool $shorthand = false,
    ) {
        // ...
    }

    public function buildFilter(int $options): AbstractFilter
    {
        $filterClass = match ($this->type) {
            TokenType::Index => IndexFilter::class,
            TokenType::Indexes => IndexesFilter::class,
            TokenType::QueryMatch => QueryMatchFilter::class,
            TokenType::QueryResult => QueryResultFilter::class,
            TokenType::Recursive => RecursiveFilter::class,
            TokenType::Slice => SliceFilter::class,
        };

        return new $filterClass($this, $options);
    }
}


================================================
FILE: src/TokenType.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath;

enum TokenType: string
{
    case Index = 'index';
    case Recursive = 'recursive';
    case QueryResult = 'queryResult';
    case QueryMatch = 'queryMatch';
    case Slice = 'slice';
    case Indexes = 'indexes';
}


================================================
FILE: tests/AccessHelperTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use ArrayAccess;
use ArrayIterator;
use Flow\JSONPath\AccessHelper;
use Flow\JSONPath\JSONPathException;
use IteratorAggregate;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Traversable;

#[CoversClass(AccessHelper::class)]
class AccessHelperTest extends TestCase
{
    public function testKeyExistsRespectsMagicGet(): void
    {
        $magic = new class {
            public function __get(string $name): string
            {
                return "magic-{$name}";
            }

            public function __set(string $name, mixed $value): void
            {
                $this->{$name} = $value;
            }

            public function __isset(string $name): bool
            {
                return isset($this->{$name});
            }
        };

        self::assertTrue(AccessHelper::keyExists($magic, 'foo', true));
        self::assertFalse(AccessHelper::keyExists($magic, 'foo'));
    }

    public function testKeyExistsSupportsArrayAccessAndNegativeIndex(): void
    {
        $arrayAccess = new class implements ArrayAccess {
            /** @var array<string, string> */
            private array $store = ['bar' => 'baz'];

            public function offsetExists($offset): bool
            {
                return \array_key_exists($offset, $this->store);
            }

            public function offsetGet($offset): mixed
            {
                return $this->store[$offset];
            }

            public function offsetSet($offset, $value): void
            {
                $this->store[$offset] = $value;
            }

            public function offsetUnset($offset): void
            {
                unset($this->store[$offset]);
            }
        };

        self::assertTrue(AccessHelper::keyExists($arrayAccess, 'bar'));
        self::assertTrue(AccessHelper::keyExists([1 => 'foo'], -1));
    }

    public function testGetValueCoversMagicArrayAndArrayAccess(): void
    {
        $magic = new class {
            public function __get(string $name): string
            {
                return "magic-{$name}";
            }

            public function __set(string $name, mixed $value): void
            {
                $this->{$name} = $value;
            }

            public function __isset(string $name): bool
            {
                return isset($this->{$name});
            }
        };

        $arrayAccess = new class implements ArrayAccess {
            /** @var array<string, string> */
            private array $store = ['bar' => 'baz'];

            public function offsetExists($offset): bool
            {
                return \array_key_exists($offset, $this->store);
            }

            public function offsetGet($offset): mixed
            {
                return $this->store[$offset];
            }

            public function offsetSet($offset, $value): void
            {
                $this->store[$offset] = $value;
            }

            public function offsetUnset($offset): void
            {
                unset($this->store[$offset]);
            }
        };

        self::assertSame('magic-foo', AccessHelper::getValue($magic, 'foo', true));
        self::assertSame('baz', AccessHelper::getValue($arrayAccess, 'bar'));
        self::assertSame('b', AccessHelper::getValue(['a', 'b'], -1));
        self::assertNull(AccessHelper::getValue(['a'], 'missing'));
        $plainObject = (object)['prop' => 'value'];
        self::assertSame('value', AccessHelper::getValue($plainObject, 'prop'));
    }

    public function testGetValueByIndexSupportsTraversableAndNegativeOffset(): void
    {
        $iterable = new class implements IteratorAggregate {
            public function getIterator(): Traversable
            {
                return new ArrayIterator(['first', 'second', 'third']);
            }
        };

        self::assertSame('third', AccessHelper::getValue($iterable, -1));
        self::assertSame('second', AccessHelper::getValue($iterable, 1));
    }

    public function testGetValueNullCases(): void
    {
        self::assertNull(AccessHelper::getValue('scalar', 'foo'));
        self::assertNull(AccessHelper::getValue('scalar', 5));

        $iterable = new ArrayIterator(['only']);
        self::assertNull(AccessHelper::getValue($iterable, 5));
    }

    public function testArrayValuesThrowsOnInvalidType(): void
    {
        $this->expectException(JSONPathException::class);
        AccessHelper::arrayValues('not-an-array');
    }

    /**
     * @throws JSONPathException
     */
    public function testArrayValuesCastsObject(): void
    {
        $obj = (object)['a' => 1, 'b' => 2];
        self::assertSame([1, 2], AccessHelper::arrayValues($obj));
        self::assertSame([1, 2], AccessHelper::arrayValues(['a' => 1, 'b' => 2]));
    }

    public function testGetValueByIndexReturnsNullWhenOutOfRange(): void
    {
        $iterable = (static function () {
            yield 'first';
        })();

        self::assertNull(AccessHelper::getValue($iterable, 10));
    }

    public function testKeyExistsAndCollectionHelpers(): void
    {
        $object = (object)['a' => 1];
        self::assertTrue(AccessHelper::keyExists($object, 'a'));
        self::assertFalse(AccessHelper::keyExists('scalar', 'a'));
        self::assertSame(['a'], AccessHelper::collectionKeys($object));
        self::assertSame(['b'], AccessHelper::collectionKeys(['b' => 2]));
        self::assertFalse(AccessHelper::isCollectionType('scalar'));
    }

    public function testSetAndUnsetValueAcrossTypes(): void
    {
        $object = (object)['a' => 1];
        AccessHelper::setValue($object, 'b', 2);
        self::assertSame(2, $object->b);
        $array = ['x' => 1];
        AccessHelper::setValue($array, 'y', 3);
        self::assertSame(3, $array['y']);

        $arrayAccess = new class implements ArrayAccess {
            /** @var array<string, string> */
            public array $store = [];

            public function offsetExists($offset): bool
            {
                return \array_key_exists($offset, $this->store);
            }

            public function offsetGet($offset): mixed
            {
                return $this->store[$offset];
            }

            public function offsetSet($offset, $value): void
            {
                $this->store[$offset] = $value;
            }

            public function offsetUnset($offset): void
            {
                unset($this->store[$offset]);
            }
        };

        AccessHelper::setValue($arrayAccess, 'k', 'v');
        self::assertSame('v', $arrayAccess->store['k']);
        AccessHelper::unsetValue($arrayAccess, 'k');
        self::assertArrayNotHasKey('k', $arrayAccess->store);

        AccessHelper::unsetValue($array, 'x');
        self::assertArrayNotHasKey('x', $array);

        $obj = (object)['x' => 1];
        AccessHelper::unsetValue($obj, 'x');
        self::assertFalse(\property_exists($obj, 'x'));

        $arrayAccess->offsetUnset('missing');
    }
}


================================================
FILE: tests/IndexFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use ArrayObject;
use Flow\JSONPath\Filters\IndexFilter;
use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(IndexFilter::class)]
class IndexFilterTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testArrayValueTokenReturnsOnlyExistingKeys(): void
    {
        $token = new JSONPathToken(TokenType::Index, [0, 2, 99]);
        $filter = new IndexFilter($token);

        self::assertSame(
            ['first', 'third'],
            $filter->filter(['first', 'second', 'third'])
        );
    }

    /**
     * @throws JSONPathException
     */
    public function testSingleIndexWorksForObjectsAndArrayAccess(): void
    {
        $token = new JSONPathToken(TokenType::Index, 'prop');
        $filter = new IndexFilter($token);
        $object = (object)['prop' => 5];

        self::assertSame([5], $filter->filter($object));

        $arrayObject = new ArrayObject(['prop' => 'value']);
        self::assertSame(['value'], $filter->filter($arrayObject));
    }

    /**
     * @throws JSONPathException
     */
    public function testWildcardReturnsValuesAndLengthReturnsCount(): void
    {
        $wildcard = new IndexFilter(new JSONPathToken(TokenType::Index, '*'));
        $length = new IndexFilter(new JSONPathToken(TokenType::Index, 'length'));

        $input = ['a' => 1, 'b' => 2];

        self::assertSame([1, 2], $wildcard->filter($input));
        self::assertSame([2], $length->filter($input));
    }

    /**
     * @throws JSONPathException
     */
    public function testReturnsEmptyWhenKeyMissing(): void
    {
        $filter = new IndexFilter(new JSONPathToken(TokenType::Index, 'missing'));

        self::assertSame([], $filter->filter(['present' => 1]));
    }

    /**
     * @throws JSONPathException
     */
    public function testJSONPathFindOnScalarProducesEmptyCollection(): void
    {
        $result = new JSONPath(123)->find('$.missing');

        self::assertSame([], $result->getData());
    }
}


================================================
FILE: tests/IndexesFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\Filters\IndexesFilter;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(IndexesFilter::class)]
class IndexesFilterTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testReturnsSliceAndExplicitIndexes(): void
    {
        $token = new JSONPathToken(TokenType::Indexes, [
            ['type' => 'slice', 'value' => ['start' => 1, 'end' => 3, 'step' => null]],
            0,
        ]);

        $filter = new IndexesFilter($token);

        self::assertSame([2, 3, 1], $filter->filter([1, 2, 3, 4]));
    }

    /**
     * @throws JSONPathException
     */
    public function testSupportsQueryAndWildcard(): void
    {
        $token = new JSONPathToken(TokenType::Indexes, [
            ['type' => 'query', 'value' => '@.v>1'],
            '*',
        ]);

        $filter = new IndexesFilter($token);
        $filter->setRootData([]);

        $data = [
            ['v' => 1],
            ['v' => 2],
        ];

        $result = $filter->filter($data);

        self::assertSame([['v' => 2], ['v' => 1], ['v' => 2]], $result);
    }
}


================================================
FILE: tests/JSONPathCoreTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\TestCase;

class JSONPathCoreTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testFindAndHelpers(): void
    {
        $data = [
            'list' => [
                ['v' => 1],
                ['v' => 2],
                ['v' => 3],
            ],
            'nested' => ['inner' => ['x' => 9]],
        ];

        $path = new JSONPath($data);
        $slice = $path->find('$.list[1:3]');

        self::assertSame([['v' => 2], ['v' => 3]], $slice->getData());

        $first = $path->first();
        $last = $path->last();

        self::assertSame($data['list'], $first instanceof JSONPath ? $first->getData() : $first);
        self::assertSame(['inner' => ['x' => 9]], $last instanceof JSONPath ? $last->getData() : $last);
        self::assertSame('list', $path->firstKey());
        self::assertSame('nested', $path->lastKey());
    }

    public function testOffsetAccessAndIteration(): void
    {
        $path = new JSONPath(['child' => ['a' => 1]]);

        self::assertTrue($path->offsetExists('child'));

        /** @var JSONPath $child */
        $child = $path['child'];

        self::assertInstanceOf(JSONPath::class, $child);
        self::assertSame(['a' => 1], $child->getData());

        $path[] = 'appended';
        $path['new'] = 'value';
        unset($path['child']);

        $collected = [];

        foreach ($path as $key => $value) {
            $collected[$key] = $value instanceof JSONPath ? $value->getData() : $value;
        }

        self::assertArrayHasKey(0, $collected);
        self::assertSame('appended', $collected[0]);
        self::assertSame('value', $collected['new']);
        self::assertEquals(new JSONPathException('oops'), new JSONPathException('oops'));
        self::assertSame('index', TokenType::Index->value);
    }

    /**
     * @throws JSONPathException
     */
    public function testParseTokensCachesResults(): void
    {
        $path = new JSONPath(['a' => ['b' => 1]]);
        $first = $path->parseTokens('$.a.b');
        $second = $path->parseTokens('$.a.b');

        self::assertNotEmpty($first);
        self::assertSame($first, $second);
    }

    public function testJsonSerializeAndMagicGet(): void
    {
        $path = new JSONPath(['a' => 1]);

        self::assertSame(['a' => 1], $path->jsonSerialize());
        self::assertSame(1, $path->__get('a'));
        self::assertNull($path->__get('missing'));

        $empty = new JSONPath([]);

        self::assertNull($empty->first());
        self::assertNull($empty->last());
        self::assertNull($empty->firstKey());
        self::assertNull($empty->lastKey());
        self::assertSame(0, $empty->count());
    }

    /**
     * @throws JSONPathException
     */
    public function testFindOnScalarReturnsEmptyResult(): void
    {
        $result = new JSONPath(123)->find('$.missing')->getData();

        self::assertSame([], $result);
    }
}


================================================
FILE: tests/JSONPathIntegrationTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use ArrayObject;
use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use PHPUnit\Framework\TestCase;

class JSONPathIntegrationTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testArrayObjectTraversal(): void
    {
        $data = new ArrayObject([
            'items' => new ArrayObject([
                ['name' => 'keep', 'active' => true],
                ['name' => 'skip', 'active' => false],
            ]),
        ]);

        $result = new JSONPath($data)->find('$.items[?(@.active==true)]')->getData();

        self::assertSame([['name' => 'keep', 'active' => true]], $result);
    }

    /**
     * @throws JSONPathException
     */
    public function testDashedIndexIsParsedWithoutQuotes(): void
    {
        $data = ['data' => ['dash-key' => 42, 'other' => 1]];

        $result = new JSONPath($data)->find('$.data[dash-key]')->getData();

        self::assertSame([42], $result);
    }

    /**
     * @throws JSONPathException
     */
    public function testSlicesResolveViaPublicApi(): void
    {
        $path = new JSONPath(['values' => [0, 1, 2, 3, 4]]);

        self::assertSame([1, 2, 3], $path->find('$.values[1:-1]')->getData());
        self::assertSame([4, 3], $path->find('$.values[-1:-3:-1]')->getData());
    }
}


================================================
FILE: tests/JSONPathLexerTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathLexer;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(JSONPathLexer::class)]
class JSONPathLexerTest extends TestCase
{
    /**
     * @param list<array{type: TokenType, value: mixed, shorthand?: bool}> $expectedTokens
     * @throws JSONPathException
     */
    #[DataProvider('expressionProvider')]
    public function testParsesExpressions(string $expression, array $expectedTokens): void
    {
        $tokens = new JSONPathLexer($expression)->parseExpression();

        self::assertCount(\count($expectedTokens), $tokens);

        foreach ($expectedTokens as $i => $expected) {
            self::assertEquals($expected['type'], $tokens[$i]->type);
            self::assertEquals($expected['value'], $tokens[$i]->value);

            if (\array_key_exists('shorthand', $expected)) {
                self::assertSame($expected['shorthand'], $tokens[$i]->shorthand);
            }
        }
    }

    /**
     * @return iterable<string, array{string, list<array{type: TokenType, value: mixed, shorthand?: bool}>}>
     */
    public static function expressionProvider(): iterable
    {
        yield 'wildcard index' => [
            '.*',
            [
                ['type' => TokenType::Index, 'value' => '*'],
            ],
        ];

        yield 'simple index' => [
            '.foo',
            [
                ['type' => TokenType::Index, 'value' => 'foo'],
            ],
        ];

        yield 'bare index normalizes dot prefix' => [
            'foo',
            [
                ['type' => TokenType::Index, 'value' => 'foo'],
            ],
        ];

        yield 'complex quoted index' => [
            '["\'b.^*_"]',
            [
                ['type' => TokenType::Index, 'value' => "'b.^*_"],
            ],
        ];

        yield 'integer index' => [
            '[0]',
            [
                ['type' => TokenType::Index, 'value' => 0],
            ],
        ];

        yield 'index after dot notation' => [
            '.books[0]',
            [
                ['type' => TokenType::Index, 'value' => 'books'],
                ['type' => TokenType::Index, 'value' => 0],
            ],
        ];

        yield 'quoted index with whitespace' => [
            '[   "foo$-/\'"     ]',
            [
                ['type' => TokenType::Index, 'value' => "foo$-/'"],
            ],
        ];

        yield 'slice with explicit bounds' => [
            '[0:1:2]',
            [
                ['type' => TokenType::Slice, 'value' => ['start' => 0, 'end' => 1, 'step' => 2]],
            ],
        ];

        yield 'negative index' => [
            '[-1]',
            [
                ['type' => TokenType::Index, 'value' => -1],
            ],
        ];

        yield 'slice all nulls' => [
            '[:]',
            [
                ['type' => TokenType::Slice, 'value' => ['start' => null, 'end' => null, 'step' => null]],
            ],
        ];

        yield 'shorthand query current' => [
            '[?@]',
            [
                ['type' => TokenType::QueryMatch, 'value' => '@', 'shorthand' => true],
            ],
        ];

        yield 'shorthand query comparison' => [
            '[?@==null]',
            [
                ['type' => TokenType::QueryMatch, 'value' => '@==null', 'shorthand' => true],
            ],
        ];

        yield 'shorthand query empty expression' => [
            '[?]',
            [
                ['type' => TokenType::QueryMatch, 'value' => '@', 'shorthand' => true],
            ],
        ];

        yield 'double quoted index with escape' => [
            '$["a\\"b"]',
            [
                ['type' => TokenType::Index, 'value' => 'a"b'],
            ],
        ];

        yield 'union with slice and negative index' => [
            '[-2,1:3]',
            [
                [
                    'type' => TokenType::Indexes,
                    'value' => [
                        -2,
                        [
                            'type' => 'slice',
                            'value' => ['start' => 1, 'end' => 3, 'step' => null],
                        ],
                    ],
                ],
            ],
        ];

        yield 'union with query' => [
            '[1,?(@.foo>1)]',
            [
                [
                    'type' => TokenType::Indexes,
                    'value' => [
                        1,
                        [
                            'type' => 'query',
                            'value' => '@.foo>1',
                        ],
                    ],
                ],
            ],
        ];

        yield 'single quoted index with escapes' => [
            "$['back\\\\slash\\'quote']",
            [
                ['type' => TokenType::Index, 'value' => "back\\slash'quote"],
            ],
        ];

        yield 'multiple quoted indexes collapse to array' => [
            '["first","second"]',
            [
                ['type' => TokenType::Index, 'value' => ['first', 'second'], 'quoted' => true],
            ],
        ];

        yield 'empty quoted index resolves to empty string' => [
            '[""]',
            [
                ['type' => TokenType::Index, 'value' => '', 'quoted' => true],
            ],
        ];

        yield 'quoted index in dot notation preserves dots' => [
            "$.'some.key'",
            [
                ['type' => TokenType::Index, 'value' => 'some.key', 'quoted' => true],
            ],
        ];

        yield 'empty bracket notation yields empty index list' => [
            '$[]',
            [
                ['type' => TokenType::Indexes, 'value' => []],
            ],
        ];

        yield 'empty filter expression tokenizes to empty query match' => [
            '$[?()]',
            [
                ['type' => TokenType::QueryMatch, 'value' => '', 'shorthand' => false],
            ],
        ];

        yield 'quoted index preserves brackets and dollar signs' => [
            '$[\'[$the.size$]\']',
            [
                ['type' => TokenType::Index, 'value' => '[$the.size$]', 'quoted' => true],
            ],
        ];

        yield 'query result expression' => [
            '[(@.foo + 2)]',
            [
                ['type' => TokenType::QueryResult, 'value' => '@.foo + 2'],
            ],
        ];

        yield 'query match' => [
            "[?(@['@language']='en')]",
            [
                ['type' => TokenType::QueryMatch, 'value' => "@['@language']='en'"],
            ],
        ];

        yield 'recursive simple' => [
            '..foo',
            [
                ['type' => TokenType::Recursive, 'value' => null],
                ['type' => TokenType::Index, 'value' => 'foo'],
            ],
        ];

        yield 'recursive wildcard' => [
            '..*',
            [
                ['type' => TokenType::Recursive, 'value' => null],
                ['type' => TokenType::Index, 'value' => '*'],
            ],
        ];

        yield 'indexes with whitespace' => [
            '[ 1,2 , 3]',
            [
                ['type' => TokenType::Indexes, 'value' => [1, 2, 3]],
            ],
        ];
    }

    /**
     * @throws JSONPathException
     */
    public function testIndexBadlyFormed(): void
    {
        $this->expectException(JSONPathException::class);
        $this->expectExceptionMessage('Unable to parse token hello* in expression: .hello*');

        new JSONPathLexer('.hello*')->parseExpression();
    }

    /**
     * @throws JSONPathException
     */
    public function testRecursiveBadlyFormed(): void
    {
        $this->expectException(JSONPathException::class);
        $this->expectExceptionMessage('Unable to parse token ba^r in expression: ..ba^r');

        new JSONPathLexer('..ba^r')->parseExpression();
    }

    /**
     * @throws JSONPathException
     */
    public function testEmptyExpressionsReturnNoTokens(): void
    {
        self::assertSame([], new JSONPathLexer('')->parseExpression());
        self::assertSame([], new JSONPathLexer('$')->parseExpression());
    }

    /**
     * @throws JSONPathException
     */
    public function testSingleCharacterExpressionNormalized(): void
    {
        self::assertSame([], new JSONPathLexer('.')->parseExpression());
    }

    /**
     * @throws JSONPathException
     */
    public function testUnclosedBracketThrowsAfterFinalFlush(): void
    {
        $this->expectException(JSONPathException::class);

        new JSONPathLexer("['unterminated")->parseExpression();
    }
}


================================================
FILE: tests/JSONPathTokenTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\Filters\IndexesFilter;
use Flow\JSONPath\Filters\IndexFilter;
use Flow\JSONPath\Filters\QueryMatchFilter;
use Flow\JSONPath\Filters\QueryResultFilter;
use Flow\JSONPath\Filters\RecursiveFilter;
use Flow\JSONPath\Filters\SliceFilter;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(JSONPathToken::class)]
class JSONPathTokenTest extends TestCase
{
    public function testBuildFilterReturnsExpectedTypes(): void
    {
        self::assertInstanceOf(
            IndexFilter::class,
            new JSONPathToken(TokenType::Index, null)->buildFilter(0)
        );

        self::assertInstanceOf(
            IndexesFilter::class,
            new JSONPathToken(TokenType::Indexes, [])->buildFilter(0)
        );

        self::assertInstanceOf(
            QueryMatchFilter::class,
            new JSONPathToken(TokenType::QueryMatch, '')->buildFilter(0)
        );

        self::assertInstanceOf(
            QueryResultFilter::class,
            new JSONPathToken(TokenType::QueryResult, '')->buildFilter(0)
        );

        self::assertInstanceOf(
            RecursiveFilter::class,
            new JSONPathToken(TokenType::Recursive, null)->buildFilter(0)
        );

        self::assertInstanceOf(
            SliceFilter::class,
            new JSONPathToken(TokenType::Slice, ['start' => 0, 'end' => 0, 'step' => 1])->buildFilter(0)
        );
    }

    public function testConstructorSetsProperties(): void
    {
        $token = new JSONPathToken(TokenType::Index, 'value');

        self::assertSame(TokenType::Index, $token->type);
        self::assertSame('value', $token->value);
    }
}


================================================
FILE: tests/QueryMatchFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\Filters\QueryMatchFilter;
use Flow\JSONPath\JSONPath;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use RuntimeException;

#[CoversClass(QueryMatchFilter::class)]
class QueryMatchFilterTest extends TestCase
{
    /**
     * @return iterable<string, array{data: mixed, expression: string, expected: array<int, mixed>}>
     */
    public static function filterProvider(): iterable
    {
        yield 'shorthand truthy filters values' => [
            'data' => [0, 1, '', 'value', false],
            'expression' => '$[?@]',
            'expected' => [1 => 1, 3 => 'value'],
        ];

        yield 'negation wrapped' => [
            'data' => [['flag' => true], ['flag' => false]],
            'expression' => '$[?(!(@.flag==true))]',
            'expected' => [['flag' => false]],
        ];

        yield 'negation unwrapped' => [
            'data' => [['flag' => true], ['flag' => false]],
            'expression' => '$[?(!@.flag==true)]',
            'expected' => [['flag' => false]],
        ];

        yield 'grouped logical expressions' => [
            'data' => [
                ['active' => true, 'score' => 1],
                ['active' => true, 'score' => 2],
                ['active' => false, 'score' => 3],
            ],
            'expression' => '$[?(@.active==true && (@.score>1))]',
            'expected' => [['active' => true, 'score' => 2]],
        ];

        yield 'path comparison current and root' => [
            'data' => [
                'threshold' => 5,
                'items' => [
                    ['v' => 5, 'w' => 5],
                    ['v' => 4, 'w' => 5],
                ],
            ],
            'expression' => '$.items[?(@.v==@.w && @.v==$.threshold)]',
            'expected' => [['v' => 5, 'w' => 5]],
        ];

        yield 'missing key compared to path still evaluates' => [
            'data' => [['foo' => 1], ['foo' => 1, 'bar' => 1]],
            'expression' => '$[?(@.bar==@.foo)]',
            'expected' => [['foo' => 1, 'bar' => 1]],
        ];

        yield 'dot separated key resolves through jsonpath' => [
            'data' => [
                ['nested' => ['value' => 3]],
                ['nested' => ['value' => 4]],
            ],
            'expression' => '$[?(@.nested.value==3)]',
            'expected' => [['nested' => ['value' => 3]]],
        ];

        yield 'deep equal lists and objects' => [
            'data' => [
                ['left' => [1, 2], 'right' => [1, 2]],
                ['left' => [1, 2], 'right' => [2, 1]],
                ['left' => (object)['a' => 1, 'b' => 2], 'right' => (object)['b' => 2, 'a' => 1]],
                ['left' => (object)['a' => 1], 'right' => (object)['a' => 2]],
            ],
            'expression' => '$[?(@.left==@.right)]',
            'expected' => [
                ['left' => [1, 2], 'right' => [1, 2]],
                ['left' => (object)['a' => 1, 'b' => 2], 'right' => (object)['b' => 2, 'a' => 1]],
            ],
        ];

        yield 'plain node selection compares current node' => [
            'data' => [0, 1, 2],
            'expression' => '$[?(@==@)]',
            'expected' => [0, 1, 2],
        ];

        yield 'deep equal failure branches' => [
            'data' => [
                ['left' => [1, 2], 'right' => ['a' => 1, 'b' => 2]],
                ['left' => [1], 'right' => [1, 2]],
            ],
            'expression' => '$[?(@.left==@.right)]',
            'expected' => [],
        ];

        yield 'regex comparison' => [
            'data' => ['foo', 'bar'],
            'expression' => '$[?(@ =~ /fo.*/)]',
            'expected' => ['foo'],
        ];

        yield 'in operator' => [
            'data' => [1, 2, 3],
            'expression' => '$[?(@ in [1,3])]',
            'expected' => [1, 3],
        ];

        yield 'nin operator with short circuit or' => [
            'data' => [
                ['a' => 1],
                ['b' => 1],
                ['a' => 3],
            ],
            'expression' => '$[?(@.a nin [2,3] || @.b==1)]',
            'expected' => [
                ['a' => 1],
                ['b' => 1],
            ],
        ];

        yield 'existence check without operator' => [
            'data' => [
                ['value' => 1],
                ['other' => 2],
            ],
            'expression' => '$[?(@.value)]',
            'expected' => [
                ['value' => 1],
            ],
        ];

        yield '!in operator' => [
            'data' => [1, 2, 3],
            'expression' => '$[?(@ !in [2])]',
            'expected' => [1, 3],
        ];

        yield 'less than comparison' => [
            'data' => [['n' => 1], ['n' => 3]],
            'expression' => '$[?(@.n<2)]',
            'expected' => [['n' => 1]],
        ];

        yield 'less or equal comparison' => [
            'data' => [['n' => 1], ['n' => 2], ['n' => 3]],
            'expression' => '$[?(@.n<=2)]',
            'expected' => [['n' => 1], ['n' => 2]],
        ];

        yield 'greater or equal comparison' => [
            'data' => [['n' => 1], ['n' => 2], ['n' => 3]],
            'expression' => '$[?(@.n>=2)]',
            'expected' => [['n' => 2], ['n' => 3]],
        ];

        yield 'not equals comparison' => [
            'data' => [['value' => 1], ['value' => 2]],
            'expression' => '$[?(@.value!=2)]',
            'expected' => [['value' => 1]],
        ];
    }

    /**
     * @param array<int, mixed> $expected
     * @throws JSONPathException
     */
    #[DataProvider('filterProvider')]
    public function testFilterScenarios(mixed $data, string $expression, array $expected): void
    {
        $result = new JSONPath($data)->find($expression)->getData();

        self::assertEquals(\array_values($expected), \array_values($result));
    }

    /**
     * @return iterable<string, array{expression: string, expectMatch: bool}>
     */
    public static function constantExpressionProvider(): iterable
    {
        yield 'num comparison true' => ['expression' => '[?(1<2)]', 'expectMatch' => true];
        yield 'num comparison false' => ['expression' => '[?(2>3)]', 'expectMatch' => false];
        yield 'num with leading zeros decoded as number' => ['expression' => '[?(0123==123)]', 'expectMatch' => true];
        yield 'string literal decoding' => ['expression' => '[?(foo==foo)]', 'expectMatch' => true];
        yield 'invalid less than comparison for non-scalars' => ['expression' => '[?([]<1)]', 'expectMatch' => false];
        yield 'not equals' => ['expression' => '[?(2!=3)]', 'expectMatch' => true];
        yield 'less or equal' => ['expression' => '[?(2<=2)]', 'expectMatch' => true];
        yield 'greater or equal' => ['expression' => '[?(1>=2)]', 'expectMatch' => false];
        yield 'json literal deep equal' => ['expression' => '[?({"a":1}=={"a":1})]', 'expectMatch' => true];
    }

    /**
     * @throws JSONPathException
     */
    #[DataProvider('constantExpressionProvider')]
    public function testConstantExpressions(string $expression, bool $expectMatch): void
    {
        $data = ['keep'];
        $result = new JSONPath($data)->find('$' . $expression)->getData();

        self::assertSame($expectMatch ? ['keep'] : [], $result);
    }

    /**
     * @throws JSONPathException
     */
    public function testShorthandTokenValueArrayFiltersTruthyNodes(): void
    {
        $token = new JSONPathToken(TokenType::QueryMatch, ['expression' => '@', 'shorthand' => true]);
        $filter = new QueryMatchFilter($token);

        $collection = [0, 1, '', 'value', false];

        self::assertSame([1 => 1, 3 => 'value'], $filter->filter($collection));
    }

    /**
     * @throws JSONPathException
     */
    public function testMalformedFilterThrowsRuntimeException(): void
    {
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('Malformed filter query');

        new JSONPath([1])->find('$[?(foo)]');
    }

    /**
     * @throws JSONPathException
     */
    public function testLiteralOnlyFilterExpressionsReturnWholeCollectionOrEmpty(): void
    {
        $data = [1, 2, 3];

        self::assertSame($data, new JSONPath($data)->find('$[?(true)]')->getData());
        self::assertSame([], new JSONPath($data)->find('$[?(false)]')->getData());
    }

    /**
     * @throws JSONPathException
     */
    public function testLogicalExpressionsWithLiteralRightOperand(): void
    {
        $data = [
            ['key' => 1],
            ['key' => -1],
        ];

        self::assertSame(
            [],
            new JSONPath($data)->find('$[?(@.key>0 && false)]')->getData()
        );
        self::assertSame(
            $data,
            new JSONPath($data)->find('$[?(@.key>0 || true)]')->getData()
        );
    }

    /**
     * @throws JSONPathException
     */
    public function testEmptyFilterExpressionReturnsEmpty(): void
    {
        self::assertSame([], new JSONPath([1, 2])->find('$[?()]')->getData());
    }

    /**
     * @throws JSONPathException
     */
    public function testNormalizeKeyCastsNumericStrings(): void
    {
        $token = new JSONPathToken(TokenType::QueryMatch, '@["2"]=="two"');
        $filter = new QueryMatchFilter($token);

        $result = $filter->filter([
            ['2' => 'two', '1' => 'one'],
            ['2' => 'nope', '1' => 'one'],
        ]);

        self::assertSame([['2' => 'two', '1' => 'one']], \array_values($result));
    }
}


================================================
FILE: tests/QueryResultFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\Filters\QueryResultFilter;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(QueryResultFilter::class)]
class QueryResultFilterTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testFilterResolvesComputedIndex(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 2');
        $filter = new QueryResultFilter($token);

        $collection = [
            'foo' => 3,
            5 => 'bar',
        ];

        self::assertSame(['bar'], $filter->filter($collection));
    }

    /**
     * @throws JSONPathException
     */
    public function testFilterReturnsEmptyWhenLengthExceeded(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.length + 1');
        $filter = new QueryResultFilter($token);

        $collection = ['a', 'b'];

        self::assertSame([], $filter->filter($collection));
    }

    /**
     * @throws JSONPathException
     */
    #[DataProvider('arithmeticProvider')]
    public function testFilterSupportsAllArithmeticOperators(string $expression, int|float $expectedIndex): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, $expression);
        $filter = new QueryResultFilter($token);

        $collection = [
            'value' => 4,
            $expectedIndex => 'found',
        ];

        self::assertSame(['found'], $filter->filter($collection));
    }

    /**
     * @return list<array{string, int|float}>
     */
    public static function arithmeticProvider(): array
    {
        return [
            ['@.value - 1', 3],
            ['@.value * 2', 8],
            ['@.value / 2', 2],
        ];
    }

    /**
     * @throws JSONPathException
     */
    public function testFilterFallsBackToLengthWhenKeyMissing(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.length - 1');
        $filter = new QueryResultFilter($token);

        $collection = ['zero', 'one'];

        self::assertSame(['one'], $filter->filter($collection));
    }

    /**
     * @throws JSONPathException
     */
    public function testFilterReturnsEmptyWhenKeyNotFound(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.missing + 1');
        $filter = new QueryResultFilter($token);

        self::assertSame([], $filter->filter(['foo' => 'bar']));
    }

    /**
     * @throws JSONPathException
     */
    public function testFilterReturnsEmptyWhenComputedIndexMissing(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 10');
        $filter = new QueryResultFilter($token);

        self::assertSame([], $filter->filter(['foo' => 1]));
    }

    /**
     * @throws JSONPathException
     */
    public function testFilterReturnsEmptyWhenResultKeyMissing(): void
    {
        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 100');
        $filter = new QueryResultFilter($token);

        self::assertSame([], $filter->filter(['foo' => 1]));
    }

    public function testUnsupportedOperatorThrows(): void
    {
        $this->expectException(JSONPathException::class);

        $token = new JSONPathToken(TokenType::QueryResult, '@.foo ^ 2');
        new QueryResultFilter($token)->filter(['foo' => 1]);
    }
}


================================================
FILE: tests/RecursiveFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use Flow\JSONPath\Filters\RecursiveFilter;
use Flow\JSONPath\JSONPathException;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(RecursiveFilter::class)]
class RecursiveFilterTest extends TestCase
{
    /**
     * @throws JSONPathException
     */
    public function testRecursesThroughNestedArraysAndObjects(): void
    {
        $token = new JSONPathToken(TokenType::Recursive, null);
        $filter = new RecursiveFilter($token);

        $nestedObject = (object)['inner' => ['value' => 3]];
        $data = ['obj' => $nestedObject, 'scalar' => 1];

        $result = $filter->filter($data);

        self::assertSame($nestedObject, $result[0]['obj']);
        self::assertSame(
            [
                ['inner' => ['value' => 3]],
                ['value' => 3],
            ],
            \array_slice($result, 1)
        );
    }
}


================================================
FILE: tests/SliceFilterTest.php
================================================
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

declare(strict_types=1);

namespace Flow\JSONPath\Test;

use ArrayObject;
use Flow\JSONPath\Filters\SliceFilter;
use Flow\JSONPath\JSONPathToken;
use Flow\JSONPath\TokenType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(SliceFilter::class)]
class SliceFilterTest extends TestCase
{
    /**
     * @param array<string, int|null> $slice
     * @param array<array-key, mixed>|object $input
     * @param array<int, mixed> $expected
     */
    #[DataProvider('sliceProvider')]
    public function testFilterHandlesNegativeAndNullBounds(array $slice, array|object $input, array $expected): void
    {
        $token = new JSONPathToken(TokenType::Slice, $slice);
        $filter = new SliceFilter($token);

        self::assertSame($expected, $filter->filter($input));
    }

    /**
     * @return iterable<string, array{array<string, int|null>, array<array-key, mixed>|object, array<int, mixed>}>
     */
    public static function edgeCaseProvider(): iterable
    {
        yield 'step zero returns empty' => [
            ['start' => 0, 'end' => null, 'step' => 0],
            ['a', 'b'],
            [],
        ];

        yield 'start beyond length yields empty' => [
            ['start' => 5, 'end' => null, 'step' => 1],
            ['a', 'b'],
            [],
        ];

        yield 'end beyond length clamps to length' => [
            ['start' => 0, 'end' => 10, 'step' => 1],
            ['a', 'b'],
            ['a', 'b'],
        ];

        yield 'negative step with null bounds reverses' => [
            ['start' => null, 'end' => null, 'step' => -1],
            ['a', 'b', 'c'],
            ['c', 'b', 'a'],
        ];

        yield 'positive step with end below zero yields empty' => [
            ['start' => 0, 'end' => -10, 'step' => 1],
            ['a', 'b', 'c'],
            [],
        ];

        yield 'negative step with start far below length clamps to -1' => [
            ['start' => -5, 'end' => null, 'step' => -1],
            ['a', 'b', 'c'],
            [],
        ];

        yield 'negative step with start beyond length clamps to last index' => [
            ['start' => 10, 'end' => null, 'step' => -1],
            ['a', 'b', 'c'],
            ['c', 'b', 'a'],
        ];

        yield 'negative step with end beyond length clamps end' => [
            ['start' => 1, 'end' => 10, 'step' => -1],
            ['a', 'b', 'c'],
            [],
        ];
        yield 'negative step with very negative start clamps to -1 and high end clamps to length' => [
            ['start' => -5, 'end' => 10, 'step' => -1],
            ['a', 'b', 'c'],
            [],
        ];
        yield 'negative step with end far below zero still collects prefix' => [
            ['start' => 1, 'end' => -10, 'step' => -1],
            ['a', 'b', 'c'],
            ['b', 'a'],
        ];

        yield 'non countable object yields empty' => [
            ['start' => 0, 'end' => null, 'step' => 1],
            (object)['a' => 1],
            [],
        ];
    }

    /**
     * @param array<string, int|null> $slice
     * @param array<array-key, mixed> $input
     * @param array<int, mixed> $expected
     */
    #[DataProvider('edgeCaseProvider')]
    public function testEdgeCases(array $slice, array|object $input, array $expected): void
    {
        $token = new JSONPathToken(TokenType::Slice, $slice);
        $filter = new SliceFilter($token);

        self::assertSame($expected, $filter->filter($input));
    }

    /**
     * @return array<string, array{array<string, int|null>, array<array-key, mixed>|object, array<int, mixed>}>
     */
    public static function sliceProvider(): array
    {
        return [
            'negative start clamps at zero' => [
                ['start' => -10, 'end' => 2, 'step' => 1],
                ['a', 'b', 'c'],
                ['a', 'b'],
            ],
            'negative end wraps from length' => [
                ['start' => 0, 'end' => -1, 'step' => 1],
                ['a', 'b', 'c'],
                ['a', 'b'],
            ],
            'nulls default to full length' => [
                ['start' => null, 'end' => null, 'step' => 2],
                ['a', 'b', 'c', 'd'],
                ['a', 'c'],
            ],
            'negative step slices in reverse order' => [
                ['start' => 2, 'end' => 0, 'step' => -1],
                ['a', 'b', 'c'],
                ['c', 'b'],
            ],
            'works with array object' => [
                ['start' => 0, 'end' => 2, 'step' => 1],
                new ArrayObject(['a', 'b', 'c']),
                ['a', 'b'],
            ],
        ];
    }
}
Download .txt
gitextract_xceowo_u/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.md
│   │   ├── documentation.md
│   │   └── feature.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── diff.json
│   ├── php-syntax.json
│   └── workflows/
│       ├── Test.yml
│       ├── UpdateContributors.yml
│       └── codestyle.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│   ├── AccessHelper.php
│   ├── Filters/
│   │   ├── AbstractFilter.php
│   │   ├── IndexFilter.php
│   │   ├── IndexesFilter.php
│   │   ├── QueryMatchFilter.php
│   │   ├── QueryResultFilter.php
│   │   ├── RecursiveFilter.php
│   │   └── SliceFilter.php
│   ├── JSONPath.php
│   ├── JSONPathException.php
│   ├── JSONPathLexer.php
│   ├── JSONPathToken.php
│   └── TokenType.php
└── tests/
    ├── AccessHelperTest.php
    ├── IndexFilterTest.php
    ├── IndexesFilterTest.php
    ├── JSONPathCoreTest.php
    ├── JSONPathIntegrationTest.php
    ├── JSONPathLexerTest.php
    ├── JSONPathTokenTest.php
    ├── QueryMatchFilterTest.php
    ├── QueryResultFilterTest.php
    ├── RecursiveFilterTest.php
    └── SliceFilterTest.php
Download .txt
SYMBOL INDEX (144 symbols across 23 files)

FILE: src/AccessHelper.php
  class AccessHelper (line 16) | class AccessHelper
    method collectionKeys (line 21) | public static function collectionKeys(mixed $collection): array
    method isCollectionType (line 30) | public static function isCollectionType(mixed $collection): bool
    method keyExists (line 35) | public static function keyExists(mixed $collection, int|string|null $k...
    method getValue (line 66) | public static function getValue(mixed $collection, int|string|null $ke...
    method getValueByIndex (line 98) | private static function getValueByIndex(mixed $collection, int $key): ...
    method setValue (line 126) | public static function setValue(mixed &$collection, int|string|null $k...
    method unsetValue (line 145) | public static function unsetValue(mixed &$collection, int|string|null ...
    method arrayValues (line 167) | public static function arrayValues(mixed $collection): array

FILE: src/Filters/AbstractFilter.php
  class AbstractFilter (line 16) | abstract class AbstractFilter
    method __construct (line 22) | public function __construct(protected JSONPathToken $token, int $optio...
    method setRootData (line 27) | public function setRootData(mixed $root): void
    method filter (line 36) | abstract public function filter(array|object $collection): array;

FILE: src/Filters/IndexFilter.php
  class IndexFilter (line 16) | class IndexFilter extends AbstractFilter
    method filter (line 22) | public function filter(array|object $collection): array

FILE: src/Filters/IndexesFilter.php
  class IndexesFilter (line 19) | class IndexesFilter extends AbstractFilter
    method filter (line 26) | public function filter(array|object $collection): array

FILE: src/Filters/QueryMatchFilter.php
  class QueryMatchFilter (line 23) | class QueryMatchFilter extends AbstractFilter
    method filter (line 41) | public function filter(array|object $collection): array
    method isNumber (line 258) | protected function isNumber(mixed $value): bool
    method resolveComparisonValue (line 266) | private function resolveComparisonValue(mixed $comparisonValue, mixed ...
    method normalizeKey (line 294) | private function normalizeKey(mixed $key): int|string|null
    method isPathComparison (line 303) | private function isPathComparison(mixed $comparisonValue): bool
    method evaluateConstantExpression (line 308) | private function evaluateConstantExpression(string $expression): ?bool
    method evaluateLiteralExpression (line 335) | private function evaluateLiteralExpression(string $expression, array|o...
    method evaluateExpressionWithTrailingLiteral (line 358) | private function evaluateExpressionWithTrailingLiteral(
    method decodeLiteral (line 384) | private function decodeLiteral(string $literal): mixed
    method isTruthy (line 399) | private function isTruthy(mixed $value): bool
    method compareEquals (line 404) | protected function compareEquals(mixed $a, mixed $b): bool
    method deepEqual (line 432) | private function deepEqual(array $a, array $b): bool
    method compareLessThan (line 454) | protected function compareLessThan(mixed $a, mixed $b): bool

FILE: src/Filters/QueryResultFilter.php
  class QueryResultFilter (line 16) | class QueryResultFilter extends AbstractFilter
    method filter (line 22) | public function filter(array|object $collection): array

FILE: src/Filters/RecursiveFilter.php
  class RecursiveFilter (line 16) | class RecursiveFilter extends AbstractFilter
    method filter (line 23) | public function filter(array|object $collection): array
    method recurse (line 38) | private function recurse(array &$result, array|object $data): void

FILE: src/Filters/SliceFilter.php
  class SliceFilter (line 15) | class SliceFilter extends AbstractFilter
    method filter (line 20) | public function filter(array|object $collection): array
    method normalizeForPositiveStep (line 66) | private function normalizeForPositiveStep(int $length, ?int $start, ?i...
    method normalizeForNegativeStep (line 98) | private function normalizeForNegativeStep(int $length, ?int $start, ?i...

FILE: src/JSONPath.php
  class JSONPath (line 23) | class JSONPath implements ArrayAccess, Iterator, JsonSerializable, Count...
    method __construct (line 34) | final public function __construct(mixed $data = [], int $options = 0)
    method find (line 47) | public function find(string $expression): self
    method first (line 73) | public function first(): mixed
    method last (line 89) | public function last(): mixed
    method firstKey (line 105) | public function firstKey(): string|int|null
    method lastKey (line 119) | public function lastKey(): string|int|null
    method parseTokens (line 134) | public function parseTokens(string $expression): array
    method getData (line 150) | public function getData(): mixed
    method __get (line 158) | public function __get(string|int $key): mixed
    method offsetExists (line 163) | #[Override]
    method offsetGet (line 169) | #[Override]
    method offsetSet (line 179) | #[Override]
    method offsetUnset (line 189) | #[Override]
    method jsonSerialize (line 195) | #[Override]
    method current (line 201) | #[Override]
    method next (line 209) | #[Override]
    method key (line 215) | #[Override]
    method valid (line 221) | #[Override]
    method rewind (line 227) | #[Override]
    method count (line 233) | #[Override]

FILE: src/JSONPathException.php
  class JSONPathException (line 15) | class JSONPathException extends Exception

FILE: src/JSONPathLexer.php
  class JSONPathLexer (line 13) | class JSONPathLexer
    method __construct (line 45) | public function __construct(string $expression)
    method parseExpressionTokens (line 80) | public function parseExpressionTokens(): array
    method lookAhead (line 165) | protected function lookAhead(int $pos, int $forward = 1): ?string
    method atEnd (line 170) | protected function atEnd(int $pos): bool
    method parseExpression (line 179) | public function parseExpression(): array
    method createToken (line 187) | protected function createToken(string $value): JSONPathToken
    method parseSlice (line 324) | private function parseSlice(string $tokenValue): array
    method isEscaped (line 335) | private function isEscaped(string $tokenValue): bool
    method decodeQuotedIndex (line 356) | private function decodeQuotedIndex(string $tokenValue, string $quote):...

FILE: src/JSONPathToken.php
  class JSONPathToken (line 21) | readonly class JSONPathToken
    method __construct (line 23) | public function __construct(
    method buildFilter (line 32) | public function buildFilter(int $options): AbstractFilter

FILE: tests/AccessHelperTest.php
  class AccessHelperTest (line 22) | #[CoversClass(AccessHelper::class)]
    method testKeyExistsRespectsMagicGet (line 25) | public function testKeyExistsRespectsMagicGet(): void
    method testKeyExistsSupportsArrayAccessAndNegativeIndex (line 48) | public function testKeyExistsSupportsArrayAccessAndNegativeIndex(): void
    method testGetValueCoversMagicArrayAndArrayAccess (line 79) | public function testGetValueCoversMagicArrayAndArrayAccess(): void
    method testGetValueByIndexSupportsTraversableAndNegativeOffset (line 131) | public function testGetValueByIndexSupportsTraversableAndNegativeOffse...
    method testGetValueNullCases (line 144) | public function testGetValueNullCases(): void
    method testArrayValuesThrowsOnInvalidType (line 153) | public function testArrayValuesThrowsOnInvalidType(): void
    method testArrayValuesCastsObject (line 162) | public function testArrayValuesCastsObject(): void
    method testGetValueByIndexReturnsNullWhenOutOfRange (line 169) | public function testGetValueByIndexReturnsNullWhenOutOfRange(): void
    method testKeyExistsAndCollectionHelpers (line 178) | public function testKeyExistsAndCollectionHelpers(): void
    method testSetAndUnsetValueAcrossTypes (line 188) | public function testSetAndUnsetValueAcrossTypes(): void

FILE: tests/IndexFilterTest.php
  class IndexFilterTest (line 22) | #[CoversClass(IndexFilter::class)]
    method testArrayValueTokenReturnsOnlyExistingKeys (line 28) | public function testArrayValueTokenReturnsOnlyExistingKeys(): void
    method testSingleIndexWorksForObjectsAndArrayAccess (line 42) | public function testSingleIndexWorksForObjectsAndArrayAccess(): void
    method testWildcardReturnsValuesAndLengthReturnsCount (line 57) | public function testWildcardReturnsValuesAndLengthReturnsCount(): void
    method testReturnsEmptyWhenKeyMissing (line 71) | public function testReturnsEmptyWhenKeyMissing(): void
    method testJSONPathFindOnScalarProducesEmptyCollection (line 81) | public function testJSONPathFindOnScalarProducesEmptyCollection(): void

FILE: tests/IndexesFilterTest.php
  class IndexesFilterTest (line 20) | #[CoversClass(IndexesFilter::class)]
    method testReturnsSliceAndExplicitIndexes (line 26) | public function testReturnsSliceAndExplicitIndexes(): void
    method testSupportsQueryAndWildcard (line 41) | public function testSupportsQueryAndWildcard(): void

FILE: tests/JSONPathCoreTest.php
  class JSONPathCoreTest (line 18) | class JSONPathCoreTest extends TestCase
    method testFindAndHelpers (line 23) | public function testFindAndHelpers(): void
    method testOffsetAccessAndIteration (line 48) | public function testOffsetAccessAndIteration(): void
    method testParseTokensCachesResults (line 80) | public function testParseTokensCachesResults(): void
    method testJsonSerializeAndMagicGet (line 90) | public function testJsonSerializeAndMagicGet(): void
    method testFindOnScalarReturnsEmptyResult (line 110) | public function testFindOnScalarReturnsEmptyResult(): void

FILE: tests/JSONPathIntegrationTest.php
  class JSONPathIntegrationTest (line 18) | class JSONPathIntegrationTest extends TestCase
    method testArrayObjectTraversal (line 23) | public function testArrayObjectTraversal(): void
    method testDashedIndexIsParsedWithoutQuotes (line 40) | public function testDashedIndexIsParsedWithoutQuotes(): void
    method testSlicesResolveViaPublicApi (line 52) | public function testSlicesResolveViaPublicApi(): void

FILE: tests/JSONPathLexerTest.php
  class JSONPathLexerTest (line 20) | #[CoversClass(JSONPathLexer::class)]
    method testParsesExpressions (line 27) | #[DataProvider('expressionProvider')]
    method expressionProvider (line 47) | public static function expressionProvider(): iterable
    method testIndexBadlyFormed (line 270) | public function testIndexBadlyFormed(): void
    method testRecursiveBadlyFormed (line 281) | public function testRecursiveBadlyFormed(): void
    method testEmptyExpressionsReturnNoTokens (line 292) | public function testEmptyExpressionsReturnNoTokens(): void
    method testSingleCharacterExpressionNormalized (line 301) | public function testSingleCharacterExpressionNormalized(): void
    method testUnclosedBracketThrowsAfterFinalFlush (line 309) | public function testUnclosedBracketThrowsAfterFinalFlush(): void

FILE: tests/JSONPathTokenTest.php
  class JSONPathTokenTest (line 24) | #[CoversClass(JSONPathToken::class)]
    method testBuildFilterReturnsExpectedTypes (line 27) | public function testBuildFilterReturnsExpectedTypes(): void
    method testConstructorSetsProperties (line 60) | public function testConstructorSetsProperties(): void

FILE: tests/QueryMatchFilterTest.php
  class QueryMatchFilterTest (line 23) | #[CoversClass(QueryMatchFilter::class)]
    method filterProvider (line 29) | public static function filterProvider(): iterable
    method testFilterScenarios (line 186) | #[DataProvider('filterProvider')]
    method constantExpressionProvider (line 197) | public static function constantExpressionProvider(): iterable
    method testConstantExpressions (line 213) | #[DataProvider('constantExpressionProvider')]
    method testShorthandTokenValueArrayFiltersTruthyNodes (line 225) | public function testShorthandTokenValueArrayFiltersTruthyNodes(): void
    method testMalformedFilterThrowsRuntimeException (line 238) | public function testMalformedFilterThrowsRuntimeException(): void
    method testLiteralOnlyFilterExpressionsReturnWholeCollectionOrEmpty (line 249) | public function testLiteralOnlyFilterExpressionsReturnWholeCollectionO...
    method testLogicalExpressionsWithLiteralRightOperand (line 260) | public function testLogicalExpressionsWithLiteralRightOperand(): void
    method testEmptyFilterExpressionReturnsEmpty (line 280) | public function testEmptyFilterExpressionReturnsEmpty(): void
    method testNormalizeKeyCastsNumericStrings (line 288) | public function testNormalizeKeyCastsNumericStrings(): void

FILE: tests/QueryResultFilterTest.php
  class QueryResultFilterTest (line 21) | #[CoversClass(QueryResultFilter::class)]
    method testFilterResolvesComputedIndex (line 27) | public function testFilterResolvesComputedIndex(): void
    method testFilterReturnsEmptyWhenLengthExceeded (line 43) | public function testFilterReturnsEmptyWhenLengthExceeded(): void
    method testFilterSupportsAllArithmeticOperators (line 56) | #[DataProvider('arithmeticProvider')]
    method arithmeticProvider (line 73) | public static function arithmeticProvider(): array
    method testFilterFallsBackToLengthWhenKeyMissing (line 85) | public function testFilterFallsBackToLengthWhenKeyMissing(): void
    method testFilterReturnsEmptyWhenKeyNotFound (line 98) | public function testFilterReturnsEmptyWhenKeyNotFound(): void
    method testFilterReturnsEmptyWhenComputedIndexMissing (line 109) | public function testFilterReturnsEmptyWhenComputedIndexMissing(): void
    method testFilterReturnsEmptyWhenResultKeyMissing (line 120) | public function testFilterReturnsEmptyWhenResultKeyMissing(): void
    method testUnsupportedOperatorThrows (line 128) | public function testUnsupportedOperatorThrows(): void

FILE: tests/RecursiveFilterTest.php
  class RecursiveFilterTest (line 20) | #[CoversClass(RecursiveFilter::class)]
    method testRecursesThroughNestedArraysAndObjects (line 26) | public function testRecursesThroughNestedArraysAndObjects(): void

FILE: tests/SliceFilterTest.php
  class SliceFilterTest (line 21) | #[CoversClass(SliceFilter::class)]
    method testFilterHandlesNegativeAndNullBounds (line 29) | #[DataProvider('sliceProvider')]
    method edgeCaseProvider (line 41) | public static function edgeCaseProvider(): iterable
    method testEdgeCases (line 113) | #[DataProvider('edgeCaseProvider')]
    method sliceProvider (line 125) | public static function sliceProvider(): array
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (143K chars).
[
  {
    "path": ".gitattributes",
    "chars": 15,
    "preview": "*.php diff=php\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 91,
    "preview": "github: softcreatr\r\ncustom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4']\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "chars": 1233,
    "preview": "---\r\nname: 🐛 Bug Report\r\nabout: Submit a bug report, to help us improve.\r\nlabels: \"bug\"\r\n---\r\n\r\n## 🐛 Bug Report\r\n\r\n(A cl"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "chars": 341,
    "preview": "---\r\nname: 📚 Documentation\r\nabout: Report an issue related to documentation.\r\nlabels: \"documentation\"\r\n---\r\n\r\n## 📚 Docum"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.md",
    "chars": 632,
    "preview": "---\r\nname: 💡 Feature / Idea\r\nabout: Submit a proposal for a new feature.\r\nlabels: \"feature\"\r\n---\r\n\r\n## 💡 Feature / Idea\r"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 624,
    "preview": "<!--\r\nThank you for sending the PR! We appreciate you spending the time to work on these changes.\r\n\r\nHelp us understand "
  },
  {
    "path": ".github/diff.json",
    "chars": 190,
    "preview": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"diff\",\n      \"pattern\": [\n        {\n          \"regexp\": \"--- a/(.*)\",\n    "
  },
  {
    "path": ".github/php-syntax.json",
    "chars": 282,
    "preview": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"php -l\",\n      \"pattern\": [\n        {\n          \"regexp\": \"^\\\\s*(PHP\\\\s+)?"
  },
  {
    "path": ".github/workflows/Test.yml",
    "chars": 1964,
    "preview": "name: Test\n\non:\n  push:\n    paths:\n      - '**.php'\n      - 'composer.json'\n    branches:\n      - 'main'\n  pull_request:"
  },
  {
    "path": ".github/workflows/UpdateContributors.yml",
    "chars": 406,
    "preview": "name: Update Contributors\n\non: [ push, workflow_dispatch]\n\njobs:\n  Update:\n    runs-on: ubuntu-latest\n\n    steps:\n      "
  },
  {
    "path": ".github/workflows/codestyle.yml",
    "chars": 547,
    "preview": "name: Code Style\n\non:\n  push:\n    paths:\n    - '**.php'\n  pull_request:\n    paths:\n    - '**.php'\n\njobs:\n  php:\n    name"
  },
  {
    "path": ".gitignore",
    "chars": 97,
    "preview": ".idea\r\n.phpunit.result.cache\r\nvendor\r\n.php_cs.cache\r\ncomposer.lock\r\ncomposer.phar\r\n.phpcs-cache\r\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "chars": 5232,
    "preview": "<?php\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\n$finder = Finder::create()\n    ->in(__DIR__)\n    ->notPath('vendor"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 8228,
    "preview": "# Changelog\r\n\r\n### 1.0.2\n- Fixed tokenizer handling for quoted bracket keys containing `$` so literals like `['[$the.siz"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3410,
    "preview": "# Contributor Covenant Code of Conduct\r\n\r\n## Our Pledge\r\n\r\nIn the interest of fostering an open and welcoming environmen"
  },
  {
    "path": "LICENSE.md",
    "chars": 1179,
    "preview": "MIT License\r\n\r\nOriginal work - Copyright (c) 2018 Flow Communications\r\nModified work - Copyright (c) 2020 Sascha Greuel "
  },
  {
    "path": "README.md",
    "chars": 10631,
    "preview": "# JSONPath for PHP 8.5+\n\n[![Build](https://img.shields.io/github/actions/workflow/status/SoftCreatR/JSONPath/.github/wor"
  },
  {
    "path": "composer.json",
    "chars": 1461,
    "preview": "{\n  \"name\": \"softcreatr/jsonpath\",\n  \"description\": \"JSONPath implementation for parsing, searching and flattening array"
  },
  {
    "path": "phpcs.xml",
    "chars": 752,
    "preview": "<?xml version=\"1.0\"?>\n<ruleset\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSche"
  },
  {
    "path": "phpstan.neon.dist",
    "chars": 130,
    "preview": "parameters:\n    treatPhpDocTypesAsCertain: false\n    level: 6\n    paths:\n        - src\n        - tests\n    tmpDir: .phps"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 459,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNam"
  },
  {
    "path": "src/AccessHelper.php",
    "chars": 4861,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/AbstractFilter.php",
    "chars": 837,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/IndexFilter.php",
    "chars": 1371,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/IndexesFilter.php",
    "chars": 2122,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/QueryMatchFilter.php",
    "chars": 16553,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/QueryResultFilter.php",
    "chars": 1528,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/RecursiveFilter.php",
    "chars": 1118,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/Filters/SliceFilter.php",
    "chars": 3042,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/JSONPath.php",
    "chars": 5385,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/JSONPathException.php",
    "chars": 271,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/JSONPathLexer.php",
    "chars": 11627,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/JSONPathToken.php",
    "chars": 1244,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "src/TokenType.php",
    "chars": 406,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/AccessHelperTest.php",
    "chars": 7260,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/IndexFilterTest.php",
    "chars": 2343,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/IndexesFilterTest.php",
    "chars": 1439,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/JSONPathCoreTest.php",
    "chars": 3251,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/JSONPathIntegrationTest.php",
    "chars": 1498,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/JSONPathLexerTest.php",
    "chars": 8934,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/JSONPathTokenTest.php",
    "chars": 1925,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/QueryMatchFilterTest.php",
    "chars": 9871,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/QueryResultFilterTest.php",
    "chars": 3649,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/RecursiveFilterTest.php",
    "chars": 1146,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  },
  {
    "path": "tests/SliceFilterTest.php",
    "chars": 4852,
    "preview": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  "
  }
]

About this extraction

This page contains the full source code of the SoftCreatR/JSONPath GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (131.3 KB), approximately 33.9k tokens, and a symbol index with 144 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!