A PHP library for parsing, validating, resolving, and serializing Design Tokens Community Group (DTCG) format documents. Targets DTCG spec version 2025.10.
Pre-1.0. The public API surface is stabilising but may still shift
before 1.0.0 is tagged. See ROADMAP.md for what's in
scope, what's deferred, and why.
- PHP 8.3 or newer
- Composer
composer require penyaskito/dtcgOptional: symfony/yaml if you want YAML input support (not yet
wired — tracked in the roadmap).
use Penyaskito\Dtcg\Parser\Parser;
$parser = new Parser();
$document = $parser->parseFile('/path/to/design.tokens.json');
// $document is a Penyaskito\Dtcg\Tom\Document wrapping the TOM root.
$spacingBase = $document->root->child('spacing')?->child('base');use Penyaskito\Dtcg\Validator\SchemaValidator;
$validator = new SchemaValidator();
$violations = $validator->validateFile('/path/to/design.tokens.json');
foreach ($violations as $v) {
printf("[%s] %s: %s\n", $v->source->value, $v->path, $v->message);
}use Penyaskito\Dtcg\Parser\Parser;
use Penyaskito\Dtcg\Validator\SemanticValidator;
$document = (new Parser())->parseFile('/path/to/design.tokens.json');
$violations = (new SemanticValidator())->validate($document);
foreach ($violations as $v) {
printf("[%s] %s: %s\n", $v->source->value, $v->path, $v->message);
}Plug in your own rule by implementing Penyaskito\Dtcg\Validator\Rule\Rule
and passing a custom rule list to the validator constructor.
use Penyaskito\Dtcg\Parser\Parser;
use Penyaskito\Dtcg\Reference\ReferenceParser;
use Penyaskito\Dtcg\Reference\Resolver;
$document = (new Parser())->parseFile('/path/to/design.tokens.json');
$resolver = new Resolver($document);
$reference = ReferenceParser::parse('#/spacing/base');
$target = $resolver->resolveChain($reference); // follows intermediate ReferenceTokensMaterializer returns a new Document with every reference resolved
end-to-end — ReferenceTokens become ValueTokens, and references inside
composite sub-fields (e.g. border.color, gradient.stops[].color) are
expanded to concrete values. Strict: throws MaterializationException on
broken, cyclic, or type-mismatched targets.
use Penyaskito\Dtcg\Parser\Parser;
use Penyaskito\Dtcg\Reference\Materializer;
use Penyaskito\Dtcg\Reference\Resolver;
$document = (new Parser())->parseFile('/path/to/design.tokens.json');
$materialized = (new Materializer(new Resolver($document)))->materialize($document);Use this when downstream code needs concrete values and shouldn't have to walk references itself (theme emitters, contrast checkers, etc.). The input document is left unchanged.
use Penyaskito\Dtcg\Parser\Parser;
use Penyaskito\Dtcg\Serializer\DtcgJsonSerializer;
$document = (new Parser())->parseFile('/path/to/design.tokens.json');
$json = (new DtcgJsonSerializer())->serialize($document);Parse → serialize → parse is a fixed point: the re-parsed TOM is equivalent to the original (see Known quirks below for the one exception).
use Penyaskito\Dtcg\Parser\Parser;
use Penyaskito\Dtcg\Serializer\CssCustomPropertiesSerializer;
$document = (new Parser())->parseFile('/path/to/design.tokens.json');
$css = (new CssCustomPropertiesSerializer())->serialize($document);This serializer is marked @internal. It exists to demonstrate the
Serializer interface and to exercise the pipeline end-to-end. Real
consumers (with naming schemes, theming, prefixes) should ship their
own Serializer implementation. See the docblock on the class for
details.
Two object models, one-way dependency:
- TOM (
Penyaskito\Dtcg\Tom): immutable representation of a parsed.tokens.json.Documentwraps the rootGroup; groups containGroupandTokenchildren.Tokenis either aValueToken(has a typed$value) or aReferenceToken(has a$ref). - ROM (
Penyaskito\Dtcg\Resolver\Rom): not yet implemented. Will represent a parsed.resolver.json. ROM depends on TOM; TOM stays oblivious to ROM.
The main surfaces:
| Component | Purpose |
|---|---|
Parser |
JSON array → Tom\Document, with strict $type inference |
Reference\Resolver |
Walk a Reference against a TOM, optionally chain-follow |
Reference\Materializer |
Resolve every reference (incl. inside composites) into a new Document |
Validator\SchemaValidator |
Structural validation against the vendored DTCG schemas |
Validator\SemanticValidator |
Rule-based semantic validation (alias targets, cycles, etc.) |
Serializer\DtcgJsonSerializer |
TOM → DTCG JSON (round-trip safe) |
Serializer\CssCustomPropertiesSerializer |
@internal reference impl |
All TOM nodes are readonly. Editing a TOM means building a new one.
Every node carries a SourceMap (URI + RFC 6901 JSON pointer into the
source) for error reporting.
ParseError messages always include the JSON Pointer location of the
offending element:
dimension $value.unit must be a string (at /spacing/base/$value)
The ParseError::$pointer property (readonly, public) also exposes the
pointer as a string for programmatic access.
- Structural validation does not catch bad
$valueshapes when$typeis inherited from an ancestor group. The DTCG schema's per-type$valuebranches are gated on$typebeing directly on the token. When it's inherited, the schema has no way to know the token's type.SemanticValidatordoes not yet close this gap either — run theParser(which resolves$typethrough inheritance and invokes value factories) to catch shape errors. CssCustomPropertiesSerializersilently skips composite-value nuances it can't represent (e.g. compositestrokeStylefalls back todashed). It's a reference implementation, not a production emitter.$root, property-level JSON Pointer references that descend into primitive-value internals (e.g. into a color's components array), and YAML input are not yet supported — see ROADMAP.md for details and workarounds. Property-level references at the sub-field level of a composite (e.g.border.colorpointing to a color token) are fully supported.
Development setup uses DDEV. Tests with PHPUnit and
static analysis with PHPStan at level max. The project also maintains:
- ROADMAP.md — committed scope and architectural decisions worth preserving.
- CLAUDE.md — instructions for coding agents (and humans) working in this repo, including commit-attribution conventions.
ddev start
ddev composer install
ddev exec vendor/bin/phpunit
ddev exec vendor/bin/phpstan analyseMIT. See LICENSE.