承接 safeaccess/inline 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

safeaccess/inline

Composer 安装命令:

composer require safeaccess/inline

包简介

Safe nested data access with dot notation for PHP.

README 文档

README

safeaccess-inline logo

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.

Packagist License: MIT PHP 8.2+ PHPStan max Tested with Pest Infection MSI

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-item each:(...), 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 我们能提供哪些服务?
定制开发 / 二次开发

基于 safeaccess/inline 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 4
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 26
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-07