nandan108/prop-path

A composable, JSONPath-inspired query engine for deep property access in PHP

v0.2.2 2025-07-05 21:07 UTC

This package is auto-updated.

Last update: 2025-07-05 21:13:10 UTC


README

CI Coverage Style Packagist Psalm Level

TL;DR PropPath is a powerful query engine for PHP that lets you extract deeply nested values from arrays and objects using concise, expressive syntax. Inspired by JSONPath but tailored for modern PHP, it supports recursive traversal, multi-key mapping, fallback resolution, bracket grouping, and structured mode โ€” all compiled into fast, reusable closures.

What is PropPath?

PropPath is a feature-rich, extensible query engine for extracting values from complex PHP object graphs, arrays, or a mix of both. Inspired by JSONPath and built for modern PHP codebases, PropPath compiles string-based or structured path expressions into efficient extractor closures that traverse nested structures using a powerful set of operators.

It powers advanced features in the DTO Toolkit, enabling concise and expressive mapping between input payloads and strongly typed DTOs.

Use cases and philosophy

PropPath is designed for:

  • Structured data extraction from deeply nested objects or mixed arrays
  • Declarative field mapping in DTO systems, data transformation layers, or form normalizers
  • Reusable compiled resolvers, allowing precompiled paths to be cached or reused

It follows a few guiding principles:

  • Minimalism: Do one thing well โ€” extract values, not transform or mutate them.
  • Expressive and powerful: Supports structured extraction, shallow and recursive wildcards, multi-key mapping, fallback resolution, flattening, and more.
  • Clarity over magic: Although expressive, the syntax is designed to be predictable and consistent.

๐Ÿ“ฆ Installation

composer require nandan108/prop-path

๐Ÿš€ Quick Start

PropPath compiles a path string (or structured array) into an extractor closure:

use Nandan108\PropPath\PropPath;
$roots = ['dto' => ['user' => ['email' => 'jane@example.com']]];
$extractor = PropPath::extract('$dto.user.email', $roots);
// $email === 'jane@example.com'

The compiled closure takes an associative array of roots as its argument.

๐Ÿ“š Nested Example

$data = [
    'dto' => [
        'user' => [
            'name' => 'Jane',
            'email' => 'jane@example.com',
            'addresses' => [
                'home' => ['city' => 'Geneva', 'zip' => '1201'],
                'office' => ['city' => 'Vernier', 'zip' => '1214', 'phone' => '1234'],
            ],
        ],
    ],
    'context' => ['request' => ['search' => 'foo']],
];

$extractor = PropPath::compile('$dto.user.addresses.home.city');
$homeCity = $extractor($data); // 'Geneva'

// direct extraction:
PropPath::extract('$dto.user["homeCity" => addresses.home.city]', $data);
// ['homeCity' => 'Geneva']

PropPath::extract('user[
    "zips"  => addresses[*[city => zip]]@~,
    "phone" => [**phone ?? "no phone"],
    "fax"   => [**fax ?? "no fax"],
    $context.request.@search
]', $data);
// $result === [
//     'zips' => ['Geneva' => '1201', 'Vernier' => 1214],
//     'phone' => '1234',
//     'fax' => 'no fax',
//     'search' => 'foo',
// ];

๐Ÿงฉ Syntax Reference

Find the full syntax reference at docs/Syntax.md

๐Ÿงต Structured Mode

Instead of a single path string, you can pass an array structure:

$roots = ['root' => ['path' => ['a', 'b', 'c']]];
$result = PropPath::extract(['foo' => 'path.0', ['path.1', ['path.2']]], $roots);
// ['foo' => 'a', ['b', ['c']]]

This allows you to mirror a desired shape without building complex bracket paths.

๐Ÿง  How It Compares to JSONPath

PropPath is inspired by JSONPath but diverges where needed for better PHP ergonomics:

Feature JSONPath PropPath
Root marker $ $, $dto, $context, etc.
Wildcard * *, with depth control
Recursive descent .. **
Filters โœ… ๐Ÿ”ธ Not supported (may be added later via symfony/expression-language)
Multi-key extraction โŒ โœ… [foo, bar] or ['x' => path]
Fallback resolution (??) โŒ โœ…
Array flattening โŒ โœ… ~, @~
Structured input mode โŒ โœ…

๐Ÿง  Container-agnostic access JSONPath uses different syntax to access objects vs arrays. PropPath does not. It uses a unified syntax for all container types (arrays, objects, or ArrayAccess).

Brackets in PropPath do not indicate container type โ€” they serve to:

  1. Build arrays from multiple values
  2. Group expressions for correct evaluation order For example, foo.*.bar.0 applies .0 per item. [foo.*.bar].0 applies .0 to the overall result.

๐Ÿ“Œ Performance Notes

PropPath compiles each path into a memoized closure using an xxh3-based hash. Structured and recursive queries (**) may be slower; typical paths are fast and safe to cache.

โš™๏ธ Tooling + Integration

๐Ÿ›  API Reference

PropPath::compile(string|array $paths, ...): \Closure
PropPath::extract(string|array $paths, array $roots, ...): mixed
PropPath::clearCache(): void
PropPath::boot(): void

โœ… Quality

  • 100% test coverage
  • Psalm: level 1 (the strictest)
  • Code style enforced with PHP-CS-Fixer:
    • Based on the @Symfony rule set
    • Aligned => for better readability
    • Disallows implicit loose comparisons (==, !=)

๐Ÿ“„ License and Attribution

MIT License ยฉ nandan108 Author: Samuel de Rougemont