PHPackages                             ecourty/okf - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. ecourty/okf

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

ecourty/okf
===========

A PHP parser and writer for the Open Knowledge Format (OKF) — Markdown bundles with YAML frontmatter for versionable, human-readable knowledge documentation.

v0.1.0(1mo ago)00MITPHPPHP &gt;=8.3CI passing

Since Jul 12Pushed 1mo agoCompare

[ Source](https://github.com/EdouardCourty/php-okf)[ Packagist](https://packagist.org/packages/ecourty/okf)[ RSS](/packages/ecourty-okf/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (4)Versions (2)Used By (0)

OKF for PHP
===========

[](#okf-for-php)

[![PHP CI](https://github.com/EdouardCourty/php-okf/actions/workflows/ci.yml/badge.svg)](https://github.com/EdouardCourty/php-okf/actions/workflows/ci.yml)

A typed PHP parser and writer for the [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing?hl=en) (OKF) — directories of Markdown files with YAML frontmatter for documenting knowledge as versionable, human- and agent-readable text.

> OKF is specified by Google Cloud's [`knowledge-catalog`](https://github.com/GoogleCloudPlatform/knowledge-catalog) repository — see [`okf/SPEC.md`](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) for the full spec (currently version 0.1, draft). This library is an independent PHP implementation of that spec, not an official Google product.

Requirements
------------

[](#requirements)

- PHP 8.3+

Installation
------------

[](#installation)

```
composer require ecourty/okf
```

Quick Start
-----------

[](#quick-start)

The `Okf` facade is the simplest way to use the library — read a bundle from disk, walk its concepts, and write it back:

```
use Ecourty\Okf\Okf;

$okf = new Okf();

$bundle = $okf->read('/path/to/bundle');

foreach ($bundle->getConcepts() as $concept) {
    echo $concept->getId() . ': ' . $concept->getFrontmatter()->getTitle() . "\n";
}

$orders = $bundle->getConcept('tables/orders');
echo $orders->getBody();

$okf->write($bundle, '/path/to/output');
```

### Reading a Single Concept

[](#reading-a-single-concept)

```
$concept = $bundle->getConcept('tables/orders'); // throws ConceptNotFoundException if missing

$frontmatter = $concept->getFrontmatter();
$frontmatter->getType();        // 'BigQuery Table'
$frontmatter->getTitle();       // 'Customer Orders'
$frontmatter->getTags();        // ['sales', 'orders', 'revenue']
$frontmatter->getExtra();       // ['owner_team' => 'data-platform', ...] — producer-defined keys, preserved as-is

$concept->getBody();            // the Markdown body, as a string
```

### Reading index.md and log.md

[](#reading-indexmd-and-logmd)

`index.md` and `log.md` are reserved filenames — the library exposes their raw Markdown body directly rather than parsing their loosely-conventional structure, so nothing is ever silently dropped:

```
$rootIndex = $bundle->getIndex();          // index.md at the bundle root, or null if absent
$tablesIndex = $bundle->getIndex('tables'); // tables/index.md, or null if absent

$rootIndex?->getOkfVersion();              // the declared OKF version, if any (root index.md only)
$rootIndex?->getBody();                    // raw Markdown

$log = $bundle->getLog();                  // log.md at the bundle root, or null if absent
$log?->getBody();
```

### Lazy Iteration

[](#lazy-iteration)

For large bundles, iterate concepts one at a time instead of building the whole `Bundle` aggregate in memory:

```
foreach ($okf->iterateConcepts('/path/to/bundle') as $concept) {
    // ...
}
```

### Modifying and Writing a Concept

[](#modifying-and-writing-a-concept)

All models are immutable — "modifying" a concept means constructing a new one:

```
use Ecourty\Okf\Model\ConceptDocument;
use Ecourty\Okf\Model\Frontmatter;

$original = $bundle->getConcept('tables/orders');

$updated = new ConceptDocument(
    id: $original->getId(),
    path: $original->getPath(),
    frontmatter: new Frontmatter(
        type: $original->getFrontmatter()->getType(),
        title: $original->getFrontmatter()->getTitle(),
        tags: [...$original->getFrontmatter()->getTags(), 'reviewed'],
        extra: $original->getFrontmatter()->getExtra(),
    ),
    body: $original->getBody(),
);

$okf->getWriter()->writeConcept('/path/to/bundle', $updated);
```

### Creating a New Concept

[](#creating-a-new-concept)

```
use Ecourty\Okf\Model\ConceptDocument;
use Ecourty\Okf\Model\Frontmatter;

$concept = new ConceptDocument(
    id: 'metrics/daily-active-users',
    path: 'metrics/daily-active-users.md',
    frontmatter: new Frontmatter(
        type: 'Metric',
        title: 'Daily Active Users',
        description: 'Count of distinct users who performed at least one action in a day.',
        tags: ['growth', 'engagement'],
    ),
    body: "# Definition\n\nCounted from the `events` table, deduplicated by `user_id` per UTC day.\n",
);

$okf->getWriter()->writeConcept('/path/to/bundle', $concept);
```

### Validation

[](#validation)

Neither reading nor writing ever validates — a concept missing `type` parses and writes back out without error, matching the OKF spec's permissive consumption model. Validation is a separate, explicit, opt-in step:

```
use Ecourty\Okf\Validator\ConceptDocumentValidator;
use Ecourty\Okf\Validator\ValidationMode;

$validator = new ConceptDocumentValidator();

// Lenient (default): only `type` is required, per OKF SPEC.md §9.
$violations = $validator->getViolations($concept, ValidationMode::Lenient);

// Strict: `type`, `title`, `description` and `timestamp` are all required.
$violations = $validator->getViolations($concept, ValidationMode::Strict);

// Or throw directly:
$validator->validate($concept, ValidationMode::Strict); // throws InvalidDocumentException
```

**Linting a whole bundle:**

```
foreach ($bundle->getConcepts() as $concept) {
    $violations = $validator->getViolations($concept, ValidationMode::Lenient);

    foreach ($violations as $violation) {
        echo "{$concept->getId()}: {$violation}\n";
    }
}
```

### Unknown Keys and Round-Trip Fidelity

[](#unknown-keys-and-round-trip-fidelity)

Producer-defined frontmatter keys — and known keys whose value doesn't match the expected type (e.g. `tags` given as a scalar instead of a list) — are preserved rather than dropped, so a read → write cycle never silently loses data:

```
$frontmatter = $concept->getFrontmatter();
$frontmatter->getExtra(); // every key not covered by the OKF spec, as given in the source YAML
$frontmatter->toArray();  // known + extra keys, ready to round-trip back to YAML
```

Error Handling
--------------

[](#error-handling)

All exceptions extend `OkfException` (a `RuntimeException`) and carry structured context:

```
use Ecourty\Okf\Exception\BundleNotFoundException;
use Ecourty\Okf\Exception\ConceptNotFoundException;
use Ecourty\Okf\Exception\FrontmatterParseException;
use Ecourty\Okf\Exception\InvalidDocumentException;

try {
    $bundle = $okf->read('/path/to/bundle');
    $concept = $bundle->getConcept('tables/missing');
} catch (BundleNotFoundException $e) {
    echo $e->getPath();
} catch (ConceptNotFoundException $e) {
    echo $e->getConceptId();
} catch (FrontmatterParseException $e) {
    echo $e->getSource() . ': ' . $e->getReason(); // e.g. unterminated "---" block
} catch (InvalidDocumentException $e) {
    echo $e->getConceptId() . ': ' . implode(', ', $e->getViolations());
}
```

Custom Filesystem
-----------------

[](#custom-filesystem)

`BundleParser`/`BundleWriter` read and write through a single `FilesystemInterface` seam (`exists`/`read`/`write`/`listFiles`), defaulting to `LocalFilesystem`. Provide your own implementation to read from a zip archive, an in-memory store, a remote object store, etc.:

```
use Ecourty\Okf\Filesystem\FilesystemInterface;
use Ecourty\Okf\Okf;
use Ecourty\Okf\Parser\BundleParser;
use Ecourty\Okf\Writer\BundleWriter;

final class MyFilesystem implements FilesystemInterface
{
    // exists(), read(), write(), listFiles()
}

$filesystem = new MyFilesystem();

$okf = new Okf(
    parser: new BundleParser($filesystem),
    writer: new BundleWriter($filesystem),
);
```

Examples
--------

[](#examples)

See the [`examples/`](examples/) directory for runnable scripts:

```
php examples/01-read-and-modify.php
php examples/02-validate-bundle.php
php examples/03-create-new-concept.php
```

Development
-----------

[](#development)

```
composer install

# Run all tests
composer test

# Run specific test suites
composer test-unit
composer test-integration

# Static analysis (PHPStan, level max)
composer phpstan

# Code style (PHP CS Fixer)
composer cs-fix       # fix
composer cs-check     # dry-run check

# Full QA pipeline (PHPStan + CS check + tests)
composer qa
```

License
-------

[](#license)

This library is released under the [MIT License](LICENSE).

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

50d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3150ffb131124e5f03272d9ed8084c514f18fff6aafff1a5973c016993f6ef66?d=identicon)[ecourty](/maintainers/ecourty)

---

Top Contributors

[![EdouardCourty](https://avatars.githubusercontent.com/u/37371516?v=4)](https://github.com/EdouardCourty "EdouardCourty (3 commits)")

---

Tags

parserdocumentationyamlmarkdownwriterfrontmatterKnowledge Baseokfopen-knowledge-format

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ecourty-okf/health.svg)

```
[![Health](https://phpackages.com/badges/ecourty-okf/health.svg)](https://phpackages.com/packages/ecourty-okf)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.3k42.4k21](/packages/tempest-framework)[daux/daux.io

Documentation generator that uses a simple folder structure and Markdown files to create custom documentation on the fly

830205.2k1](/packages/daux-dauxio)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12323.1k](/packages/rcsofttech-audit-trail-bundle)[dallgoot/yaml

Provides loader, dumper and an API for YAML content. Loader builds to equivalent data types in PHP 8.x

44276.5k11](/packages/dallgoot-yaml)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1718.2k18](/packages/2lenet-crudit-bundle)[pagerange/metaparsedown

Adds ability to have meta data in markdown files parsed by eursev/parsedown or eruseve/parsedown-extra

2738.7k2](/packages/pagerange-metaparsedown)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
