Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authoring Examples

Receive the stranger who beareth one seed as gladly as the caravan bearing a thousand jars; harvest judgeth the gift by what awakeneth, not by the noise of its arrival.

Ordinances of the Synthetic Dawn 30:27

A traveler bearing one luminous seed and a caravan of jars welcomed at the same harvest gate

Akashi discovers Markdown documents and PHP source files, extracts PHP fenced blocks and PHPDoc references to canonical PHP files, and preserves their maintained source locations. Corpus selection controls which documentation files participate; example metadata adds identity and runtime behavior but does not make unmarked PHP fences disappear.

Build a Corpus

Create a source configuration from an absolute project root, then add project-relative files and directories:

<?php

use jbboehr\Akashi\Source\DocumentationSource;

$corpus = DocumentationSource::forProject(dirname(__DIR__))
    ->includeFile('README.md')
    ->includeDirectory('docs')
    ->includeDirectory('src')
    ->exclude('docs/archive')
    ->load();

Each configuration method returns a new immutable value. Scalar path syntax is checked immediately; filesystem paths, readability, and document identity are checked by load(). DocumentationSource selects case-sensitive .md and .php files and dispatches each format to its corresponding extractor.

includeFiles() accepts an array, generator, or iterator of project-relative strings, ProjectPath values, or SplFileInfo objects. A Symfony Finder configured with files() can therefore be passed directly without adding Symfony Finder as an Akashi dependency. Directory includes remain available for the zero-dependency common case.

Exclusions match an exact file or a complete directory subtree. See Configuration for ordering, symlink, duplicate-document, and failure behavior. MarkdownSource remains available when a project wants an explicitly Markdown-only manifest.

Choose documents whose PHP fences are meant to participate in at least one configured workflow. Use compile-only for valid PHP that PHPUnit should parse without executing, and runtime skip when PHPUnit should report a skipped data set. Akashi has no global ignore directive; use another fence language for fragments that should not enter the corpus, or exclude their containing document.

Write PHP Fences

Akashi selects a fenced block when the first word of its info string is php, compared case-insensitively. An opening PHP tag is optional:

```php
$message = sprintf('Hello, %s!', 'Akashi');

assert($message === 'Hello, Akashi!');
```

Backtick and tilde fences, longer fences, indentation, block quotes, and other CommonMark structure are handled by the CommonMark parser. Additional info-string words are retained as metadata but do not currently change Akashi behavior.

Every inline example retains the original code, document, fence metadata, line and byte spans, and its ordinal in the document. Generated inline IDs combine a hash of the project-relative path with that ordinal. Moving the document or inserting an earlier PHP fence therefore changes the generated ID. Referenced examples instead derive stable IDs from their canonical project-relative path and optional region name.

The exact generated form is example-{first 12 hexadecimal characters of sha1(project-relative path)}-{ordinal}, with the ordinal padded to at least two digits. Use an explicit example ID when another tool needs an identity that survives reordering.

Write PHPDoc Fences

Every php fence on the interior lines of a selected /** ... */ comment enters the corpus, whether or not the comment is attached to a named declaration:

<?php

namespace Acme;

/**
 * Return a stable display name.
 *
 * ```php
 * $name = \Acme\Text::displayName('akashi');
 *
 * assert($name === 'AKASHI');
 * ```
 */
final class Text
{
    public static function displayName(string $name): string
    {
        return strtoupper($name);
    }
}

Akashi removes conventional docblock indentation, the leading *, and one following space before CommonMark parsing. The opening /** and closing */ lines are delimiters rather than Markdown content, so put fences and prose on the interior lines. An opening <?php tag inside the fence remains optional.

The extracted code is prefix-free, while failures refer to the original .php path and PHPDoc line. Each docblock is parsed independently, so metadata cannot associate with a fence in a later comment. Use another fence language for a PHP fragment that should not enter any workflow.

Akashi extracts the fence, not its surrounding declaration. The example above therefore calls the project class through its fully qualified, Composer-autoloadable name. Supporting declarations must already be available through normal project bootstrap or autoloading, or be written inside the fence.

Reference Canonical PHP Examples

Use an inline PHPDoc fence for a short demonstration tied closely to one symbol. For a substantial or reused example, keep ordinary PHP as the source of truth and reference it from PHPDoc:

/**
 * @akashi-example examples/conversion.php#basic-conversion
 */

The target is relative to the configured project root. It names either a whole case-sensitive .php file or one stable named region. A canonical region file remains ordinary valid PHP:

<?php

declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';

// akashi-region: basic-conversion
$result = convert(1, 'meter', 'centimeter');

assert($result === 100);
// akashi-region-end: basic-conversion

Akashi executes and analyzes only the bytes between the named marker lines. The surrounding opening tag, bootstrap, and other regions keep the complete file directly executable and friendly to IDEs, formatters, and static-analysis tools. Whole-file references use the complete file instead. A named region must not rely on a surrounding require, use, or other file-level setup being copied into the example; keep required source inside the region or provide project setup through the normal runtime and PHPStan configuration.

Region markers must be standalone PHP line comments with matching lowercase kebab-case names. Missing, malformed, orphaned, mismatched, nested, duplicate, and empty regions fail during load() instead of being guessed at. Stable names are deliberately used instead of line-number ranges, which unrelated edits would shift.

The default reference tag is @akashi-example. To consume another public tag convention, replace the accepted set:

$source = DocumentationSource::forProject(dirname(__DIR__))
    ->includeDirectory('src')
    ->withPhpDocReferenceTags('example');

Pass more than one name to accept a migration overlap, for example withPhpDocReferenceTags('akashi-example', 'example'). Akashi’s model remains independent of PHPDocumentor; configuring example does not adopt PHPDocumentor’s line-range behavior or trailing-description syntax.

The canonical PHP file need not also be in the include manifest. It must resolve to a readable .php file inside the same canonical project root. Multiple PHPDoc sites may reference the same whole file or region; Akashi creates one example and retains every presentation site for tooling and future renderer integrations. Failures point to the canonical PHP code line, while ReferencedExampleSource::$references preserves the referring PHPDoc locations. Resolution is not recursive: PHPDoc inside a referenced file is scanned only when that file is also selected by the source manifest.

External references are currently a PHPDoc authoring mode. Markdown continues to use physically embedded fences.

Synchronized Presentations

Some documentation renderers cannot include an external file directly. Akashi can inspect an inline presentation whose canonical source remains an ordinary PHP file or named region:

<!-- akashi-sync: examples/conversion.php#basic-conversion -->

```php
$result = convert(1, 'meter', 'centimeter');
assert($result === 100);
```

<!-- akashi-sync-end -->

The start comment, PHP fence, and end comment must remain consecutive Markdown blocks, with only optional blank lines between them. This form is stable under formatters such as Prettier, which insert those blank lines. The fence must be explicitly closed and labelled php. The target follows the same project-relative .php path and optional lowercase named-region rules as a PHPDoc external reference. The same canonical target may be presented in several documentation locations. Directive names are case-sensitive; a case variant that resembles an Akashi directive is rejected as malformed rather than silently ignored.

SynchronizationChecker parses this form in Markdown and conventional multiline PHPDoc comments and returns typed SynchronizationMismatch values without modifying the document. Its library-only rewrite seam returns a new Document containing the canonical code without writing it to disk:

$checker = SynchronizationChecker::forProject($projectRoot);
$updated = $checker->rewrite($document);

$updated->contents; // Corrected document bytes.

The rewrite changes only recorded code spans. It retains directives, fences, prose, Markdown or PHPDoc container prefixes, and the local line-ending convention, and it rejects canonical code that would break the surrounding fence or PHPDoc structure. Rewriting the returned document again is a no-op.

To check explicit files in CI without writing them, run:

vendor/bin/akashi sync --check --project-root=. README.md docs/examples.md src/Example.php

The command is silent when every presentation is current. Stale presentations receive a source-labelled unified diff from their embedded code to the canonical replacement. Stale or invalid presentations are reported on stderr and exit with status 1. To apply the same validated replacements, select write mode explicitly:

vendor/bin/akashi sync --write --project-root=. README.md docs/examples.md src/Example.php

Akashi validates every selected document before the first write, rejects documents changed since loading, and atomically replaces each changed file through a temporary sibling. See the CLI reference for the exact path, stream, write-safety, diff, and exit-status contract.

SynchronizationRegion::$embeddedCode contains the logical, undecorated PHP seen by CommonMark. Its location and regionSpan point into the original maintained document, so slicing those spans returns raw Markdown indentation or PHPDoc leading * decoration. This distinction preserves both comparison-ready code and the exact authored bytes that a future writer would need.

Comparison is intentionally narrow and deterministic:

  • CRLF and CR line endings compare as LF;
  • a missing final newline is treated as one final LF, while additional trailing blank lines remain significant;
  • Markdown fence indentation and conventional PHPDoc indentation and leading * decoration are containers, not code;
  • indentation inside the logical PHP fence remains significant; and
  • PHP opening tags are canonical code and are not inserted, removed, or case-normalized. A synchronized whole-file presentation therefore includes its opening tag when the canonical file does.

Malformed, orphaned, nested, overlapping, or incomplete synchronization structures fail rather than being guessed at. Canonical named-region validation continues to reject missing, malformed, nested, mismatched, empty, or duplicate regions. Synchronization is independent of the optional formatter check described below.

Check or Write Inline Formatting

Ordinary external PHP files and named regions should be formatted directly by the project’s normal PHP tooling. PHP embedded in Markdown or PHPDoc is harder for those tools to reach, so Akashi can optionally present each inline example to a project-installed PHP-CS-Fixer:

vendor/bin/akashi format --check --project-root=. README.md docs/examples.md src/Example.php

The command defaults to vendor/bin/php-cs-fixer and PHP-CS-Fixer’s normal project-root configuration discovery. Pass --php-cs-fixer=PATH or --config=PATH to select other project-relative files. PHP-CS-Fixer is optional and never runs during discovery, PHPUnit, PHPStan, extraction, synchronization, or the normal Akashi library workflow.

Akashi checks only physically embedded Markdown and PHPDoc fences from the selected documentation files. It deliberately skips referenced whole files and named regions because ordinary formatter commands already cover them. Each inline body is placed in a private temporary PHP file, PHP-CS-Fixer runs through an explicit argument vector without constructing a shell command or using a cache, and Akashi compares only the body after a protected boundary. File-level additions such as a configured license header do not enter the fence. An authored opening <?php tag and its separator are preserved outside the comparison; body line endings and final-newline changes remain significant.

Check mode never changes maintained documentation. A clean check is silent. A mismatch produces a source-labelled unified diff on stderr and status 1. Formatter launch, timeout, invalid output, and cleanup failures identify the maintained example rather than only the temporary file. Closing tags, inline HTML, and __halt_compiler() are rejected by this initial adapter because they cannot be safely enclosed.

For applications that need to inspect a proposed update without writing it, FormattingRewriter::rewrite() can apply the checked mismatches for one loaded document and return a new immutable Document. It changes only the inline code spans and re-extracts the result before returning it; stale inputs, mismatches from another document, and output that would damage a fence or PHPDoc comment are rejected.

To apply the same validated changes to the selected inline examples, use write mode:

vendor/bin/akashi format --write --project-root=. README.md docs/examples.md src/Example.php

Before writing, Akashi reloads the complete source and requires a second formatter pass to produce the same set of changes. It then rejects stale bytes and symbolic-link paths and atomically replaces each changed document from a same-directory temporary file. Write mode reports each updated project path on stderr; a current set is silent. External canonical PHP remains the responsibility of ordinary formatter commands. Hidden support code and renderer inclusion remain deferred.

Labels and PHPUnit Data Sets

The human-readable example label is derived from its source location and becomes the PHPUnit data-set name. PhpUnitExampleDataSets::fromCorpus() rejects duplicate labels before yielding the first data set, which keeps PHPUnit filters and reports unambiguous.

Use ordinary prose immediately around a fence to explain the example. Akashi does not require each example to carry a special name unless another consumer needs a durable identity.

Add a Stable Example ID

For consumer extraction, assign an example property in an Akashi metadata comment:

<!-- akashi: example=conversion-basic -->

```php
$result = convert(1, 'meter', 'centimeter');
```

Example IDs use lowercase kebab-case and must be unique across the corpus. Identity is optional metadata: load() still returns every PHP fence. The same property may appear as // akashi: example=conversion-basic inside fenced or referenced canonical PHP. Continue to Extracting Named Examples when a consumer needs one named example.

Projects retaining an older marker comment such as <!-- yumemi-example: conversion-basic --> can add that dialect with withMarkerName('yumemi-example'). Canonical akashi: metadata remains recognized alongside it.

Add a Runtime Directive

Akashi currently recognizes skip, compile-only, and separate-process. Place metadata immediately before the PHP fence; adjacent comments and blank lines may be stacked together. Prose or an unrelated block breaks the association.

<!-- akashi: separate-process -->

```php
exit(0);
```

Unknown, duplicated, orphaned, or non-PHP metadata fails during extraction. See Example Metadata for grammar and precedence and Separate-Process Execution for backend configuration.