safeaccess/inline
Composer 安装命令:
composer require safeaccess/inline
包简介
Safe nested data access with dot notation for PHP.
README 文档
README
Safe Access Inline — PHP
PHP library for safe nested data access with security validation on by default — JSON, YAML, TOML, XML, INI, ENV, NDJSON, CSV/TSV, arrays and objects. Includes a full PathQuery engine with filters, wildcards, slices, and projections, plus optional schema validation. Zero production dependencies.
What's new in 0.2.0
- New formats: TOML (
fromToml) and CSV/TSV (fromCsv,fromTsv). - Optional schema validation:
validate()/assert()with type rules, constraints (min,max,enum,pattern,email,url,uuid), per-itemeach:(...), and*wildcard paths.
The problem
Reading nested data from external sources requires more than null-safe access. You also need to defend against XXE in XML, anchor bombs in YAML, PHP magic method injection, stream wrapper abuse, superglobal access, and payload size attacks. Without a tool for this, that validation is boilerplate you write manually for every format and every endpoint.
Without this library (XML from an external API):
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:
$host = Inline::fromXml($input)->get('database.host'); // XXE blocked, forbidden keys validated, depth enforced — by default
Installation
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).
Quick start
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)
Security
All public entry points validate input by default. Every key passes through SecurityGuard and SecurityParser before being accessible.
What gets blocked
| Category | Examples | Reason |
|---|---|---|
| PHP magic methods | __construct, __destruct, __wakeup, __sleep, __toString, ... |
Prevent PHP magic behavior via data keys |
| Prototype pollution | __proto__, constructor, prototype |
Prevent prototype pollution attacks |
| PHP superglobals | GLOBALS, _GET, _POST, _COOKIE, _SERVER, _ENV, ... |
Prevent superglobal variable access |
| Stream wrapper URIs | php://input, phar://..., data://..., file://... |
Prevent stream wrapper injection |
Format-specific protections
| Format | Protection |
|---|---|
| XML | Rejects <!DOCTYPE — prevents XXE (XML External Entity) attacks |
| YAML | Blocks unsafe tags, anchors (&), aliases (*), and merge keys (<<) |
| All | Forbidden key validation on every parsed key |
Structural limits
| Limit | Default | Description |
|---|---|---|
maxPayloadBytes |
10 MB | Maximum raw string input size |
maxKeys |
10,000 | Maximum total key count |
maxDepth |
512 | Maximum structural nesting depth |
maxResolveDepth |
100 | Maximum recursion for path resolution |
maxCountRecursiveDepth |
100 | Maximum recursion when counting keys |
Custom forbidden keys
$guard = new SecurityGuard(extraForbiddenKeys: ['secret', 'internal_token']); $accessor = Inline::withSecurityGuard($guard)->fromJson($data);
Disabling validation for trusted input
$accessor = Inline::withStrictMode(false)->fromJson($trustedPayload);
Warning: Disabling strict mode skips all validation. Only use with application-controlled input.
For vulnerability reports, see SECURITY.md.
Dot notation syntax
Basic syntax
| 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) |
$data = Inline::fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}'); $data->get('users.0.name'); // 'Alice' $data->get('users.1.name'); // 'Bob'
Advanced PathQuery
| 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 selection |
['a','b'] |
['name','age'] |
Multi-key selection |
[0:5] |
items[0:5] |
Slice — indices 0 through 4 |
[::2] |
items[::2] |
Slice with step |
[::-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
$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: +, -, *, / $orders = Inline::fromJson('[{"price": 10, "qty": 5}, {"price": 3, "qty": 2}]'); $orders->get('[?@.price * @.qty > 20]'); // first order only
Supported formats
JSON
$accessor = Inline::fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}'); $accessor->get('users.0.name'); // 'Alice'
YAML
$yaml = <<<YAML database: host: localhost port: 5432 credentials: user: admin YAML; $accessor = Inline::fromYaml($yaml); $accessor->get('database.credentials.user'); // 'admin'
TOML
$toml = <<<TOML [database] host = "localhost" port = 5432 [[servers]] name = "alpha" [[servers]] name = "beta" TOML; $accessor = Inline::fromToml($toml); $accessor->get('database.host'); // 'localhost' $accessor->get('servers.1.name'); // 'beta'
Supports tables, arrays of tables, dotted keys, inline arrays and tables, and the scalar types (integer, float, boolean, string). Duplicate keys and redefined tables are rejected; datetimes are preserved as strings.
XML
$xml = '<config><database><host>localhost</host></database></config>'; $accessor = Inline::fromXml($xml); $accessor->get('database.host'); // 'localhost' // Also accepts SimpleXMLElement $accessor = Inline::fromXml(simplexml_load_string($xml));
INI
$accessor = Inline::fromIni("[database]\nhost=localhost\nport=5432"); $accessor->get('database.host'); // 'localhost'
ENV (dotenv)
$accessor = Inline::fromEnv("APP_NAME=MyApp\nAPP_DEBUG=true\nDB_HOST=localhost"); $accessor->get('DB_HOST'); // 'localhost'
NDJSON
Each line is parsed as an independent JSON object and indexed from 0 by its position in the input. Blank lines and trailing newlines are skipped. Security validation is applied to each line individually.
$ndjson = '{"id":1,"name":"Alice"}' . "\n" . '{"id":2,"name":"Bob"}'; $accessor = Inline::fromNdjson($ndjson); $accessor->get('0.name'); // 'Alice' $accessor->get('1.name'); // 'Bob'
CSV / TSV
The first row is the header; each subsequent row becomes a record indexed from 0 by position, keyed by the header columns. All values are strings — CSV has no type system, so 007 stays "007". Quoted fields may contain the delimiter, escaped quotes (""), and embedded newlines. Duplicate header columns, rows whose field count differs from the header, and unterminated quotes are rejected. Header columns are validated against forbidden keys.
$csv = "name,age,city\nAlice,30,Porto\nBob,25,Lisbon"; $accessor = Inline::fromCsv($csv); $accessor->get('0.name'); // 'Alice' $accessor->get('1.city'); // 'Lisbon' // Tab-separated $accessor = Inline::fromTsv("name\tage\nAlice\t30"); $accessor->get('0.age'); // '30'
Array / Object
$accessor = Inline::fromArray(['users' => [['name' => 'Alice'], ['name' => 'Bob']]]); $accessor->get('users.0.name'); // 'Alice' $accessor = Inline::fromObject((object) ['name' => 'Alice']); $accessor->get('name'); // 'Alice'
Any (custom format via integration)
use SafeAccess\Inline\Contracts\ParseIntegrationInterface; // Requires implementing ParseIntegrationInterface $accessor = Inline::withParserIntegration(new MyCsvIntegration())->fromAny($csvString); $accessor->get('0.column_name');
Dynamic (by TypeFormat enum)
use SafeAccess\Inline\Enums\TypeFormat; $accessor = Inline::from(TypeFormat::Json, '{"key": "value"}'); $accessor->get('key'); // 'value'
Reading & writing
$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
Schema validation
Beyond security, validate the shape of the data. A schema maps dot-notation paths to pipe-separated rules: a base type (string, int, float, number, bool, array, object, null, any) followed by optional constraints. A trailing ? marks the path optional. Validation runs against the already-parsed value.
| Constraint | Applies to | Meaning |
|---|---|---|
min:N |
number | value >= N |
min:N |
string / array | length / size >= N |
max:N |
number / string / array | value or length / size <= N |
enum:a,b,c |
string / number | value must be one of the listed options |
pattern:REGEX |
string | must match the (unanchored) regular expression |
email url uuid |
string | common string formats |
each:(RULE) |
array | every element must satisfy RULE (may nest) |
validate() returns a SchemaResult you can inspect; assert() throws SchemaValidationException on failure and returns the accessor for chaining. A path may contain * wildcards to apply a rule to every match. A malformed rule throws AccessorException.
$accessor = Inline::fromJson('{"db":{"host":"localhost","port":5432},"users":[{"email":"a@b.com"}]}'); // Inspect without throwing $result = $accessor->validate([ 'db.host' => 'string|min:1', 'db.port' => 'int|min:1|max:65535', 'db.ssl' => 'bool?', // optional 'users.*.email' => 'string|email', // every user's email ]); $result->isValid(); // true $result->errors(); // SchemaError[] when invalid $result->errorsByPath(); // ['path' => [messages]] grouped per field // Assert and chain (throws SchemaValidationException on failure) $host = $accessor->assert(['db.host' => 'string'])->get('db.host');
Configure
Builder pattern
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);
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:
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 } catch (SecurityException $e) { // Forbidden key, payload too large, depth/key-count exceeded } catch (PathNotFoundException $e) { // Path does not exist } catch (ReadonlyViolationException $e) { // Write on readonly accessor } catch (AccessorException $e) { // Catch-all for any library error }
Exception hierarchy
| Exception | Extends | When |
|---|---|---|
AccessorException |
RuntimeException |
Root — catch-all |
SecurityException |
AccessorException |
Forbidden key, payload, structural limits |
InvalidFormatException |
AccessorException |
Malformed JSON, XML, INI, NDJSON |
YamlParseException |
InvalidFormatException |
Unsafe or malformed YAML |
TomlParseException |
InvalidFormatException |
Malformed TOML, duplicate keys, tables |
CsvParseException |
InvalidFormatException |
Malformed CSV/TSV, duplicate columns |
SchemaValidationException |
AccessorException |
assert() when data fails the schema |
PathNotFoundException |
AccessorException |
getOrFail() on missing path |
ReadonlyViolationException |
AccessorException |
Write on readonly accessor |
UnsupportedTypeException |
AccessorException |
Unknown accessor class in make() |
ParserException |
AccessorException |
Internal parser errors |
Advanced usage
Strict mode
// Disable all security validation for trusted input $accessor = Inline::withStrictMode(false)->fromJson($trustedPayload);
Warning: Disabling strict mode skips all validation. Only use with application-controlled input.
Path cache
// Implement PathCacheInterface for repeated lookups $cache = new MyPathCache(); $accessor = Inline::withPathCache($cache)->fromJson($data); $accessor->get('deeply.nested.path'); // parses path $accessor->get('deeply.nested.path'); // cache hit
Custom format integration
// Implement ParseIntegrationInterface for custom formats 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);
API reference
Inline facade
Static factory methods
| Method | Input | Returns |
|---|---|---|
fromArray($data) |
array<array-key, mixed> |
ArrayAccessor |
fromObject($data) |
object |
ObjectAccessor |
fromJson($data) |
JSON string |
JsonAccessor |
fromXml($data) |
XML string or SimpleXMLElement |
XmlAccessor |
fromYaml($data) |
YAML string |
YamlAccessor |
fromToml($data) |
TOML string |
TomlAccessor |
fromIni($data) |
INI string |
IniAccessor |
fromEnv($data) |
dotenv string |
EnvAccessor |
fromNdjson($data) |
NDJSON string |
NdjsonAccessor |
fromCsv($data) |
CSV string |
CsvAccessor |
fromTsv($data) |
TSV string |
TsvAccessor |
fromAny($data, $integration?) |
mixed |
AnyAccessor |
from($typeFormat, $data) |
TypeFormat enum |
AccessorsInterface |
make($class, $data) |
class-string |
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) |
bool |
hasAt($segments) |
bool |
getMany($paths) |
array<string, mixed> |
all() |
array<string, mixed> |
count($path?) |
int |
keys($path?) |
list<string> |
getRaw() |
mixed |
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
Contributing
See CONTRIBUTING.md for development setup, commit conventions, and pull request guidelines.
License
MIT © Felipe Sauer
safeaccess/inline 适用场景与选型建议
safeaccess/inline 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 4 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 04 月 07 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「dot-notation」 「data-access」 「array-access」 「object-access」 「nested-data」 「null-safe」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 safeaccess/inline 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 safeaccess/inline 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 safeaccess/inline 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Unleash the Power of Optics in your code!
DotContainer enables deep array access using dot-notation.
Library that allows to access array and object with strong type support in Dot Notation
Lightweight, iterable breadcrumb collection for Symfony 8 applications
This library loads/saves php array from/to a configuration file.
This library reads/writes flat array data from/to key=value format compatible with .ini and .env files.
统计信息
- 总下载量: 4
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 2
- 点击次数: 26
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-04-07