softcreatr/jsonpath
A fast, eval-free JSONPath implementation for PHP 8.5+. Query arrays, objects, and ArrayAccess/traversables with unions, slices, wildcards, filters, regex, and deep equality. Includes tokenized parsing and internal caching.
Require the package — 2.2.0 adds an opt-in RFC 9535 mode and findPaths(), so pin the minor version if you rely on those:
composer require softcreatr/jsonpath:^2.2
Instantiate the parser and extract nested data. The default mode preserves pre-2.2 behavior:
use Softcreatr\JsonPath\JsonPath;
$json = '{"users": [{"name": "Alice", "role": "admin", "email": "[email protected]"}]}';
$jp = new JsonPath();
$results = $jp->find($json, '$.users[*].name'); // ['Alice']
For standards-compliant parsing, opt in to RFC 9535 mode. This is a per-instance mode; it does not change existing call sites:
$jp = new JsonPath(JsonPath::RFC9535); // opt-in mode; see README if your version uses a setter
$results = $jp->find($json, '$.users[?match(@.email, "[a-z]+@example\\.com")]');
$paths = $jp->findPaths($json, '$.users[*].name'); // normalized RFC result paths
The first thing worth trying in 2.2.0 is findPaths() when you need to know which nodes matched, not just their values, and RFC 9535 mode for queries that must be portable or strictly validated:
$jp = new JsonPath(JsonPath::RFC9535);
$matches = $jp->find($json, '$.users[?search(@.name, "lic")]');
$paths = $jp->findPaths($json, '$.users[?length(@.name) > 4]');
Skim the README for selector syntax. The RFC 9535 function list (length, count, match, search, value) and RFC 9485 I-Regexp pattern rules still apply. There is still no Laravel service provider — see Gotchas for wiring it up.
match()/search() in the path instead of array_filter over a fully decoded tree. Cheaper on memory for large payloads and keeps selection logic declarative.JsonPath::RFC9535. This enables full standard query parsing, eager type validation, Unicode and escaped member names, mixed selector unions, and RFC filter and slice semantics. Keep legacy mode for existing path strings that depend on the package's historical behavior.findPaths(): Use findPaths() when you need normalized RFC result paths, including ordered duplicate matches. This is the fastest way to debug a filter that returns more or fewer nodes than expected, and to feed match locations into logs or downstream processing.length() and count() as preconditions in job handlers and listeners:
$invalid = $jp->find($payload, '$.line_items[?count(@.taxes[*]) == 0]');
if (! empty($invalid)) {
throw new InvalidWebhookPayloadException();
}
preg_match calls in custom validation rules with match() for full-string validation of nested JSON fields. In RFC 9535 mode, invalid queries are rejected independently of document contents, so a malformed pattern fails during query validation instead of producing an empty result:
$ok = $jp->find($payload, '$.customer[?match(@.email, "[^@]+@[^@]+\\.[a-z]{2,}")]') !== [];
value() collapses a nodelist to a scalar so comparisons don't need [0] juggling, which is handy when the same path may match one node or none:
$jp->find($json, '$.meta[?value(@.version) == "2.2.0"]');
collect() for mapping and plucking:
$matches = $jp->find($json, '$.items[?(@.status == "active")]');
$ids = collect($matches)->pluck('id')->all();
config/ and inject them, rather than scattering string literals. Keep interpolated values (tenant ids, statuses) escaped/quoted — never interpolate raw user input into a filter.assertJsonPath() for asserting a single known key; reach for find()/findPaths() when you need to assert on a filtered set or on which paths matched.JsonPath instance as a singleton in a service provider (e.g. AppServiceProvider::register()) and inject it into job handlers and clients. The parser is stateless and safe to reuse. If you need both modes, bind named instances (e.g. jsonpath and jsonpath.rfc9535) rather than flipping mode at call sites.JsonPath::RFC9535. Don't assume two JsonPath instances parse the same path the same way — mode isolation is intentional and covered by the package's regression tests.try/catch.findPaths() Returns Normalized RFC Paths: Use findPaths() for normalized result paths, including ordered duplicate matches. It is not a drop-in replacement for findAll() when you need values alongside paths — keep using findAll() for value+path extraction and find() for values.match() Is Full-String, search() Is Substring: This remains the biggest behavioral surprise. match() behaves like an anchored pattern and does not behave like PHP's unanchored preg_match(). If you're porting a preg_match('/foo/') check, you almost certainly want search(). Anchors (^, $) aren't the mechanism here — don't add them.try/catch.try/catch around dynamic paths.count() Counts Nodes, length() Counts Members: count() operates on a nodelist (@.items[*]); length() operates on a string, array, or object value. Using the wrong one — or calling length() on a nodelist — is a type error, not a silent null.value() on Multiple Nodes Yields Nothing: A filter comparing value() of a multi-node nodelist simply doesn't match; it does not raise. Silent non-matches are the classic "my filter returns zero rows" cause — narrow the path first.$jp->find($json, '$.missing') returns [], not null. Guard with empty($results), especially when the filter contains functions that legitimately match nothing.json_decode error handling and lets you cache the decoded structure. Repeated decoding is the usual performance culprit, not the matching itself.\Softcreatr\JsonPath\JsonPath with domain helpers (e.g. findActiveUsers()) that pre-apply common filters, so I-Regexp patterns and type constraints live in one tested place. If you extend the class, decide explicitly whether helpers use default or RFC 9535 mode.==) will do — match()/search() are convenient, not free.How can I help you explore Laravel packages today?