hoa/kitab
Hoa\Kitab is a PHP library for authoring and compiling structured books. It provides tools to organize chapters and sections, handle references, and generate publishable documentation or ebook output within the Hoa ecosystem.
Kitab is a documentation compiler and test harness for the Hoa project: you write documentation in Markdown, decorate code samples with annotations, and Kitab renders the result to HTML (and can execute/lint the samples as part of your test suite). The project is effectively dormant — 81 stars, zero dependents, no roadmap — but it is not completely frozen: release 0.12 landed after the long gap (Bors CI, anonymous-class support in the IR parser, nikic/php-parser 3 → 4). Treat it as a small, self-contained tool you vendor once and pin, rather than a stack you grow with.
Minimal path in:
composer.json (the bin entry, the hoa/* dependency set, and now the nikic/php-parser constraint — it is ^4.0 on 0.12, ^3.0 before), the README.md example, then Hoa/Kitab/Book.php and Hoa/Kitab/Chapter.php. Those two classes are the model; everything else (compilers, testers) hangs off them.composer require --dev hoa/kitab:0.12.* and commit composer.lock. Unpinned hoa/* metadata is loosely constrained and will drift; likewise an unpinned php-parser will silently pull a different major the next time you touch the lockfile. Plan for PHP 7.x in a container: the php-parser 4 bump moves the parser's own floor to PHP 7.0 and removes PHP 5 support, but Hoa's runtime packages are still what breaks on 8.x.Kitab.php at your docs root. It is plain PHP, executed via include, that assembles a Hoa\Kitab\Book and adds Hoa\Kitab\Chapter objects, each pointing at one Markdown file. Keep it to paths and ordering only.Chapter), run the CLI (vendor/bin/kitab — check composer.json bin and the README for the exact subcommand), and render it to a scratch directory. Compare the output HTML against what your current docs site produces; this tells you immediately whether the Markdown dialect is compatible before you migrate more pages.composer test. If you only adopt one half of Kitab, adopt this half — rendering is a commodity, executing docs is not.If step 4 produces garbage for your existing Markdown, stop there. Kitab's parser is a Hoa grammar, not CommonMark, and retrofitting a large existing docs tree is usually more work than rewriting the pages.
Programmatic book assembly. Kitab.php is arbitrary PHP, so build the book in a loop for anything beyond a handful of chapters:
// doc/Kitab.php
$book = new Hoa\Kitab\Book();
foreach (glob(__DIR__ . '/chapters/*.md') as $file) {
$book->addChapter(new Hoa\Kitab\Chapter(
basename($file, '.md'),
$file
));
}
return $book;
Use __DIR__ everywhere. Explicit addChapter ordering is the alternative when the narrative order matters more than alphabetical order — for a tutorial, it always does.
Annotated code blocks. Kitab's convention is that comments inside fenced code blocks act as anchors so prose can reference specific lines (and the renderer can highlight or strip them). The workflow is: write the sample, add anchors, reference those anchors from the surrounding paragraph. Keep anchor numbering local to each block and sequential; the renderer relies on those markers to build links, and hand-edited gaps show up as dead references. Verify anchor rendering on one page before adopting it across the book, since this is the part most likely to need style overrides in your HTML template.
Documentation-as-test. Mark the samples you want executed and run the documentation test pass in CI alongside PHPUnit. A practical split:
Snippets are parsed by php-parser, so write modern PHP. The IR path is built on nikic/php-parser. On 0.12 you can write realistic samples — including anonymous classes (new class implements Handler { … }, the idiomatic way to show a one-off implementation or a test double) — and they will parse instead of blowing up. Before 0.12 that construct was unsupported and would surface as a parse/IR error that looked like your snippet was malformed. This is the single most useful change in the release for docs authors; prefer anonymous classes over "define a named class above the snippet" workarounds now that they are safe.
Upgrade your visitors with the parser bump. If you had subclassed a compiler or written a custom NodeVisitor against php-parser 3 (the documented extension point for post-processing extracted blocks), 0.12 breaks you: 3.x and 4.x differ in node/visitor APIs and in the parser's node-traversal behaviour. Port the visitor in the same commit as the version bump rather than discovering it in CI.
Keep executable snippets hermetic. Design samples so each one is self-contained: its own use statements, its own fixtures, no reliance on state established by a previous block. Samples that depend on earlier ones make failures impossible to localize.
Render for the site, test for the gate. Run the renderer only in the docs deploy job (output into a gitignored directory, published as static assets), and run the test pass on every PR. Rendering on every PR slows CI for no signal.
Fixture sharing. Put small example programs in examples/ so the same file is both the Markdown sample (via include or copy) and something PHPUnit can execute. One artifact, two consumers, no drift.
Integration tip: expose it as composer scripts — "doc:build": "kitab …", "doc:test": "kitab …" — and call those from Make/CI rather than embedding CLI flags in YAML. When you eventually replace Kitab (likely, given its activity level), you change one file.
Check for a php-parser conflict before you start, not after. 0.12 requires nikic/php-parser ^4.0. Anything else in your tree pinning 3.x — older PHPStan, phpDocumentor, some code-generator packages, a locally patched tool — will make composer require either fail or downgrade Kitab to the older release. Run composer why nikic/php-parser first. If you are stuck on 3.x, you are on pre-0.12 Kitab and lose anonymous-class parsing.
Archived-adjacent means frozen-ish, not stable. There is no support contract behind 0.12 and no promise of a 0.13. Expect friction from Hoa's runtime packages (hoa/consistency, hoa/stream, hoa/protocol) around error-handler signatures and reflection usage; the parser bump does not fix those. If it fatals, don't debug it — pin PHP 7.4 in a Docker image, install with the lock file, and move on. (If Composer happens to resolve php-parser ≥ 4.10, the parser itself tolerates PHP 8, but Hoa remains the blocker.)
The hoa:// protocol wrapper is a trap for newcomers. Hoa packages commonly resolve paths through their own stream wrapper, so a relative path in Kitab.php may resolve against the Hoa root rather than your doc directory. Always pass absolute paths built from __DIR__, and be suspicious of any "file not found" where the file plainly exists. Related: the test harness may not inherit your php.ini include_path, so autoloading inside snippets can behave differently than under PHPUnit.
Markdown support is a grammar, not CommonMark. Tables, footnotes, nested fences, HTML blocks, and unusual indentation are candidates for silent misparse or a compiler exception. Symptoms come out as grammar trace messages that name token types rather than line contents. Debug by compiling one chapter at a time — bisect the book down to a single Chapter and a single code block. Note that "it parses" and "it parses into something usable" are different stages: 0.12's anonymous-class fix was in the intermediate representation, one step past the grammar, which is exactly why failures there are hard to read.
Kitab.php failures are fatal, not graceful. It is included as PHP, so a typo or a missing use surfaces as an uncaught error with the CLI's stack trace, often looking like a Kitab bug. Keep the file declarative and small; do not put logic, environment lookups, or conditionals in it.
Executed snippets share a process. Global state, defined functions/classes, and constants can leak between samples; a snippet containing exit/die will terminate the whole run and look like a hang or a truncated report. Namespace your snippet classes and avoid defining anything at global scope twice — anonymous classes reduce this pressure, but the moment you name a class in a sample you own the collision risk.
Annotation comments are not free. If a sample's output or behavior depends on a literal comment line, the render step may strip or rewrite it. Keep anchors syntactically distinct from comments you actually care about.
Rebuilds are not incremental. Clear the output directory before every render or you will publish stale pages and stale assets after a chapter is renamed or removed. Wire the clean into the same script as the build so nobody can forget.
Extension points, in order of cost: (1) override the HTML template/CSS for code block presentation; (2) subclass Chapter or a compiler class to post-process extracted blocks before rendering (budget for the php-parser 4 API when you do); (3) write a custom visitor against the Hoa\Compiler grammar if you need a different Markdown dialect. Only (1) and (2) are realistically maintainable for a low-activity dependency.
One release is not a revival — keep the exit plan. Bors support and a dependency bump are maintenance hygiene, not a roadmap, and zero dependents is still your bus factor. Before adopting, write down the exit: your Kitab.php plus Markdown files should be portable to a plain static-site generator, with the documentation tests reimplemented as a small custom script that extracts fenced blocks and runs them. That script is maybe 60 lines; keep it in your back pocket so "unmaintained" never becomes "blocked".
How can I help you explore Laravel packages today?