An open API service indexing awesome lists of open source software.

https://github.com/felipesauer/safeaccess-inline

A dual-language library for safe nested data access using dot notation. Navigate deeply nested structures in JSON, YAML, XML, INI, ENV, NDJSON, arrays, and objects - with built-in security validation, immutable writes, and identical behavior in both PHP and TypeScript.
https://github.com/felipesauer/safeaccess-inline

dot-notation immutable javascript monorepo php safe-access typescript zero-dependency

Last synced: 19 days ago
JSON representation

A dual-language library for safe nested data access using dot notation. Navigate deeply nested structures in JSON, YAML, XML, INI, ENV, NDJSON, arrays, and objects - with built-in security validation, immutable writes, and identical behavior in both PHP and TypeScript.

Awesome Lists containing this project

README

          


safeaccess-inline logo

Safe Access Inline


A query engine for untrusted external data — with security validation, advanced filtering, and immutable writes. PHP & TypeScript, identical API, zero production dependencies.


Coverage
npm
Packagist
License: MIT
PHP 8.2+
Node.js 22+
PHP Infection MSI
JS Stryker MSI

---

## The problem

Reading nested data from external sources — API responses, uploaded files, config from third parties — requires more than null-safe access. You also need to defend against XXE in XML, anchor bombs in YAML, prototype pollution in JSON, PHP stream wrapper injection, and payload size abuse. Without a dedicated tool, that's boilerplate you write manually for every format, every endpoint.

**Without this library (XML from an external API):**

```php
// PHP
libxml_disable_entity_loader(true);
$xml = simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NOENT);
if ($xml === false) {
throw new RuntimeException('Invalid XML');
}
// validate keys against magic methods, superglobals, stream wrappers...
// enforce depth and key count limits...
$host = isset($xml->database->host) ? (string) $xml->database->host : null;
```

**With this library:**

```php
$host = Inline::fromXml($input)->get('database.host');
// XXE blocked, forbidden keys validated, depth enforced — by default
```

The same applies to JSON, YAML, INI, ENV, and NDJSON. Every format is parsed with security validation enabled out of the box. No configuration required for safe defaults.

## When to use this — and when not to

**Use this library when your data comes from outside your application:** API responses, user-uploaded files, third-party webhooks, config files you don't control.

**Use `??` or `data_get` when your data is already trusted and internal.** If you control the data source entirely and have no security concerns, native operators are simpler. This library is for the case where you don't.

## What makes it different

**Security by default.** Every public entry point validates input before it reaches your code. Forbidden key blocking, payload size limits, and depth enforcement are on by default — not something you opt into.

**A real query engine, not just dot notation.** PathQuery supports wildcard expansion, recursive descent, slices, multi-index selection, projections, and filter expressions with comparisons, logical operators, string functions, and arithmetic — none of which exist natively in PHP or JavaScript.

**Cross-language behavioral parity.** The PHP and TypeScript packages expose the same API and produce identical output for the same input. One mental model, two runtimes.

**Immutable writes.** `set()`, `remove()`, and `merge()` return new instances. The original is never modified.

**Zero production dependencies.** No transitive risk.

## Packages

| Package | Language | Install |
| ------------------------------------ | ---------------- | ------------------------------------ |
| [`safeaccess/inline`](packages/php/) | PHP 8.2+ | `composer require safeaccess/inline` |
| [`@safeaccess/inline`](packages/js/) | TypeScript (ESM) | `npm install @safeaccess/inline` |

Both packages expose the same public API surface and are tested for behavioral parity.

## Installation

### PHP

```bash
composer require safeaccess/inline
```

**Requirements:** PHP 8.2+, extensions: `json`, `simplexml`, `libxml`

**Optional:** `ext-yaml` for improved YAML parsing performance (a built-in minimal parser is used by default).

### TypeScript

```bash
npm install @safeaccess/inline
```

**Requirements:** Node.js 22+

## Quick start

### PHP

```php
use SafeAccess\Inline\Inline;

$accessor = Inline::fromJson('{"user": {"name": "Alice", "age": 30}}');

$accessor->get('user.name'); // 'Alice'
$accessor->get('user.email', 'N/A'); // 'N/A' (default when missing)
$accessor->has('user.age'); // true
$accessor->getOrFail('user.name'); // 'Alice' (throws if missing)

// Immutable writes - original is never modified
$updated = $accessor->set('user.email', 'alice@example.com');
$updated->get('user.email'); // 'alice@example.com'
$accessor->has('user.email'); // false (original unchanged)
```

### TypeScript

```typescript
import { Inline } from '@safeaccess/inline';

const accessor = Inline.fromJson('{"user": {"name": "Alice", "age": 30}}');

accessor.get('user.name'); // 'Alice'
accessor.get('user.email', 'N/A'); // 'N/A' (default when missing)
accessor.has('user.age'); // true
accessor.getOrFail('user.name'); // 'Alice' (throws if missing)

// Immutable writes - original is never modified
const updated = accessor.set('user.email', 'alice@example.com');
updated.get('user.email'); // 'alice@example.com'
accessor.has('user.email'); // false (original unchanged)
```

## Security

Safe Access Inline applies security validation **by default** on every public entry point. All keys pass through `SecurityGuard` and `SecurityParser` before being accessible.

### What gets blocked

**PHP** (`packages/php`)

| Category | Examples | Reason |
| ------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| PHP magic methods | `__construct`, `__destruct`, `__wakeup`, `__sleep`, `__toString`, `__call`, `__get`, `__set`, `__invoke`, ... | Prevent triggering PHP magic behavior via data keys |
| Prototype pollution | `__proto__`, `constructor`, `prototype` | Prevent prototype pollution attacks |
| PHP superglobals | `GLOBALS`, `_GET`, `_POST`, `_COOKIE`, `_REQUEST`, `_SERVER`, `_ENV`, `_FILES`, `_SESSION` | Prevent superglobal variable access |
| Stream wrapper URIs | `php://input`, `php://filter`, `phar://...`, `data://...`, `file://...`, ... | Prevent stream wrapper injection |

**TypeScript** (`packages/js`)

| Category | Examples | Reason |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| Prototype pollution | `__proto__`, `constructor`, `prototype` | Prevent prototype pollution attacks |
| Legacy prototype manipulation | `__defineGetter__`, `__defineSetter__`, `__lookupGetter__`, `__lookupSetter__` | Prevent legacy prototype tampering |
| Property shadow | `hasOwnProperty` | Overriding it can bypass guard checks |
| Node.js globals | `__dirname`, `__filename` | Prevent path-injection via dynamic property access |
| Protocol / stream URIs | `javascript:`, `blob:`, `ws://`, `wss://`, `node:`, `file://`, `http://`, `https://`, `ftp://`, `data:`, ... | Prevent URI injection and XSS vectors |

Keys starting with `__` are matched case-insensitively. Stream wrapper URIs and protocol schemes are matched by prefix.

### Format-specific protections

| Format | Protection |
| ------ | ----------------------------------------------------------------------- |
| XML | Rejects `fromJson($data);
```

```typescript
// TypeScript
const guard = new SecurityGuard(512, ['secret', 'internal_token']);
const accessor = Inline.withSecurityGuard(guard).fromJson(data);
```

### Disabling validation for trusted input

```php
// PHP
$accessor = Inline::withStrictMode(false)->fromJson($trustedPayload);
```

```typescript
// TypeScript
const accessor = Inline.withStrictMode(false).fromJson(trustedPayload);
```

> **Warning:** Disabling strict mode skips **all** validation — forbidden keys, payload size, depth and key-count limits. Only use with application-controlled input.

For vulnerability reports, see [SECURITY.md](SECURITY.md).

## PathQuery — advanced querying

PathQuery goes well beyond dot notation. It is the part of this library with no native equivalent in PHP or JavaScript.

### Basic dot notation

| Syntax | Example | Description |
| ----------------- | ------------------ | ----------------------------------- |
| `key.key` | `user.name` | Nested key access |
| `key.0.key` | `users.0.name` | Numeric key (array index) |
| `key\.with\.dots` | `config\.db\.host` | Escaped dots in key names |
| `$` or `$.path` | `$.user.name` | Optional `$` root prefix (stripped) |

### Full PathQuery syntax

| Syntax | Example | Description |
| --------------- | ------------------- | ----------------------------------------- |
| `[0]` | `users[0]` | Bracket index access |
| `*` or `[*]` | `users.*` | Wildcard — expand all children |
| `..key` | `..name` | Recursive descent — find key at any depth |
| `..['a','b']` | `..['name','age']` | Multi-key recursive descent |
| `[0,1,2]` | `users[0,1,2]` | Multi-index — select multiple indices |
| `['a','b']` | `['name','age']` | Multi-key — select multiple keys |
| `[0:5]` | `items[0:5]` | Slice — indices 0 through 4 |
| `[::2]` | `items[::2]` | Slice with step — every 2nd item |
| `[::-1]` | `items[::-1]` | Reverse slice |
| `[?expr]` | `users[?age>18]` | Filter predicate expression |
| `.{fields}` | `.{name, age}` | Projection — select fields |
| `.{alias: src}` | `.{fullName: name}` | Aliased projection |

### Filter expressions

```php
// PHP
$data = Inline::fromJson('[
{"name": "Alice", "age": 25, "role": "admin"},
{"name": "Bob", "age": 17, "role": "user"},
{"name": "Carol", "age": 30, "role": "admin"}
]');

// Comparison: ==, !=, >, <, >=, <=
$data->get('[?age>18]'); // Alice and Carol

// Logical: && and ||
$data->get('[?age>18 && role==\'admin\']'); // Alice and Carol

// Built-in functions: starts_with, contains, values
$data->get('[?starts_with(@.name, \'A\')]'); // Alice
$data->get('[?contains(@.name, \'ob\')]'); // Bob

// Arithmetic in predicates: +, -, *, /
$orders = Inline::fromJson('[{"price": 10, "qty": 5}, {"price": 3, "qty": 2}]');
$orders->get('[?@.price * @.qty > 20]'); // first order only
```

```typescript
// TypeScript
const data = Inline.fromJson(`[
{"name": "Alice", "age": 25, "role": "admin"},
{"name": "Bob", "age": 17, "role": "user"},
{"name": "Carol", "age": 30, "role": "admin"}
]`);

// Comparison: ==, !=, >, <, >=, <=
data.get('[?age>18]'); // Alice and Carol

// Logical: && and ||
data.get('[?age>18 && role=="admin"]'); // Alice and Carol

// Built-in functions: starts_with, contains, values
data.get('[?starts_with(@.name, "A")]'); // Alice
data.get('[?contains(@.name, "ob")]'); // Bob

// Arithmetic in predicates: +, -, *, /
const orders = Inline.fromJson('[{"price": 10, "qty": 5}, {"price": 3, "qty": 2}]');
orders.get('[?@.price * @.qty > 20]'); // first order only
```

## Supported formats

Each format has a dedicated accessor with automatic parsing and security validation applied on load.

JSON

```php
// PHP
$accessor = Inline::fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}');
$accessor->get('users.0.name'); // 'Alice'
```

```typescript
// TypeScript
const accessor = Inline.fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}');
accessor.get('users.0.name'); // 'Alice'
```

YAML

```php
// PHP
$yaml = <<get('database.credentials.user'); // 'admin'
```

```typescript
// TypeScript
const yaml = `database:
host: localhost
port: 5432
credentials:
user: admin`;

const accessor = Inline.fromYaml(yaml);
accessor.get('database.credentials.user'); // 'admin'
```

XML

```php
// PHP
$xml = 'localhost5432';
$accessor = Inline::fromXml($xml);
$accessor->get('database.host'); // 'localhost'

// Also accepts SimpleXMLElement
$accessor = Inline::fromXml(simplexml_load_string($xml));
```

```typescript
// TypeScript
const accessor = Inline.fromXml('localhost');
accessor.get('database.host'); // 'localhost'
```

INI

```php
// PHP
$accessor = Inline::fromIni("[database]\nhost=localhost\nport=5432");
$accessor->get('database.host'); // 'localhost'
```

```typescript
// TypeScript
const accessor = Inline.fromIni('[database]\nhost=localhost\nport=5432');
accessor.get('database.host'); // 'localhost'
```

ENV (dotenv)

```php
// PHP
$accessor = Inline::fromEnv("APP_NAME=MyApp\nAPP_DEBUG=true\nDB_HOST=localhost");
$accessor->get('DB_HOST'); // 'localhost'
```

```typescript
// TypeScript
const accessor = Inline.fromEnv('APP_NAME=MyApp\nAPP_DEBUG=true\nDB_HOST=localhost');
accessor.get('DB_HOST'); // 'localhost'
```

NDJSON (Newline-Delimited JSON)

```php
// PHP
$ndjson = '{"id":1,"name":"Alice"}' . "\n" . '{"id":2,"name":"Bob"}';
$accessor = Inline::fromNdjson($ndjson);
$accessor->get('0.name'); // 'Alice'
$accessor->get('1.name'); // 'Bob'
```

```typescript
// TypeScript
const accessor = Inline.fromNdjson('{"id":1,"name":"Alice"}\n{"id":2,"name":"Bob"}');
accessor.get('0.name'); // 'Alice'
accessor.get('1.name'); // 'Bob'
```

Array / Object

```php
// PHP
$accessor = Inline::fromArray(['users' => [['name' => 'Alice'], ['name' => 'Bob']]]);
$accessor->get('users.0.name'); // 'Alice'

$accessor = Inline::fromObject((object) ['name' => 'Alice', 'age' => 30]);
$accessor->get('name'); // 'Alice'
```

```typescript
// TypeScript
const accessor = Inline.fromArray({ users: [{ name: 'Alice' }, { name: 'Bob' }] });
accessor.get('users.0.name'); // 'Alice'

const objAccessor = Inline.fromObject({ name: 'Alice', age: 30 });
objAccessor.get('name'); // 'Alice'
```

Any (custom format via integration)

```php
// PHP - requires implementing ParseIntegrationInterface
$accessor = Inline::withParserIntegration(new MyCsvIntegration())->fromAny($csvString);
$accessor->get('0.column_name');
```

```typescript
// TypeScript - requires implementing ParseIntegrationInterface
const accessor = Inline.withParserIntegration(new MyCsvIntegration()).fromAny(csvString);
accessor.get('0.column_name');
```

Dynamic (by TypeFormat enum)

```php
// PHP
use SafeAccess\Inline\Enums\TypeFormat;
$accessor = Inline::from(TypeFormat::Json, '{"key": "value"}');
$accessor->get('key'); // 'value'
```

```typescript
// TypeScript
import { Inline, TypeFormat } from '@safeaccess/inline';
const accessor = Inline.from(TypeFormat.Json, '{"key": "value"}');
accessor.get('key'); // 'value'
```

## Reading & writing

All accessor methods are identical in PHP and TypeScript.

### PHP

```php
$accessor = Inline::fromJson('{"a": {"b": 1, "c": 2}}');

// Read
$accessor->get('a.b'); // 1
$accessor->get('a.missing', 'default'); // 'default'
$accessor->getOrFail('a.b'); // 1 (throws PathNotFoundException if missing)
$accessor->has('a.b'); // true
$accessor->all(); // ['a' => ['b' => 1, 'c' => 2]]
$accessor->count(); // 1 (root keys)
$accessor->count('a'); // 2 (keys under 'a')
$accessor->keys(); // ['a']
$accessor->keys('a'); // ['b', 'c']
$accessor->getMany([
'a.b' => null,
'a.x' => 'fallback',
]); // ['a.b' => 1, 'a.x' => 'fallback']
$accessor->getRaw(); // original JSON string

// Write (immutable - every write returns a new instance)
$updated = $accessor->set('a.d', 3);
$updated = $updated->remove('a.c');
$updated = $updated->merge('a', ['e' => 4]);
$updated = $updated->mergeAll(['f' => 5]);
$updated->all(); // ['a' => ['b' => 1, 'd' => 3, 'e' => 4], 'f' => 5]

// Readonly mode - block all writes
$readonly = $accessor->readonly();
$readonly->get('a.b'); // 1 (reads work)
$readonly->set('a.b', 99); // throws ReadonlyViolationException
```

### TypeScript

```typescript
const accessor = Inline.fromJson('{"a": {"b": 1, "c": 2}}');

// Read
accessor.get('a.b'); // 1
accessor.get('a.missing', 'default'); // 'default'
accessor.getOrFail('a.b'); // 1 (throws PathNotFoundException if missing)
accessor.has('a.b'); // true
accessor.all(); // { a: { b: 1, c: 2 } }
accessor.count(); // 1
accessor.count('a'); // 2
accessor.keys(); // ['a']
accessor.keys('a'); // ['b', 'c']
accessor.getMany({
'a.b': null,
'a.x': 'fallback',
}); // { 'a.b': 1, 'a.x': 'fallback' }
accessor.getRaw(); // original JSON string

// Write (immutable)
const updated = accessor.set('a.d', 3).remove('a.c').merge('a', { e: 4 }).mergeAll({ f: 5 });
updated.all(); // { a: { b: 1, d: 3, e: 4 }, f: 5 }

// Readonly mode
const readonly = accessor.readonly();
readonly.get('a.b'); // 1
readonly.set('a.b', 99); // throws ReadonlyViolationException
```

## Configure

Customize security, caching, and parsing via the builder pattern:

### PHP

```php
use SafeAccess\Inline\Inline;
use SafeAccess\Inline\Security\SecurityGuard;
use SafeAccess\Inline\Security\SecurityParser;

$accessor = Inline::withSecurityGuard(new SecurityGuard(extraForbiddenKeys: ['secret']))
->withSecurityParser(new SecurityParser(maxDepth: 5))
->withStrictMode(true)
->fromJson($untrustedInput);
```

### TypeScript

```typescript
import { Inline, SecurityGuard, SecurityParser } from '@safeaccess/inline';

const accessor = Inline.withSecurityGuard(new SecurityGuard(512, ['secret']))
.withSecurityParser(new SecurityParser({ maxDepth: 5 }))
.withStrictMode(true)
.fromJson(untrustedInput);
```

### Builder methods

| Method | Description |
| ------------------------------------ | ------------------------------------------------ |
| `withSecurityGuard(guard)` | Custom forbidden-key rules and depth limits |
| `withSecurityParser(parser)` | Custom payload size and structural limits |
| `withPathCache(cache)` | Path segment cache for repeated lookups |
| `withParserIntegration(integration)` | Custom format parser for `fromAny()` |
| `withStrictMode(false)` | Disable security validation (trusted input only) |

## Error handling

All exceptions extend `AccessorException`, making it easy to catch every library error in a single block.

### PHP

```php
use SafeAccess\Inline\Inline;
use SafeAccess\Inline\Exceptions\AccessorException;
use SafeAccess\Inline\Exceptions\InvalidFormatException;
use SafeAccess\Inline\Exceptions\SecurityException;
use SafeAccess\Inline\Exceptions\PathNotFoundException;
use SafeAccess\Inline\Exceptions\ReadonlyViolationException;

try {
$accessor = Inline::fromJson($untrustedInput);
$value = $accessor->getOrFail('config.key');
} catch (InvalidFormatException $e) {
// Malformed JSON, XML, INI, or NDJSON input
} catch (SecurityException $e) {
// Forbidden key, payload too large, depth/key-count exceeded
} catch (PathNotFoundException $e) {
// Path does not exist in the data
} catch (ReadonlyViolationException $e) {
// Write attempted on a readonly accessor
} catch (AccessorException $e) {
// Catch-all for any SafeAccess error
}
```

### TypeScript

```typescript
import {
Inline,
AccessorException,
InvalidFormatException,
SecurityException,
PathNotFoundException,
ReadonlyViolationException,
} from '@safeaccess/inline';

try {
const accessor = Inline.fromJson(untrustedInput);
const value = accessor.getOrFail('config.key');
} catch (e) {
if (e instanceof InvalidFormatException) {
/* malformed input */
}
if (e instanceof SecurityException) {
/* security violation */
}
if (e instanceof PathNotFoundException) {
/* path not found */
}
if (e instanceof ReadonlyViolationException) {
/* readonly violation */
}
if (e instanceof AccessorException) {
/* any library error */
}
}
```

### Exception hierarchy

| Exception | Extends | When |
| ---------------------------- | ---------------------------- | ------------------------------------------------------------ |
| `AccessorException` | `RuntimeException` / `Error` | Root — catch-all for any library error |
| `SecurityException` | `AccessorException` | Forbidden key, payload too large, structural limits exceeded |
| `InvalidFormatException` | `AccessorException` | Malformed JSON, XML, INI, NDJSON |
| `YamlParseException` | `InvalidFormatException` | Unsafe or malformed YAML |
| `PathNotFoundException` | `AccessorException` | `getOrFail()` on a missing path |
| `ReadonlyViolationException` | `AccessorException` | Write on a readonly accessor |
| `UnsupportedTypeException` | `AccessorException` | Unknown accessor class in `make()` |
| `ParserException` | `AccessorException` | Reserved for custom parser-level errors |

## Advanced usage

### Path cache

Cache parsed path segments for repeated lookups:

```php
// PHP - implement PathCacheInterface
$cache = new MyPathCache();
$accessor = Inline::withPathCache($cache)->fromJson($data);
$accessor->get('deeply.nested.path'); // parses path
$accessor->get('deeply.nested.path'); // cache hit - skips parsing
```

```typescript
// TypeScript - implement PathCacheInterface
const cacheMap = new Map();
const cache: PathCacheInterface = {
get: (path) => cacheMap.get(path) ?? null,
set: (path, segments) => {
cacheMap.set(path, segments);
},
has: (path) => cacheMap.has(path),
clear: () => {
cacheMap.clear();
return cache;
},
};
const accessor = Inline.withPathCache(cache).fromJson(data);
```

### Custom format integration

Add support for custom data formats by implementing `ParseIntegrationInterface`:

```php
// PHP
class CsvIntegration implements ParseIntegrationInterface
{
public function assertFormat(mixed $raw): bool
{
return is_string($raw) && str_contains($raw, ',');
}

public function parse(mixed $raw): array
{
// Parse CSV to associative array
return $parsed;
}
}

$accessor = Inline::withParserIntegration(new CsvIntegration())->fromAny($csvString);
```

```typescript
// TypeScript
const csvIntegration: ParseIntegrationInterface = {
assertFormat: (raw: unknown) => typeof raw === 'string' && (raw as string).includes(','),
parse: (raw: unknown) => {
// Parse CSV to object
return parsed;
},
};

const accessor = Inline.withParserIntegration(csvIntegration).fromAny(csvString);
```

## API reference

### `Inline` facade

#### Static factory methods

| Method | Input | Returns |
| ----------------------------- | ---------------------------------- | -------------------- |
| `fromArray(data)` | Array / plain object | `ArrayAccessor` |
| `fromObject(data)` | Object | `ObjectAccessor` |
| `fromJson(data)` | JSON `string` | `JsonAccessor` |
| `fromXml(data)` | XML `string` or `SimpleXMLElement` | `XmlAccessor` |
| `fromYaml(data)` | YAML `string` | `YamlAccessor` |
| `fromIni(data)` | INI `string` | `IniAccessor` |
| `fromEnv(data)` | dotenv `string` | `EnvAccessor` |
| `fromNdjson(data)` | NDJSON `string` | `NdjsonAccessor` |
| `fromAny(data, integration?)` | Any format | `AnyAccessor` |
| `from(typeFormat, data)` | `TypeFormat` enum | `AccessorsInterface` |
| `make(accessorClass, data)` | Accessor class | `AbstractAccessor` |

#### Accessor read methods

| Method | Returns |
| --------------------------- | --------------------------------------- |
| `get(path, default?)` | Value at path, or default |
| `getOrFail(path)` | Value or throws `PathNotFoundException` |
| `getAt(segments, default?)` | Value at key segments |
| `has(path)` | `boolean` |
| `hasAt(segments)` | `boolean` |
| `getMany(paths)` | `Record` |
| `all()` | All parsed data |
| `count(path?)` | Element count |
| `keys(path?)` | Key names |
| `getRaw()` | Original input |

#### Accessor write methods (immutable)

| Method | Description |
| ------------------------ | ---------------------- |
| `set(path, value)` | Set at path |
| `setAt(segments, value)` | Set at key segments |
| `remove(path)` | Remove at path |
| `removeAt(segments)` | Remove at key segments |
| `merge(path, value)` | Deep-merge at path |
| `mergeAll(value)` | Deep-merge at root |

#### Modifier methods

| Method | Description |
| ----------------- | -------------------------- |
| `readonly(flag?)` | Block all writes |
| `strict(flag?)` | Toggle security validation |

#### TypeFormat enum

`Array` · `Object` · `Json` · `Xml` · `Yaml` · `Ini` · `Env` · `Ndjson` · `Any`

## Comparison

| Feature | Safe Access Inline | `lodash.get` | Laravel `data_get` | `jmespath` |
| ------------------------- | ----------------------- | --------------- | ------------------ | ----------- |
| Language | PHP + TypeScript | JavaScript | PHP | Multi |
| Security validation | Built-in, on by default | None | None | None |
| Forbidden key blocking | Yes | No | No | No |
| Payload size limits | Yes | No | No | No |
| Immutable writes | Yes | No | No | N/A |
| Readonly mode | Yes | No | No | N/A |
| Multi-format support | 9 formats | Object only | Array only | Object only |
| Custom format integration | Yes | No | No | No |
| Wildcard / filter / slice | Yes | No | Partial (`*`) | Yes |
| Zero prod dependencies | Yes | lodash | Laravel | jmespath |
| Cross-language parity | PHP + TypeScript | JavaScript only | PHP only | Multi |

## Project structure

```
packages/php/ - PHP package (PSR-4, Pest, PHPStan, Infection)
packages/js/ - TypeScript package (ESM, Vitest, Stryker)
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit conventions, and pull request guidelines.

## License

[MIT](LICENSE) © Felipe Sauer