PHP parser for DMS — a data syntax with strong typing, ordered maps, multi-line heredocs, and front-matter metadata.

Maintainers

Package info

gitlab.com/flo-labs/pub/dms-php

Issues

pkg:composer/flo-labs/dms

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

0.5.1 2026-05-05 21:13 UTC

This package is auto-updated.

Last update: 2026-05-06 05:19:05 UTC


README

DMS

dms-php

PHP parser for DMS, a data syntax with strong typing, ordered maps, multi-line heredocs, and front-matter metadata.

Two packages live in this repo, both with the same API and value shape:

packageimplementationwhen to use
flo-labs/dmspure PHPshared hosting, no FFI required
flo-labs/dms-cphp_ffi binding to the dms-c C parserhot paths; ~7× faster than pure PHP

What DMS looks like

A medium-size tier-0 document, exercising every feature you'd touch in a real config — front matter, comments (line + trailing), nested tables, list-of-tables with the + marker, flow forms, distinct types, and a heredoc with a trim modifier:

+++
title:    "DMS feature tour"
version:  "1.0.0"
updated:  2026-04-24T09:30:00-04:00
+++

# Hash and // line comments both work.
// Bare keys allow full Unicode; quoted keys take any string.

database:
  host:    "db.internal"
  port:    5432            # bumped after the LB change
  pool:    { size: 10, idle_timeout_s: 30 }   # flow table

servers:
  + name: "web1"
    disks:
      + mount: "/"
        size_gb: 100
      + mount: "/var"
        size_gb: 500
  + name: "web2"

regions: ["us-east-1", "eu-west-1", "ap-south-1"]

sql: """SQL _trim("\n", ">")
    SELECT id, email
      FROM users
     WHERE active = true
    SQL

Tier 1 layers structured decorators on top of the value tree. Sigils bind to families published by a dialect; here is dms+html carrying an HTML fragment as a DMS document:

+++
_dms_tier: 1
_dms_imports:
  + dialect: "html"
    version: "1.0.0"
+++

+ |html(lang: "en")
  + |head
    + |title "DMS feature tour"
    + |meta(charset: "UTF-8")
  + |body(class: "main")
    + |h1 "Welcome to DMS"
    + |p(class: "lede")
      + "Click "
      + |a(href: "/spec.html") "here"
      + " to read the spec."

Full feature tour, format comparison, and dialect index on the DMS website.

Install

Status: not yet on Packagist. Until the first release lands there, consume this port by adding the GitLab repository to your composer.json and requiring flo-labs/dms from it:

{
  "repositories": [
    { "type": "vcs", "url": "https://gitlab.com/flo-labs/pub/dms-php" }
  ],
  "require": { "flo-labs/dms": "^0.5" },
  "minimum-stability": "dev"
}

Once the package is published, the install command will simply be:

# pure PHP:
composer require flo-labs/dms

# native (C) parser via FFI:
composer require flo-labs/dms-c

The dms-c package's composer install runs sh build.sh to compile dms.dll / libdms.so from the vendored sources. You need a C compiler (gcc or clang) and the php_ffi extension enabled.

Usage

use Dms\Parser;
use Dms\Emitter;
use DmsC\DmsC;

$src = file_get_contents("config.dms");

// Full document (preserves comments + literal forms for encode round-trip).
$doc = Parser::decode($src);             // pure PHP — returns Dms\Document
// $doc = DmsC::decode($src);             // native (C), same API
$meta           = $doc->meta;            // Dms\Table | null
$body           = $doc->body;
$comments       = $doc->comments;        // list of Dms\AttachedComment
$originalForms  = $doc->originalForms;   // list of [path, OriginalLiteral]

// Re-emit DMS source.
$output = Emitter::encode($doc);

0.3.0 rename: the old names Parser::parse() / Parser::parseLite() and Emitter::toDms() / Emitter::toDmsLite() still work as wrappers that emit E_USER_DEPRECATED and forward to the canonical decode / decodeLite / encode / encodeLite. They will be removed in a future release. The same rename applies to the FFI tier (DmsC::parseDmsC::decode, etc.).

The conformance JSON encoder (formerly Dms\Encoder) is now Dms\TaggedJsonEncoder, so its encode() no longer collides with Dms\Emitter::encode() (DMS source round-trip). And the parser's exception type was renamed Dms\ParseErrorDms\DecodeException, with Dms\EncodeException added as the matching emitter-error class — ParseError stays as a deprecated subclass alias.

Tables are insertion-ordered associative arrays. Lists are arrays. Datetimes are wrapped types: the pure module returns Dms\LocalDate / Dms\LocalTime / Dms\LocalDateTime / Dms\OffsetDateTime class instances; the C package returns plain ['__dms_type' => '...', 'value' => '...'] arrays with the same data. Encoders that detect via __dms_type + value work unchanged across both packages.

Working with comments and heredocs

DMS preserves comments through parse → mutate → re-emit (SPEC §Comments). Dms\Document is immutable — its meta, body, comments, and originalForms fields are readonly. To attach a comment programmatically, build the new comments array and use the withComments clone helper (PSR-7 style):

use Dms\Parser;
use Dms\Emitter;
use Dms\AttachedComment;
use Dms\Comment;

$doc = Parser::decode("db:\n  port: 8080\n");

// Build the new comments list with one extra leading line comment.
$comments = [...$doc->comments,
    new AttachedComment(
        comment:  new Comment(content: '# bumped after LB change', kind: 'line'),
        position: 'leading',
        path:     ['db', 'port'],
    ),
];

// Body is a Dms\Table; mutating the underlying value follows the same
// "build-then-replace" pattern via withBody if you need to substitute it.
echo Emitter::encode($doc->withComments($comments));

Forcing a heredoc on emit

Strings parse and re-emit in their source form. To switch a basic-quoted string to a heredoc (or to construct one from scratch), append an OriginalLiteral::string(...) record keyed by the value's path and swap it in via withOriginalForms:

use Dms\OriginalLiteral;
use Dms\StringForm;
use Dms\HeredocFlavor;

$forms = [...$doc->originalForms,
    [
        ['db', 'greeting'],
        OriginalLiteral::string(StringForm::heredoc(
            HeredocFlavor::basicTriple(),     // or ::literalTriple() for '''
            null,                             // null = unlabeled
            [],                               // _trim(...), _fold_paragraphs(), …
        )),
    ],
];

$doc = $doc->withOriginalForms($forms);

Round-trip rules (SPEC §Round-trip semantics): comments stick to still-present nodes; deleting a node drops its comments; newly inserted nodes start with no comments. The first originalForms entry per path wins, so override a parser-recorded form by replacing rather than appending if the key is already present.

Performance

50,000-key flat document (~700 KB), best-of-5, startup-subtracted, PHP 8.3 on Windows 11:

tierDMS porttimeJSON peertimeYAML peertimeDMS / JSONDMS / YAML
pure PHPdms328 msn/asymfony/yaml290 msn/a1.13×
native (C)dms-c45 msjson_decode16 msn/a (no FFI YAML)2.81×n/a

The FFI binding cuts parse time 7.3× vs pure PHP. Against C-backed peers DMS sits at ~2.8× JSON cost — the standard cost of carrying comments, ordered keys, and source-form metadata.

PHP's stdlib json_decode is C-backed, so JSON only appears in the FFI tier (no widely-used pure-PHP JSON parser to compare against). symfony/yaml is pure PHP — the de-facto YAML library — so it sits in the pure tier; there's no widely-used libyaml binding for PHP that ships across distros, so YAML doesn't appear in the FFI row.

Reproduce with:

composer install                                    # pulls symfony/yaml + phpbench
sh dms-c/build.sh                                   # builds dms.dll / libdms.so
php -d extension=ffi -d ffi.enable=true \
    bench/run_bench_decoders.php

Build & test

# pure package:
composer install
vendor/bin/phpunit

# native (FFI) package:
sh dms-c/build.sh

Conformance

The fixture corpus lives in dms-tests (4500+ pairs). Clone it once as a sibling:

cd ..
git clone https://gitlab.com/flo-labs/pub/dms-tests.git

The bin/dms-encoder binary reads DMS from stdin and writes tagged JSON to stdout, matching the format the conformance runner consumes. The dms-c package passes 3931/3931 valid fixtures.

License

Dual-licensed: MIT or Apache-2.0, your choice.