Architecture
Table of Contents
- Architecture
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
- Zero Surprises —
get()never throws exceptions. Missing paths return a default value. - Format-Agnostic — The same API works identically across all supported formats.
- Immutability —
set()andremove()always return new instances; the original is never mutated. - Real Dependencies for Complex Formats — YAML and TOML use real libraries (
js-yaml/smol-tomlin JS,symfony/yaml/devium/tomlin PHP) as dependencies. The Plugin System provides optional override capability. - Extensibility — Custom accessors via
SafeAccess::extend(), custom parsers and serializers viaPluginRegistry.
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. ThrowsInvalidFormatExceptionon 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
| Aspect | PHP | JS/TS |
|---|---|---|
| YAML/TOML parsing | Real 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 plugins | 6 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 accessuser.profile.name— nested accessitems.0.title— numeric index accessmatrix[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:
- Parse path into key segments via
parseKeys() - Walk the data structure segment by segment
- On
*wildcard: iterate all children, recursively resolve remaining path - On escaped dot: treat as literal key name
- 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 instanceImplementation:
- PHP:
clone $this+ update$data - JS:
clone(newData)method creates a new instance with modified data (viastructuredClone)
TypeDetector
Auto-detection priority (first match wins):
- Array →
ArrayAccessor - SimpleXMLElement (PHP only) →
XmlAccessor - Object →
ObjectAccessor - JSON string (
{or[) →JsonAccessor - NDJSON string (multiple
{...}lines) →NdjsonAccessor - XML string (
<?xmlor<) →XmlAccessor - YAML string (contains
:without=) →YamlAccessor - TOML string (
key = "quoted"pattern) →TomlAccessor - INI string (has
[section]headers) →IniAccessor - ENV string (
KEY=VALUEuppercase pattern) →EnvAccessor - 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.mdSecurity 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:
- Path validation — Resolved path must be within
allowedDirs(if specified) - Format detection — Extension-based (
resolveFormatFromExtension) - Content reading — File system or HTTPS fetch
- Audit emission —
file.readorurl.fetchevent - 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
AbstractAccessorandDotNotationParser. 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, anddetailfields.
CLI Package
The @safe-access-inline/cli package provides command-line access to all library features:
| Command | Description |
|---|---|
get | Read a value by path |
set | Set a value at a path |
remove | Remove a value at a path |
transform | Convert between formats (JSON ↔ YAML ↔ TOML) |
diff | Generate JSON Patch diff between two files |
mask | Redact sensitive values |
layer | Merge multiple config files |
keys | List keys at a path |
type | Show the type of a value at a path |
has | Check path existence (exit code 0/1) |
count | Count elements at a path |
Supports stdin input (-), all formats (JSON, YAML, TOML, XML, INI, CSV, ENV, NDJSON), pretty output, and path expressions.
Framework Integrations
| Framework | Package | Module | Features |
|---|---|---|---|
| NestJS | JS | SafeAccessModule | Dynamic module, SAFE_ACCESS injection token |
| Vite | JS | safeAccessPlugin | Virtual module, HMR support, multi-file merging |
| Laravel | PHP | LaravelServiceProvider | Singleton binding, config repository wrapping |
| Symfony | PHP | SymfonyIntegration | ParameterBag 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:
InvalidFormatExceptionat the accessor level,UnsupportedTypeExceptionat 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.