Skip to content

Architecture

Table of Contents

Overview

safe-access-inline is a format-agnostic data access library that provides a single API for safely reading, writing, and transforming deeply nested data structures. It follows the Facade pattern with a pluggable accessor system and an extensible Plugin Registry for format parsing and serialization.

Design Principles

  1. Zero Surprisesget() never throws exceptions. Missing paths return a default value.
  2. Format-Agnostic — The same API works identically across all supported formats.
  3. Immutabilityset() and remove() always return new instances; the original is never mutated.
  4. Real Dependencies for Complex Formats — YAML and TOML use real libraries (js-yaml/smol-toml in JS, symfony/yaml/devium/toml in PHP) as dependencies. The Plugin System provides optional override capability.
  5. Extensibility — Custom accessors via SafeAccess::extend(), custom parsers and serializers via PluginRegistry.

Component Diagram

Plugin System

The Plugin System provides optional override capability for format-specific parsing and serialization. YAML and TOML use real libraries by default — plugins let users swap in alternative implementations.

Contracts

  • ParserPluginInterface — receives a raw string input (e.g., YAML text), returns a normalized associative array. Throws InvalidFormatException on malformed input.
  • SerializerPluginInterface — receives a normalized array, returns a formatted string output (e.g., YAML text).

PluginRegistry

A static registry that maps format names (e.g., 'yaml', 'toml') to parser and serializer implementations.

PluginRegistry::registerParser('yaml', new SymfonyYamlParser());
PluginRegistry::registerSerializer('yaml', new SymfonyYamlSerializer());

// Accessors query the registry:
// YamlAccessor::parse() → PluginRegistry::getParser('yaml')->parse($raw)
// $accessor->toYaml()   → PluginRegistry::getSerializer('yaml')->serialize($data)
// $accessor->transform('yaml') → same as toYaml()

PHP vs JS Behavior

AspectPHPJS/TS
YAML/TOML parsingReal library by default (ext-yaml or symfony/yaml for YAML, devium/toml for TOML); plugin optional (overrides)Real library by default (js-yaml, smol-toml); plugin optional (overrides)
Serialization (toYaml, toToml, toXml, transform)Plugin override → ext-yaml/real library fallback (with SimpleXMLElement fallback for XML)Real library by default for YAML/TOML; plugin required for XML
Shipped plugins6 plugins (SymfonyYamlParser, SymfonyYamlSerializer, NativeYamlParser, NativeYamlSerializer, DeviumTomlParser, DeviumTomlSerializer)4 plugins (JsYamlParser, JsYamlSerializer, SmolTomlParser, SmolTomlSerializer)

Data Flow

DotNotationParser Engine

The parser resolves paths like user.profile.name against nested data structures.

Supported path syntax:

  • name — simple key access
  • user.profile.name — nested access
  • items.0.title — numeric index access
  • matrix[0][1] — bracket notation (converted to dot notation)
  • users.*.name — wildcard (returns array of all matching values)
  • config\.db.host — escaped dot (literal dot in key name)

Resolution algorithm:

  1. Parse path into key segments via parseKeys()
  2. Walk the data structure segment by segment
  3. On * wildcard: iterate all children, recursively resolve remaining path
  4. On escaped dot: treat as literal key name
  5. Return default value if any segment is not found

Immutability Pattern

$original = SafeAccess::fromJson('{"a": 1}');
$modified = $original->set('b', 2);

// $original->data = ['a' => 1]     ← unchanged
// $modified->data = ['a' => 1, 'b' => 2]  ← new instance

Implementation:

  • PHP: clone $this + update $data
  • JS: clone(newData) method creates a new instance with modified data (via structuredClone)

TypeDetector

Auto-detection priority (first match wins):

  1. ArrayArrayAccessor
  2. SimpleXMLElement (PHP only) → XmlAccessor
  3. ObjectObjectAccessor
  4. JSON string ({ or [) → JsonAccessor
  5. NDJSON string (multiple {...} lines) → NdjsonAccessor
  6. XML string (<?xml or <) → XmlAccessor
  7. YAML string (contains : without =) → YamlAccessor
  8. TOML string (key = "quoted" pattern) → TomlAccessor
  9. INI string (has [section] headers) → IniAccessor
  10. ENV string (KEY=VALUE uppercase pattern) → EnvAccessor
  11. Unsupported → throws UnsupportedTypeError / UnsupportedTypeException

Limitations: CSV is not auto-detected. The YAML heuristic (: without =) may produce false positives, and TOML detection (key = "quoted") can occasionally conflict with some INI formats. Always prefer explicit factory methods (e.g., fromYaml(), fromToml()) for ambiguous inputs.

Monorepo Structure

safe-access-inline/
├── packages/
│   ├── php/                 # Composer package
│   │   ├── src/
│   │   │   ├── Accessors/   # 10 format accessors (incl. NDJSON)
│   │   │   ├── Contracts/   # Interfaces (incl. ParserPlugin, SerializerPlugin, SchemaAdapter)
│   │   │   ├── Core/        # AbstractAccessor (root) + Parsers/, Resolvers/, Operations/, Rendering/, Io/, Registries/, Config/ subdirs
│   │   │   ├── Enums/       # AccessorFormat, AuditEventType, PatchOperationType, SegmentType
│   │   │   ├── Exceptions/  # Exception hierarchy (incl. SecurityException, SchemaValidationException, ReadonlyViolationException)
│   │   │   ├── Integrations/# LaravelServiceProvider, SymfonyIntegration, SafeAccessBundle
│   │   │   ├── Plugins/     # Shipped plugins (SymfonyYaml*, NativeYaml*, DeviumToml*, SimpleXmlSerializer)
│   │   │   ├── SchemaAdapters/ # JsonSchemaAdapter, SymfonyValidatorAdapter
│   │   │   ├── Security/    # Guards/ (SecurityPolicy, SecurityOptions, SecurityGuard), Audit/ (AuditLogger), Sanitizers/ (CsvSanitizer, DataMasker)
│   │   │   ├── Traits/      # HasFactory, HasTransformations, HasWildcardSupport, HasArrayOperations
│   │   │   └── SafeAccess.php
│   │   └── tests/
│   │       ├── Unit/        # Mock-based unit tests
│   │       └── Integration/ # Real parser integration tests
│   ├── js/                  # npm package
│   │   ├── src/
│   │   │   ├── accessors/   # 10 format accessors (incl. NDJSON)
│   │   │   ├── contracts/   # TypeScript interfaces
│   │   │   ├── core/        # abstract-accessor.ts (root) + parsers/, resolvers/, operations/, rendering/, io/, registries/, config/ subdirs
│   │   │   ├── enums/       # Format, AuditEventType, PatchOperationType, SegmentType
│   │   │   ├── exceptions/  # Error hierarchy (incl. SecurityError, SchemaValidationError, ReadonlyViolationError)
│   │   │   ├── integrations/# NestJS module, Vite plugin
│   │   │   ├── plugins/     # Shipped plugins (JsYaml*, SmolToml*)
│   │   │   ├── schema-adapters/ # JsonSchemaAdapter, ZodAdapter, YupAdapter, ValibotAdapter
│   │   │   ├── security/    # guards/ (SecurityPolicy, SecurityOptions, SecurityGuard), audit/ (AuditEmitter), sanitizers/ (CsvSanitizer, DataMasker, IpRangeChecker)
│   │   │   ├── types/       # DeepPaths, ValueAtPath utility types
│   │   │   ├── safe-access.ts
│   │   │   └── index.ts     # Barrel export
│   │   └── tests/
│   │       ├── unit/        # Mock-based unit tests
│   │       └── integration/ # Cross-format pipeline tests
│   └── cli/                 # CLI package (@safe-access-inline/cli)
│       ├── src/            # cli.ts (dispatcher), command-handlers.ts (shared utils), handlers/ (12 individual command handlers)
│       └── tests/
├── docs/                    # Documentation (VitePress, English + pt-BR)
├── .github/workflows/       # CI/CD
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── SECURITY.md
└── README.md

Security Architecture

The security module provides defense-in-depth for data processing:

  • SecurityPolicy — Immutable aggregate of all security settings. Supports merge() for creating derived policies.
  • SecurityOptions — Static assertion methods for payload size, key count, and nesting depth limits.
  • SecurityGuard — Blocks prototype pollution keys (__proto__, constructor, prototype, __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__, valueOf, toString, hasOwnProperty, isPrototypeOf). Sanitizes objects recursively.
  • IoLoader — Path-traversal protection for file reads. SSRF protection for URL fetches (blocks private IPs, cloud metadata, enforces HTTPS).
  • CsvSanitizer — Guards against CSV injection attacks with configurable modes (none, prefix, strip, error).
  • DataMasker — Replaces sensitive values (password, token, secret, etc.) with [REDACTED]. Supports custom glob/regex patterns.

I/O & File Loading

File and URL loading follow a secure pipeline:

  1. Path validation — Resolved path must be within allowedDirs (if specified)
  2. Format detection — Extension-based (resolveFormatFromExtension)
  3. Content reading — File system or HTTPS fetch
  4. Audit emissionfile.read or url.fetch event
  5. Accessor creation — Delegated to the appropriate SafeAccess.from*() method

File watching uses polling (FileWatcher) — checks mtime at configurable intervals. Returns a stop function for cleanup.

  • PathCache — LRU in-memory cache layered between AbstractAccessor and DotNotationParser. Parsed path segment arrays are stored keyed by path string, eliminating redundant re-parsing on repeated hot-path accesses.

Layered configuration (layer(), layerFiles()) deep-merges multiple sources with last-wins semantics.

Schema Validation

Schema validation uses the Adapter pattern to remain library-agnostic:

Users implement SchemaAdapterInterface with their preferred validation library (Zod, Joi, JSON Schema, etc.) and register it via SchemaRegistry.setDefaultAdapter().

Audit System

The audit system provides observability for security-relevant operations:

  • Event types: file.read, file.watch, url.fetch, security.violation, data.mask, data.freeze, schema.validate
  • Subscription: SafeAccess.onAudit(listener) returns an unsubscribe function
  • Emission: Internal — triggered automatically by IoLoader, DataMasker, schema validation, etc.
  • Design: Pub/sub pattern. Listeners are synchronous. Events include type, timestamp, and detail fields.

CLI Package

The @safe-access-inline/cli package provides command-line access to all library features:

CommandDescription
getRead a value by path
setSet a value at a path
removeRemove a value at a path
transformConvert between formats (JSON ↔ YAML ↔ TOML)
diffGenerate JSON Patch diff between two files
maskRedact sensitive values
layerMerge multiple config files
keysList keys at a path
typeShow the type of a value at a path
hasCheck path existence (exit code 0/1)
countCount elements at a path

Supports stdin input (-), all formats (JSON, YAML, TOML, XML, INI, CSV, ENV, NDJSON), pretty output, and path expressions.

Framework Integrations

FrameworkPackageModuleFeatures
NestJSJSSafeAccessModuleDynamic module, SAFE_ACCESS injection token
ViteJSsafeAccessPluginVirtual module, HMR support, multi-file merging
LaravelPHPLaravelServiceProviderSingleton binding, config repository wrapping
SymfonyPHPSymfonyIntegrationParameterBag wrapping, YAML file loading

Architecture Decision Records

ADR-1: set() / remove() use clone instead of static::from()

Context: The PLAN.md specifies set() returns static::from($newData). However, some accessors carry metadata beyond the normalized array — for example, XmlAccessor stores the originalXml string.

Decision: Both PHP and JS use clone (PHP: clone $this; JS: clone(newData) method) to preserve any accessor-specific metadata when producing a new instance. Only $data is updated.

Consequence: Metadata like originalXml survives mutations, which is the expected behavior. The round-trip set() → toXml() can still access the original XML via getOriginalXml().

ADR-2: JS toXml() / toYaml() / toToml() via Real Libraries + Plugin Override

Context: Initially, the JS package omitted toXml() and toYaml() because JavaScript has no native XML emitter and YAML serialization would require a runtime dependency.

Decision (original): JS only exposed toArray(), toJson(), and toObject().

Decision (revised #1): With the introduction of the Plugin System, JS exposed toYaml(), toXml(), and transform() via PluginRegistry.

Decision (revised #2): js-yaml and smol-toml are now real dependencies. toYaml() and toToml() work zero-config using these libraries. Plugins provide optional override for users who need different libraries. toXml() still requires a plugin.

Consequence: YAML and TOML serialization works out of the box. Users who need alternative serializers register a plugin override. XML serialization still requires explicit plugin registration.

ADR-3: Real Dependencies for YAML/TOML + PluginRegistry for Override

Context: PHP's YAML and TOML accessors originally used class_exists() and function_exists() checks to detect available parsers at runtime. Later, a PluginRegistry was introduced that required manual plugin registration. Both PHP and JS had different approaches: PHP required plugins, JS had built-in lightweight parsers.

Decision: Make YAML/TOML libraries real dependencies in both platforms. js-yaml + smol-toml are dependencies in JS. symfony/yaml + devium/toml are require in PHP. PluginRegistry continues to exist for override: if a plugin is registered, it takes priority over the default library.

Key design choices:

  • Both platforms: YAML/TOML parsing and serialization work out of the box — zero configuration needed.
  • Plugin override: PluginRegistry.registerParser() / PluginRegistry.registerSerializer() still works — registered plugins take priority over default libraries.
  • JS built-in parsers removed: The old lightweight YAML/TOML parsers were removed in favor of proven libraries (js-yaml, smol-toml).
  • Exception types: InvalidFormatException at the accessor level, UnsupportedTypeException at the registry level.
  • Testing: Unit tests use mock plugins (anonymous classes/objects) for isolation. Integration tests use real libraries.

Consequence: Zero configuration for YAML/TOML in both platforms. Consistent behavior between PHP and JS. Users who need alternative parsers/serializers register them via PluginRegistry.

Released under the MIT License.