{"id":48622180,"url":"https://github.com/felipesauer/safeaccess-inline","last_synced_at":"2026-04-12T04:03:44.676Z","repository":{"id":349665753,"uuid":"1197603093","full_name":"felipesauer/safeaccess-inline","owner":"felipesauer","description":"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.","archived":false,"fork":false,"pushed_at":"2026-04-09T02:15:04.000Z","size":739,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-09T04:29:53.702Z","etag":null,"topics":["dot-notation","immutable","javascript","monorepo","php","safe-access","typescript","zero-dependency"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felipesauer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["felipesauer"]}},"created_at":"2026-03-31T18:05:42.000Z","updated_at":"2026-04-09T02:14:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/felipesauer/safeaccess-inline","commit_stats":null,"previous_names":["felipesauer/safe-access-inline","felipesauer/safeaccess-inline"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/felipesauer/safeaccess-inline","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipesauer%2Fsafeaccess-inline","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipesauer%2Fsafeaccess-inline/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipesauer%2Fsafeaccess-inline/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipesauer%2Fsafeaccess-inline/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felipesauer","download_url":"https://codeload.github.com/felipesauer/safeaccess-inline/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipesauer%2Fsafeaccess-inline/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31703501,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-11T21:17:31.016Z","status":"online","status_checked_at":"2026-04-12T02:00:06.763Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dot-notation","immutable","javascript","monorepo","php","safe-access","typescript","zero-dependency"],"created_at":"2026-04-09T04:05:01.792Z","updated_at":"2026-04-12T04:03:44.670Z","avatar_url":"https://github.com/felipesauer.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/felipesauer/safeaccess-inline/main/.github/assets/logo.svg\" width=\"80\" alt=\"safeaccess-inline logo\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eSafe Access Inline\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  A query engine for untrusted external data — with security validation, advanced filtering, and immutable writes. PHP \u0026amp; TypeScript, identical API, zero production dependencies.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://codecov.io/gh/felipesauer/safeaccess-inline\"\u003e\u003cimg src=\"https://img.shields.io/codecov/c/github/felipesauer/safeaccess-inline?label=Coverage\" alt=\"Coverage\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@safeaccess/inline\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@safeaccess/inline?label=npm\" alt=\"npm\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://packagist.org/packages/safeaccess/inline\"\u003e\u003cimg src=\"https://img.shields.io/packagist/v/safeaccess/inline?label=packagist\" alt=\"Packagist\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License: MIT\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/PHP-8.2%2B-777BB4?logo=php\u0026logoColor=white\" alt=\"PHP 8.2+\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs\u0026logoColor=white\" alt=\"Node.js 22+\"\u003e\n  \u003cimg src=\"https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/felipesauer/80c602b17107f88fb17794d4d44c94fa/raw/infection-msi.json\" alt=\"PHP Infection MSI\"\u003e\n  \u003cimg src=\"https://img.shields.io/endpoint?style=flat\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ffelipesauer%2Fsafeaccess-inline%2Fmain\" alt=\"JS Stryker MSI\"\u003e\n\u003c/p\u003e\n\n---\n\n## The problem\n\nReading 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.\n\n**Without this library (XML from an external API):**\n\n```php\n// PHP\nlibxml_disable_entity_loader(true);\n$xml = simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NOENT);\nif ($xml === false) {\n    throw new RuntimeException('Invalid XML');\n}\n// validate keys against magic methods, superglobals, stream wrappers...\n// enforce depth and key count limits...\n$host = isset($xml-\u003edatabase-\u003ehost) ? (string) $xml-\u003edatabase-\u003ehost : null;\n```\n\n**With this library:**\n\n```php\n$host = Inline::fromXml($input)-\u003eget('database.host');\n// XXE blocked, forbidden keys validated, depth enforced — by default\n```\n\nThe 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.\n\n## When to use this — and when not to\n\n**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.\n\n**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.\n\n## What makes it different\n\n**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.\n\n**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.\n\n**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.\n\n**Immutable writes.** `set()`, `remove()`, and `merge()` return new instances. The original is never modified.\n\n**Zero production dependencies.** No transitive risk.\n\n## Packages\n\n| Package                              | Language         | Install                              |\n| ------------------------------------ | ---------------- | ------------------------------------ |\n| [`safeaccess/inline`](packages/php/) | PHP 8.2+         | `composer require safeaccess/inline` |\n| [`@safeaccess/inline`](packages/js/) | TypeScript (ESM) | `npm install @safeaccess/inline`     |\n\nBoth packages expose the same public API surface and are tested for behavioral parity.\n\n## Installation\n\n### PHP\n\n```bash\ncomposer require safeaccess/inline\n```\n\n**Requirements:** PHP 8.2+, extensions: `json`, `simplexml`, `libxml`\n\n**Optional:** `ext-yaml` for improved YAML parsing performance (a built-in minimal parser is used by default).\n\n### TypeScript\n\n```bash\nnpm install @safeaccess/inline\n```\n\n**Requirements:** Node.js 22+\n\n## Quick start\n\n### PHP\n\n```php\nuse SafeAccess\\Inline\\Inline;\n\n$accessor = Inline::fromJson('{\"user\": {\"name\": \"Alice\", \"age\": 30}}');\n\n$accessor-\u003eget('user.name');           // 'Alice'\n$accessor-\u003eget('user.email', 'N/A');   // 'N/A' (default when missing)\n$accessor-\u003ehas('user.age');            // true\n$accessor-\u003egetOrFail('user.name');     // 'Alice' (throws if missing)\n\n// Immutable writes - original is never modified\n$updated = $accessor-\u003eset('user.email', 'alice@example.com');\n$updated-\u003eget('user.email');           // 'alice@example.com'\n$accessor-\u003ehas('user.email');          // false (original unchanged)\n```\n\n### TypeScript\n\n```typescript\nimport { Inline } from '@safeaccess/inline';\n\nconst accessor = Inline.fromJson('{\"user\": {\"name\": \"Alice\", \"age\": 30}}');\n\naccessor.get('user.name'); // 'Alice'\naccessor.get('user.email', 'N/A'); // 'N/A' (default when missing)\naccessor.has('user.age'); // true\naccessor.getOrFail('user.name'); // 'Alice' (throws if missing)\n\n// Immutable writes - original is never modified\nconst updated = accessor.set('user.email', 'alice@example.com');\nupdated.get('user.email'); // 'alice@example.com'\naccessor.has('user.email'); // false (original unchanged)\n```\n\n## Security\n\nSafe Access Inline applies security validation **by default** on every public entry point. All keys pass through `SecurityGuard` and `SecurityParser` before being accessible.\n\n### What gets blocked\n\n**PHP** (`packages/php`)\n\n| Category            | Examples                                                                                                      | Reason                                              |\n| ------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |\n| PHP magic methods   | `__construct`, `__destruct`, `__wakeup`, `__sleep`, `__toString`, `__call`, `__get`, `__set`, `__invoke`, ... | Prevent triggering PHP magic behavior via data keys |\n| Prototype pollution | `__proto__`, `constructor`, `prototype`                                                                       | Prevent prototype pollution attacks                 |\n| PHP superglobals    | `GLOBALS`, `_GET`, `_POST`, `_COOKIE`, `_REQUEST`, `_SERVER`, `_ENV`, `_FILES`, `_SESSION`                    | Prevent superglobal variable access                 |\n| Stream wrapper URIs | `php://input`, `php://filter`, `phar://...`, `data://...`, `file://...`, ...                                  | Prevent stream wrapper injection                    |\n\n**TypeScript** (`packages/js`)\n\n| Category                      | Examples                                                                                                     | Reason                                             |\n| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |\n| Prototype pollution           | `__proto__`, `constructor`, `prototype`                                                                      | Prevent prototype pollution attacks                |\n| Legacy prototype manipulation | `__defineGetter__`, `__defineSetter__`, `__lookupGetter__`, `__lookupSetter__`                               | Prevent legacy prototype tampering                 |\n| Property shadow               | `hasOwnProperty`                                                                                             | Overriding it can bypass guard checks              |\n| Node.js globals               | `__dirname`, `__filename`                                                                                    | Prevent path-injection via dynamic property access |\n| Protocol / stream URIs        | `javascript:`, `blob:`, `ws://`, `wss://`, `node:`, `file://`, `http://`, `https://`, `ftp://`, `data:`, ... | Prevent URI injection and XSS vectors              |\n\nKeys starting with `__` are matched case-insensitively. Stream wrapper URIs and protocol schemes are matched by prefix.\n\n### Format-specific protections\n\n| Format | Protection                                                              |\n| ------ | ----------------------------------------------------------------------- |\n| XML    | Rejects `\u003c!DOCTYPE` — prevents XXE (XML External Entity) attacks        |\n| YAML   | Blocks unsafe tags, anchors (`\u0026`), aliases (`*`), and merge keys (`\u003c\u003c`) |\n| All    | Forbidden key validation on every parsed key                            |\n\n### Structural limits\n\n| Limit                    | Default            | Description                                          |\n| ------------------------ | ------------------ | ---------------------------------------------------- |\n| `maxPayloadBytes`        | 10 MB (10,485,760) | Maximum raw string input size                        |\n| `maxKeys`                | 10,000             | Maximum total key count across the entire structure  |\n| `maxDepth`               | 512                | Maximum structural nesting depth                     |\n| `maxResolveDepth`        | 100                | Maximum recursion for path resolution and deep merge |\n| `maxCountRecursiveDepth` | 100                | Maximum recursion when counting keys                 |\n\n### Custom forbidden keys\n\n```php\n// PHP\n$guard = new SecurityGuard(extraForbiddenKeys: ['secret', 'internal_token']);\n$accessor = Inline::withSecurityGuard($guard)-\u003efromJson($data);\n```\n\n```typescript\n// TypeScript\nconst guard = new SecurityGuard(512, ['secret', 'internal_token']);\nconst accessor = Inline.withSecurityGuard(guard).fromJson(data);\n```\n\n### Disabling validation for trusted input\n\n```php\n// PHP\n$accessor = Inline::withStrictMode(false)-\u003efromJson($trustedPayload);\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.withStrictMode(false).fromJson(trustedPayload);\n```\n\n\u003e **Warning:** Disabling strict mode skips **all** validation — forbidden keys, payload size, depth and key-count limits. Only use with application-controlled input.\n\nFor vulnerability reports, see [SECURITY.md](SECURITY.md).\n\n## PathQuery — advanced querying\n\nPathQuery goes well beyond dot notation. It is the part of this library with no native equivalent in PHP or JavaScript.\n\n### Basic dot notation\n\n| Syntax            | Example            | Description                         |\n| ----------------- | ------------------ | ----------------------------------- |\n| `key.key`         | `user.name`        | Nested key access                   |\n| `key.0.key`       | `users.0.name`     | Numeric key (array index)           |\n| `key\\.with\\.dots` | `config\\.db\\.host` | Escaped dots in key names           |\n| `$` or `$.path`   | `$.user.name`      | Optional `$` root prefix (stripped) |\n\n### Full PathQuery syntax\n\n| Syntax          | Example             | Description                               |\n| --------------- | ------------------- | ----------------------------------------- |\n| `[0]`           | `users[0]`          | Bracket index access                      |\n| `*` or `[*]`    | `users.*`           | Wildcard — expand all children            |\n| `..key`         | `..name`            | Recursive descent — find key at any depth |\n| `..['a','b']`   | `..['name','age']`  | Multi-key recursive descent               |\n| `[0,1,2]`       | `users[0,1,2]`      | Multi-index — select multiple indices     |\n| `['a','b']`     | `['name','age']`    | Multi-key — select multiple keys          |\n| `[0:5]`         | `items[0:5]`        | Slice — indices 0 through 4               |\n| `[::2]`         | `items[::2]`        | Slice with step — every 2nd item          |\n| `[::-1]`        | `items[::-1]`       | Reverse slice                             |\n| `[?expr]`       | `users[?age\u003e18]`    | Filter predicate expression               |\n| `.{fields}`     | `.{name, age}`      | Projection — select fields                |\n| `.{alias: src}` | `.{fullName: name}` | Aliased projection                        |\n\n### Filter expressions\n\n```php\n// PHP\n$data = Inline::fromJson('[\n    {\"name\": \"Alice\", \"age\": 25, \"role\": \"admin\"},\n    {\"name\": \"Bob\",   \"age\": 17, \"role\": \"user\"},\n    {\"name\": \"Carol\", \"age\": 30, \"role\": \"admin\"}\n]');\n\n// Comparison: ==, !=, \u003e, \u003c, \u003e=, \u003c=\n$data-\u003eget('[?age\u003e18]');                          // Alice and Carol\n\n// Logical: \u0026\u0026 and ||\n$data-\u003eget('[?age\u003e18 \u0026\u0026 role==\\'admin\\']');       // Alice and Carol\n\n// Built-in functions: starts_with, contains, values\n$data-\u003eget('[?starts_with(@.name, \\'A\\')]');      // Alice\n$data-\u003eget('[?contains(@.name, \\'ob\\')]');        // Bob\n\n// Arithmetic in predicates: +, -, *, /\n$orders = Inline::fromJson('[{\"price\": 10, \"qty\": 5}, {\"price\": 3, \"qty\": 2}]');\n$orders-\u003eget('[?@.price * @.qty \u003e 20]');          // first order only\n```\n\n```typescript\n// TypeScript\nconst data = Inline.fromJson(`[\n    {\"name\": \"Alice\", \"age\": 25, \"role\": \"admin\"},\n    {\"name\": \"Bob\",   \"age\": 17, \"role\": \"user\"},\n    {\"name\": \"Carol\", \"age\": 30, \"role\": \"admin\"}\n]`);\n\n// Comparison: ==, !=, \u003e, \u003c, \u003e=, \u003c=\ndata.get('[?age\u003e18]'); // Alice and Carol\n\n// Logical: \u0026\u0026 and ||\ndata.get('[?age\u003e18 \u0026\u0026 role==\"admin\"]'); // Alice and Carol\n\n// Built-in functions: starts_with, contains, values\ndata.get('[?starts_with(@.name, \"A\")]'); // Alice\ndata.get('[?contains(@.name, \"ob\")]'); // Bob\n\n// Arithmetic in predicates: +, -, *, /\nconst orders = Inline.fromJson('[{\"price\": 10, \"qty\": 5}, {\"price\": 3, \"qty\": 2}]');\norders.get('[?@.price * @.qty \u003e 20]'); // first order only\n```\n\n## Supported formats\n\nEach format has a dedicated accessor with automatic parsing and security validation applied on load.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eJSON\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$accessor = Inline::fromJson('{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}');\n$accessor-\u003eget('users.0.name'); // 'Alice'\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromJson('{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}');\naccessor.get('users.0.name'); // 'Alice'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eYAML\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$yaml = \u003c\u003c\u003cYAML\ndatabase:\n  host: localhost\n  port: 5432\n  credentials:\n    user: admin\nYAML;\n\n$accessor = Inline::fromYaml($yaml);\n$accessor-\u003eget('database.credentials.user'); // 'admin'\n```\n\n```typescript\n// TypeScript\nconst yaml = `database:\n  host: localhost\n  port: 5432\n  credentials:\n    user: admin`;\n\nconst accessor = Inline.fromYaml(yaml);\naccessor.get('database.credentials.user'); // 'admin'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eXML\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$xml = '\u003cconfig\u003e\u003cdatabase\u003e\u003chost\u003elocalhost\u003c/host\u003e\u003cport\u003e5432\u003c/port\u003e\u003c/database\u003e\u003c/config\u003e';\n$accessor = Inline::fromXml($xml);\n$accessor-\u003eget('database.host'); // 'localhost'\n\n// Also accepts SimpleXMLElement\n$accessor = Inline::fromXml(simplexml_load_string($xml));\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromXml('\u003cconfig\u003e\u003cdatabase\u003e\u003chost\u003elocalhost\u003c/host\u003e\u003c/database\u003e\u003c/config\u003e');\naccessor.get('database.host'); // 'localhost'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eINI\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$accessor = Inline::fromIni(\"[database]\\nhost=localhost\\nport=5432\");\n$accessor-\u003eget('database.host'); // 'localhost'\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromIni('[database]\\nhost=localhost\\nport=5432');\naccessor.get('database.host'); // 'localhost'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eENV (dotenv)\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$accessor = Inline::fromEnv(\"APP_NAME=MyApp\\nAPP_DEBUG=true\\nDB_HOST=localhost\");\n$accessor-\u003eget('DB_HOST'); // 'localhost'\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromEnv('APP_NAME=MyApp\\nAPP_DEBUG=true\\nDB_HOST=localhost');\naccessor.get('DB_HOST'); // 'localhost'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eNDJSON (Newline-Delimited JSON)\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$ndjson = '{\"id\":1,\"name\":\"Alice\"}' . \"\\n\" . '{\"id\":2,\"name\":\"Bob\"}';\n$accessor = Inline::fromNdjson($ndjson);\n$accessor-\u003eget('0.name'); // 'Alice'\n$accessor-\u003eget('1.name'); // 'Bob'\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromNdjson('{\"id\":1,\"name\":\"Alice\"}\\n{\"id\":2,\"name\":\"Bob\"}');\naccessor.get('0.name'); // 'Alice'\naccessor.get('1.name'); // 'Bob'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eArray / Object\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\n$accessor = Inline::fromArray(['users' =\u003e [['name' =\u003e 'Alice'], ['name' =\u003e 'Bob']]]);\n$accessor-\u003eget('users.0.name'); // 'Alice'\n\n$accessor = Inline::fromObject((object) ['name' =\u003e 'Alice', 'age' =\u003e 30]);\n$accessor-\u003eget('name'); // 'Alice'\n```\n\n```typescript\n// TypeScript\nconst accessor = Inline.fromArray({ users: [{ name: 'Alice' }, { name: 'Bob' }] });\naccessor.get('users.0.name'); // 'Alice'\n\nconst objAccessor = Inline.fromObject({ name: 'Alice', age: 30 });\nobjAccessor.get('name'); // 'Alice'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eAny (custom format via integration)\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP - requires implementing ParseIntegrationInterface\n$accessor = Inline::withParserIntegration(new MyCsvIntegration())-\u003efromAny($csvString);\n$accessor-\u003eget('0.column_name');\n```\n\n```typescript\n// TypeScript - requires implementing ParseIntegrationInterface\nconst accessor = Inline.withParserIntegration(new MyCsvIntegration()).fromAny(csvString);\naccessor.get('0.column_name');\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eDynamic (by TypeFormat enum)\u003c/strong\u003e\u003c/summary\u003e\n\n```php\n// PHP\nuse SafeAccess\\Inline\\Enums\\TypeFormat;\n$accessor = Inline::from(TypeFormat::Json, '{\"key\": \"value\"}');\n$accessor-\u003eget('key'); // 'value'\n```\n\n```typescript\n// TypeScript\nimport { Inline, TypeFormat } from '@safeaccess/inline';\nconst accessor = Inline.from(TypeFormat.Json, '{\"key\": \"value\"}');\naccessor.get('key'); // 'value'\n```\n\n\u003c/details\u003e\n\n## Reading \u0026 writing\n\nAll accessor methods are identical in PHP and TypeScript.\n\n### PHP\n\n```php\n$accessor = Inline::fromJson('{\"a\": {\"b\": 1, \"c\": 2}}');\n\n// Read\n$accessor-\u003eget('a.b');                  // 1\n$accessor-\u003eget('a.missing', 'default'); // 'default'\n$accessor-\u003egetOrFail('a.b');            // 1 (throws PathNotFoundException if missing)\n$accessor-\u003ehas('a.b');                  // true\n$accessor-\u003eall();                       // ['a' =\u003e ['b' =\u003e 1, 'c' =\u003e 2]]\n$accessor-\u003ecount();                     // 1 (root keys)\n$accessor-\u003ecount('a');                  // 2 (keys under 'a')\n$accessor-\u003ekeys();                      // ['a']\n$accessor-\u003ekeys('a');                   // ['b', 'c']\n$accessor-\u003egetMany([\n    'a.b' =\u003e null,\n    'a.x' =\u003e 'fallback',\n]);                                     // ['a.b' =\u003e 1, 'a.x' =\u003e 'fallback']\n$accessor-\u003egetRaw();                    // original JSON string\n\n// Write (immutable - every write returns a new instance)\n$updated = $accessor-\u003eset('a.d', 3);\n$updated = $updated-\u003eremove('a.c');\n$updated = $updated-\u003emerge('a', ['e' =\u003e 4]);\n$updated = $updated-\u003emergeAll(['f' =\u003e 5]);\n$updated-\u003eall();                        // ['a' =\u003e ['b' =\u003e 1, 'd' =\u003e 3, 'e' =\u003e 4], 'f' =\u003e 5]\n\n// Readonly mode - block all writes\n$readonly = $accessor-\u003ereadonly();\n$readonly-\u003eget('a.b');                  // 1 (reads work)\n$readonly-\u003eset('a.b', 99);             // throws ReadonlyViolationException\n```\n\n### TypeScript\n\n```typescript\nconst accessor = Inline.fromJson('{\"a\": {\"b\": 1, \"c\": 2}}');\n\n// Read\naccessor.get('a.b'); // 1\naccessor.get('a.missing', 'default'); // 'default'\naccessor.getOrFail('a.b'); // 1 (throws PathNotFoundException if missing)\naccessor.has('a.b'); // true\naccessor.all(); // { a: { b: 1, c: 2 } }\naccessor.count(); // 1\naccessor.count('a'); // 2\naccessor.keys(); // ['a']\naccessor.keys('a'); // ['b', 'c']\naccessor.getMany({\n    'a.b': null,\n    'a.x': 'fallback',\n}); // { 'a.b': 1, 'a.x': 'fallback' }\naccessor.getRaw(); // original JSON string\n\n// Write (immutable)\nconst updated = accessor.set('a.d', 3).remove('a.c').merge('a', { e: 4 }).mergeAll({ f: 5 });\nupdated.all(); // { a: { b: 1, d: 3, e: 4 }, f: 5 }\n\n// Readonly mode\nconst readonly = accessor.readonly();\nreadonly.get('a.b'); // 1\nreadonly.set('a.b', 99); // throws ReadonlyViolationException\n```\n\n## Configure\n\nCustomize security, caching, and parsing via the builder pattern:\n\n### PHP\n\n```php\nuse SafeAccess\\Inline\\Inline;\nuse SafeAccess\\Inline\\Security\\SecurityGuard;\nuse SafeAccess\\Inline\\Security\\SecurityParser;\n\n$accessor = Inline::withSecurityGuard(new SecurityGuard(extraForbiddenKeys: ['secret']))\n    -\u003ewithSecurityParser(new SecurityParser(maxDepth: 5))\n    -\u003ewithStrictMode(true)\n    -\u003efromJson($untrustedInput);\n```\n\n### TypeScript\n\n```typescript\nimport { Inline, SecurityGuard, SecurityParser } from '@safeaccess/inline';\n\nconst accessor = Inline.withSecurityGuard(new SecurityGuard(512, ['secret']))\n    .withSecurityParser(new SecurityParser({ maxDepth: 5 }))\n    .withStrictMode(true)\n    .fromJson(untrustedInput);\n```\n\n### Builder methods\n\n| Method                               | Description                                      |\n| ------------------------------------ | ------------------------------------------------ |\n| `withSecurityGuard(guard)`           | Custom forbidden-key rules and depth limits      |\n| `withSecurityParser(parser)`         | Custom payload size and structural limits        |\n| `withPathCache(cache)`               | Path segment cache for repeated lookups          |\n| `withParserIntegration(integration)` | Custom format parser for `fromAny()`             |\n| `withStrictMode(false)`              | Disable security validation (trusted input only) |\n\n## Error handling\n\nAll exceptions extend `AccessorException`, making it easy to catch every library error in a single block.\n\n### PHP\n\n```php\nuse SafeAccess\\Inline\\Inline;\nuse SafeAccess\\Inline\\Exceptions\\AccessorException;\nuse SafeAccess\\Inline\\Exceptions\\InvalidFormatException;\nuse SafeAccess\\Inline\\Exceptions\\SecurityException;\nuse SafeAccess\\Inline\\Exceptions\\PathNotFoundException;\nuse SafeAccess\\Inline\\Exceptions\\ReadonlyViolationException;\n\ntry {\n    $accessor = Inline::fromJson($untrustedInput);\n    $value = $accessor-\u003egetOrFail('config.key');\n} catch (InvalidFormatException $e) {\n    // Malformed JSON, XML, INI, or NDJSON input\n} catch (SecurityException $e) {\n    // Forbidden key, payload too large, depth/key-count exceeded\n} catch (PathNotFoundException $e) {\n    // Path does not exist in the data\n} catch (ReadonlyViolationException $e) {\n    // Write attempted on a readonly accessor\n} catch (AccessorException $e) {\n    // Catch-all for any SafeAccess error\n}\n```\n\n### TypeScript\n\n```typescript\nimport {\n    Inline,\n    AccessorException,\n    InvalidFormatException,\n    SecurityException,\n    PathNotFoundException,\n    ReadonlyViolationException,\n} from '@safeaccess/inline';\n\ntry {\n    const accessor = Inline.fromJson(untrustedInput);\n    const value = accessor.getOrFail('config.key');\n} catch (e) {\n    if (e instanceof InvalidFormatException) {\n        /* malformed input */\n    }\n    if (e instanceof SecurityException) {\n        /* security violation */\n    }\n    if (e instanceof PathNotFoundException) {\n        /* path not found */\n    }\n    if (e instanceof ReadonlyViolationException) {\n        /* readonly violation */\n    }\n    if (e instanceof AccessorException) {\n        /* any library error */\n    }\n}\n```\n\n### Exception hierarchy\n\n| Exception                    | Extends                      | When                                                         |\n| ---------------------------- | ---------------------------- | ------------------------------------------------------------ |\n| `AccessorException`          | `RuntimeException` / `Error` | Root — catch-all for any library error                       |\n| `SecurityException`          | `AccessorException`          | Forbidden key, payload too large, structural limits exceeded |\n| `InvalidFormatException`     | `AccessorException`          | Malformed JSON, XML, INI, NDJSON                             |\n| `YamlParseException`         | `InvalidFormatException`     | Unsafe or malformed YAML                                     |\n| `PathNotFoundException`      | `AccessorException`          | `getOrFail()` on a missing path                              |\n| `ReadonlyViolationException` | `AccessorException`          | Write on a readonly accessor                                 |\n| `UnsupportedTypeException`   | `AccessorException`          | Unknown accessor class in `make()`                           |\n| `ParserException`            | `AccessorException`          | Reserved for custom parser-level errors                      |\n\n## Advanced usage\n\n### Path cache\n\nCache parsed path segments for repeated lookups:\n\n```php\n// PHP - implement PathCacheInterface\n$cache = new MyPathCache();\n$accessor = Inline::withPathCache($cache)-\u003efromJson($data);\n$accessor-\u003eget('deeply.nested.path'); // parses path\n$accessor-\u003eget('deeply.nested.path'); // cache hit - skips parsing\n```\n\n```typescript\n// TypeScript - implement PathCacheInterface\nconst cacheMap = new Map();\nconst cache: PathCacheInterface = {\n    get: (path) =\u003e cacheMap.get(path) ?? null,\n    set: (path, segments) =\u003e {\n        cacheMap.set(path, segments);\n    },\n    has: (path) =\u003e cacheMap.has(path),\n    clear: () =\u003e {\n        cacheMap.clear();\n        return cache;\n    },\n};\nconst accessor = Inline.withPathCache(cache).fromJson(data);\n```\n\n### Custom format integration\n\nAdd support for custom data formats by implementing `ParseIntegrationInterface`:\n\n```php\n// PHP\nclass CsvIntegration implements ParseIntegrationInterface\n{\n    public function assertFormat(mixed $raw): bool\n    {\n        return is_string($raw) \u0026\u0026 str_contains($raw, ',');\n    }\n\n    public function parse(mixed $raw): array\n    {\n        // Parse CSV to associative array\n        return $parsed;\n    }\n}\n\n$accessor = Inline::withParserIntegration(new CsvIntegration())-\u003efromAny($csvString);\n```\n\n```typescript\n// TypeScript\nconst csvIntegration: ParseIntegrationInterface = {\n    assertFormat: (raw: unknown) =\u003e typeof raw === 'string' \u0026\u0026 (raw as string).includes(','),\n    parse: (raw: unknown) =\u003e {\n        // Parse CSV to object\n        return parsed;\n    },\n};\n\nconst accessor = Inline.withParserIntegration(csvIntegration).fromAny(csvString);\n```\n\n## API reference\n\n### `Inline` facade\n\n#### Static factory methods\n\n| Method                        | Input                              | Returns              |\n| ----------------------------- | ---------------------------------- | -------------------- |\n| `fromArray(data)`             | Array / plain object               | `ArrayAccessor`      |\n| `fromObject(data)`            | Object                             | `ObjectAccessor`     |\n| `fromJson(data)`              | JSON `string`                      | `JsonAccessor`       |\n| `fromXml(data)`               | XML `string` or `SimpleXMLElement` | `XmlAccessor`        |\n| `fromYaml(data)`              | YAML `string`                      | `YamlAccessor`       |\n| `fromIni(data)`               | INI `string`                       | `IniAccessor`        |\n| `fromEnv(data)`               | dotenv `string`                    | `EnvAccessor`        |\n| `fromNdjson(data)`            | NDJSON `string`                    | `NdjsonAccessor`     |\n| `fromAny(data, integration?)` | Any format                         | `AnyAccessor`        |\n| `from(typeFormat, data)`      | `TypeFormat` enum                  | `AccessorsInterface` |\n| `make(accessorClass, data)`   | Accessor class                     | `AbstractAccessor`   |\n\n#### Accessor read methods\n\n| Method                      | Returns                                 |\n| --------------------------- | --------------------------------------- |\n| `get(path, default?)`       | Value at path, or default               |\n| `getOrFail(path)`           | Value or throws `PathNotFoundException` |\n| `getAt(segments, default?)` | Value at key segments                   |\n| `has(path)`                 | `boolean`                               |\n| `hasAt(segments)`           | `boolean`                               |\n| `getMany(paths)`            | `Record\u003cstring, unknown\u003e`               |\n| `all()`                     | All parsed data                         |\n| `count(path?)`              | Element count                           |\n| `keys(path?)`               | Key names                               |\n| `getRaw()`                  | Original input                          |\n\n#### Accessor write methods (immutable)\n\n| Method                   | Description            |\n| ------------------------ | ---------------------- |\n| `set(path, value)`       | Set at path            |\n| `setAt(segments, value)` | Set at key segments    |\n| `remove(path)`           | Remove at path         |\n| `removeAt(segments)`     | Remove at key segments |\n| `merge(path, value)`     | Deep-merge at path     |\n| `mergeAll(value)`        | Deep-merge at root     |\n\n#### Modifier methods\n\n| Method            | Description                |\n| ----------------- | -------------------------- |\n| `readonly(flag?)` | Block all writes           |\n| `strict(flag?)`   | Toggle security validation |\n\n#### TypeFormat enum\n\n`Array` · `Object` · `Json` · `Xml` · `Yaml` · `Ini` · `Env` · `Ndjson` · `Any`\n\n## Comparison\n\n| Feature                   | Safe Access Inline      | `lodash.get`    | Laravel `data_get` | `jmespath`  |\n| ------------------------- | ----------------------- | --------------- | ------------------ | ----------- |\n| Language                  | PHP + TypeScript        | JavaScript      | PHP                | Multi       |\n| Security validation       | Built-in, on by default | None            | None               | None        |\n| Forbidden key blocking    | Yes                     | No              | No                 | No          |\n| Payload size limits       | Yes                     | No              | No                 | No          |\n| Immutable writes          | Yes                     | No              | No                 | N/A         |\n| Readonly mode             | Yes                     | No              | No                 | N/A         |\n| Multi-format support      | 9 formats               | Object only     | Array only         | Object only |\n| Custom format integration | Yes                     | No              | No                 | No          |\n| Wildcard / filter / slice | Yes                     | No              | Partial (`*`)      | Yes         |\n| Zero prod dependencies    | Yes                     | lodash          | Laravel            | jmespath    |\n| Cross-language parity     | PHP + TypeScript        | JavaScript only | PHP only           | Multi       |\n\n## Project structure\n\n```\npackages/php/   - PHP package (PSR-4, Pest, PHPStan, Infection)\npackages/js/    - TypeScript package (ESM, Vitest, Stryker)\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit conventions, and pull request guidelines.\n\n## License\n\n[MIT](LICENSE) © Felipe Sauer\n","funding_links":["https://github.com/sponsors/felipesauer"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipesauer%2Fsafeaccess-inline","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelipesauer%2Fsafeaccess-inline","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipesauer%2Fsafeaccess-inline/lists"}