ktav-lang / ktav
PHP bindings for Ktav — a plain configuration format with three rules, zero indentation, and zero quoting. FFI over the reference Rust crate; no extension compilation on the consumer side.
Requires
- php: ^7.4 || ^8.0
- ext-ffi: *
- ext-json: *
Requires (Dev)
- kahlan/kahlan: ^5.2 || ^6.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Languages: English · Русский · 简体中文
Playground: convert JSON / YAML / TOML / INI ⇄ Ktav in your browser at ktav-lang.github.io.
PHP bindings for the Ktav configuration format. Thin wrapper around the reference Rust parser, loaded at runtime through the PHP FFI extension — no PHP extension to compile, no PECL install. Plain Composer dependency, the native binary is fetched on first call.
Requires PHP 7.4+ with ext-ffi enabled (default in CLI; web SAPIs
need ffi.enable=1 in php.ini).
Install
composer require ktav-lang/ktav
Quick start
Parse — read typed values straight off the array
use Ktav\Ktav; $src = <<<KTAV service: web port: 8080 ratio: 0.75 tls: true tags: [ prod eu-west-1 ] db.host: primary.internal db.timeout: 30 KTAV; $cfg = Ktav::loads($src); $service = $cfg['service']; // string $port = $cfg['port']; // int $ratio = $cfg['ratio']; // float $tls = $cfg['tls']; // bool $tags = $cfg['tags']; // array<string> $dbHost = $cfg['db']['host']; // string $dbTimeout = $cfg['db']['timeout']; // int
Build & render — construct a document in code
$doc = [ 'name' => 'frontend', 'port' => 8443, 'tls' => true, 'ratio' => 0.95, 'upstreams' => [ ['host' => 'a.example', 'port' => 1080], ['host' => 'b.example', 'port' => 1080], ], 'notes' => null, ]; $text = Ktav::dumps($doc);
A complete runnable example lives in examples/basic.php.
API
| Method | Purpose |
|---|---|
Ktav::loads(string $src): mixed |
Parse a Ktav document. |
Ktav::loadsStrict(string $src): mixed |
Parse with strict numeric spelling checks. |
Ktav::dumps(array $value): string |
Render an associative array as Ktav text. |
Ktav::format(string $src): string |
Normalise a document's spelling, keeping comments. |
Ktav::nativeVersion(): string |
Version of the loaded ktav_cabi. |
Formatting
Ktav::format() takes Ktav source text and returns Ktav source
text. It normalises structure to canonical form (§ 5.9) while keeping
the trivia the canonical writer drops:
echo Ktav::format("## the server\nserver: {host: a, port: 80}\n"); // ## the server // server: { // host: a // port: 80 // }
Every comment survives verbatim — Ktav has no trailing comments (§ 3.4: a comment owns a whole line), so attachment is unambiguous. Blank lines survive as a grouping hint, but a run of two or more collapses to exactly one and blank padding just inside a bracket is dropped, which makes the transform a fixed point: formatting already-formatted text changes nothing. Key order is never changed — canonical form has no sorting rule, and reordering keys would make review diffs worse.
Errors
KtavException is thrown on any parse or render failure. Beyond a
human-readable getMessage(), it carries the nine structured fields of
the core's error envelope:
try { Ktav::loadsStrict("a: 1.10\n"); } catch (KtavException $e) { $e->getError(); // "LossyScalar" $e->getBody(); // "1.10" — as written $e->getCanonical(); // "1.1" — as it would be stored $e->getSpecSection(); // "§3.6/§5.2" $e->getSpan(); // ["start" => 0, "end" => 7] }
The full set is getError(), getReason(), getErrorLine(),
getLineText(), getSpan(), getPath(), getBody(),
getCanonical(), getSpecSection(). Absent information is null,
never a missing accessor, so a caller can read any field without
checking the error class first.
getPath() is an array of exact decoded key segments, never a joined
string: a key literally named a.b is one segment and cannot be
confused with a two-segment path.
Two writer rejections are named apart — "UnrepresentableAt" when the
writer can say which node is at fault (it fills getPath() too), and
"Unrepresentable" when it cannot. The reason code is the same in
both, so matching on getReason() is enough when you only need to know
that a write was refused.
getErrorLine() rather than getLine(), because PHP declares
Exception::getLine() final.
Type mapping
| Ktav | PHP |
|---|---|
null |
null |
true / false |
bool |
| bare integer | int if it fits, else string (PHP has no native bigint — wrap your own GMP / BCMath if you need arithmetic). |
| bare decimal | float |
| other scalar | string |
[ ... ] |
sequential array |
{ ... } |
associative array (insertion order preserved) |
To emit an arbitrary-precision integer, wrap the digit string yourself:
['big' => ['$i' => '9999999999999999999']] — same envelope used on the
wire between PHP and the native side.
Key escaping
Since spec 0.6.4 a literal . or : inside a key segment is written
with a backslash:
a\.b: v # key is the single segment "a.b" → ["a.b" => "v"]
a\:b: v # key contains a colon → ["a:b" => "v"]
x.y\.z: v # split on the first dot only → ["x" => ["y.z" => "v"]]
A literal backslash in a key is \\.
How the native library is resolved
On first call:
KTAV_LIB_PATHenv var, if set.- User cache —
<userCache>/ktav-php/v<version>/<asset>, downloaded on a previous call. - GitHub Release download — fetched once from
github.com/ktav-lang/php/releases/download/v<version>/<asset>and cached under (2). Requires network on first call after install.
<userCache> is %LOCALAPPDATA% on Windows, ~/Library/Caches on
macOS, $XDG_CACHE_HOME or ~/.cache on Linux.
Runtime support
- PHP 7.4 / 8.0 / 8.1 / 8.2 / 8.3+. Tested on the LTS lines on every CI run.
- Prebuilt binaries for:
linux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64,windows/arm64. - Linux distros must use glibc 2.17+ (zigbuild baseline). Alpine (musl) support is planned.
License
MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-APACHE.
Other Ktav implementations
spec— specification + conformance suiterust— reference Rust crate (cargo add ktav)csharp— C# / .NET (dotnet add package Ktav)golang— Go (go get github.com/ktav-lang/golang)java— Java / JVM (io.github.ktav-lang:ktavon Maven Central)js— JS / TS (npm install @ktav-lang/ktav)python— Python (pip install ktav)