Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/docs export-ignore
/tests export-ignore
/examples export-ignore
/tools export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.gitmessage export-ignore
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@ jobs:
- run: node --check resources/js/client.js
- run: node --test tests/js/*.test.mjs

api-reference:
# ADR-0028: docs/api is generated from src/ and committed. It must match the source, and real
# front-matter tooling (gray-matter) must be able to read and follow it.
name: API reference
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
tools: composer:v2
coverage: none
- run: composer install --no-interaction --no-progress --prefer-dist
- name: Reference matches the source
run: composer docs:check
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# Only the front-matter parser is needed here, not Playwright or wp-env's lifecycle scripts.
- run: npm ci --ignore-scripts
- run: node --test tests/docs/api-reference.test.mjs

integration:
# Adapters against a real WordPress + MySQL (wp-env tests container), PHPUnit 10 with a
# wp-load.php bootstrap (ADR-0010 amendment).
Expand Down
44 changes: 44 additions & 0 deletions bin/generate-api-docs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Generates the API reference (docs/api/) from src/ (ADR-0028).
*
* php bin/generate-api-docs.php write the reference
* php bin/generate-api-docs.php --check exit 1 if the files on disk differ from the source
*
* Exit codes: 0 current or written, 1 out of date (--check), 2 the generator could not run.
*
* @license GPL-2.0-or-later
*/

declare(strict_types=1);

use Codad5\WPToolkit\Tools\ApiDocs\Generator;

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

try {
$generator = Generator::fromConfigFile($root);
$files = $generator->build();

if (in_array('--check', $argv, true)) {
$problems = $generator->drift($files);
foreach ($problems as $problem) {
fwrite(STDERR, $problem . PHP_EOL);
}
if ($problems !== []) {
fwrite(STDERR, "The API reference is out of date. Run: composer docs\n");
exit(1);
}
fwrite(STDOUT, sprintf("API reference is current (%d files).\n", count($files)));
exit(0);
}

$generator->write($files);
fwrite(STDOUT, sprintf("Wrote %d files.\n", count($files)));
exit(0);
} catch (Throwable $error) {
fwrite(STDERR, 'Could not generate the API reference: ' . $error->getMessage() . PHP_EOL);
exit(2);
}
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"autoload-dev": {
"psr-4": {
"Codad5\\WPToolkit\\Tests\\": "tests/",
"Codad5\\WPToolkit\\Tools\\ApiDocs\\": "tools/apidocs/src/",
"WptkTodo\\": "examples/todo/src/"
}
},
Expand All @@ -71,7 +72,7 @@
"sort-packages": true
},
"scripts": {
"lint": "@php bin/lint.php src tests bin bootstrap examples",
"lint": "@php bin/lint.php src tests bin bootstrap examples packages tools",
"cs": "phpcs",
"analyse": "phpstan analyse --memory-limit=1G",
"test": "phpunit",
Expand All @@ -81,8 +82,11 @@
"@cs",
"@analyse",
"@test",
"@i18n"
"@i18n",
"@docs:check"
],
"docs": "@php bin/generate-api-docs.php",
"docs:check": "@php bin/generate-api-docs.php --check",
"i18n": "wp i18n make-pot . languages/wptoolkit.pot --slug=wptoolkit --domain=wptoolkit --package-name=WPToolkit --include=src,bootstrap",
"test:integration": "npx wp-env run tests-cli --env-cwd=wp-content/plugins/wptoolkit vendor/bin/phpunit -c phpunit.integration.xml.dist"
},
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ order it gets built in, **reference/** holds facts that change. Agent rules live

[Plan, timeline, dependency graph →](phases/README.md) · live progress in [STATUS.md](../STATUS.md)

## api/

[The API reference](api/index.md): one generated page per public class, in reading order, with machine-readable front matter ([ADR-0028](adr/0028-the-api-reference-is-generated-from-the-source.md)). `composer docs` regenerates it.

## reference/

[Open decisions](reference/open-decisions.md) · [Tech stack](reference/tech-stack.md) · [Work log](reference/work-log.md)
75 changes: 75 additions & 0 deletions docs/adr/0028-the-api-reference-is-generated-from-the-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ADR-0028 — The API reference is generated from the source, with machine-readable front matter

- **Status:** Accepted
- **Date:** 2026-10-18
- **Deciders:** codad5
- **Affects:** `docs/api/` (generated), `tools/apidocs/`, `bin/generate-api-docs.php`, `composer verify`, CI

## Context

The guides explain how to do things; nothing lists what exists. The plan deferred "API reference from
docblocks" to 1.1, and 0.x's hand-maintained `API.md` (5,075 lines) is the cautionary tale: it went stale
and was retired. The library has about 150 classes whose docblocks already say *why* (a repo rule), and its
readers now include AI agents and documentation tools that want to walk the reference in order and read
facts about each page without parsing prose.

## Decision

1. **The reference is generated, never hand-written.** `composer docs` reads `src/` with Reflection
(signatures, modifiers, defaults — always in step with the code) and the docblocks (the prose), and
writes `docs/api/`. The files are committed, so GitHub and agents can read them without running
anything. `composer docs:check` (part of `composer verify` and CI) fails when the files differ from the
source, or when a file is stale — the same pattern as the translation template.
2. **What is documented:** public classes, interfaces, enums and traits of the namespaces listed in
`tools/apidocs/config.json`; their public constants, enum cases, properties and methods. Anything tagged
`@internal` (a class, a method, a constant) is left out: hook callbacks and wiring are not API.
Namespaces are added to the config as their docblocks are reviewed.
3. **One page per class**, at `docs/api/{Layer}/…/{Class}.md`, plus `{Layer}/index.md` per top-level namespace
and `docs/api/index.md`. The reading order is one chain through all pages: the layers in the order the
config lists them (the order to learn the library), each layer's classes in the order the config names
first and then alphabetically.
4. **Every page starts with YAML front matter**, schema `wptoolkit-api/1`, in a fixed field order, all
strings double-quoted:

| Field | On | Meaning |
| --- | --- | --- |
| `schema` | all | `"wptoolkit-api/1"`; changes only with an incompatible change to this table |
| `title` | all | The class's short name, or the layer name |
| `type` | all | `class`, `interface`, `enum`, `trait`, `namespace` or `index` |
| `fqcn` | classes | Fully qualified class name |
| `namespace` | classes, layer pages | Its namespace |
| `layer` | all | Top-level namespace under `Codad5\WPToolkit` (empty on the index) |
| `source` | classes | Path of the source file, relative to the repository root |
| `since` | classes | The `@since` tag; `1.0.0` when there is none |
| `stability` | classes | `stable`, or `deprecated` when the class carries `@deprecated` |
| `order` | all | Position in the reading order, from 0, unique |
| `summary` | all | The first paragraph of the docblock, on one line |
| `related` | when any | `{ adr: [7, 13] }`: ADRs the class or its methods cite |
| `prev`, `next`, `up` | when any | `{ title, path }`; `path` is relative to **this page**, so it is also a working Markdown link |

The footer repeats previous / up / next as ordinary links, for people reading on GitHub.
5. **`docs/api/index.json`** lists every page in reading order with the same facts. Its paths are relative
to `docs/api/` (not to each page), so `prev`, `next` and `up` join directly to other entries' `path`.
6. **The output is deterministic**: no dates, LF line endings, forward slashes, a fixed order. Values that
change with every release (`Application::VERSION`) are listed in the config and printed as
`(set at release)`, so a version bump does not make the reference stale.
7. **No dependencies.** The generator is a PHP script under `tools/` (dev-only, `export-ignore`d). Node's
test job parses the front matter with gray-matter (a dev dependency recorded in `tech-stack.md`) to
prove that real front-matter tooling reads it.

## Options considered

- **phpDocumentor** (what the plan named). A large dependency with its own site theme; it does not emit
the machine-readable ordering and links agents need, and its output is not Markdown to commit.
- **Hand-written pages.** The `API.md` failure again.
- **Markdown with no front matter.** An agent would have to parse headings and links to learn what a page
is, where it sits and what comes next.
- **JSON only.** Good for tools, useless for a person browsing GitHub.

## Consequences

- A thin docblock shows as a thin page; the reference is also a to-do list for documentation.
- A public method that should not be API must be marked `@internal`.
- A change to `src/` that alters a public signature or docblock fails CI until `composer docs` is run and
the result committed.
- Changing the front matter fields is a schema change: bump `wptoolkit-api/1` and update this table.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Not for a naming choice, a library with no real alternative, or anything you wou
| [0022](0022-shortcodes-are-declared-validated-and-lazy.md) | Shortcodes are declared, validated, access-checked and load assets lazily | Accepted |
| [0023](0023-settings-may-keep-a-legacy-option-prefix.md) | Settings may keep an option prefix that already exists in users' data | Accepted |
| [0024](0024-the-packager-scopes-the-consumers-code-too.md) | The packager scopes the consumer's code and autoloader, not only the library | Accepted |
| [0028](0028-the-api-reference-is-generated-from-the-source.md) | The API reference is generated from the source, with machine-readable front matter | Accepted |
| [0027](0027-multiple-choice-fields-render-as-checkboxes-with-an-optional-picker.md) | Multiple-choice fields are checkboxes, with an optional chip picker | Accepted |
| [0026](0026-builds-lock-write-atomically-and-clean-up-after-themselves.md) | Builds take a lock, write atomically and clean up after themselves | Accepted |
| [0025](0025-the-guard-can-autoload-the-plugins-own-namespaces.md) | The guard can autoload the plugin's own namespaces (completes ADR-0014) | Accepted |
178 changes: 178 additions & 0 deletions docs/api/Foundation/Application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
schema: "wptoolkit-api/1"
title: "Application"
type: "class"
fqcn: "Codad5\\WPToolkit\\Foundation\\Application"
namespace: "Codad5\\WPToolkit\\Foundation"
layer: "Foundation"
source: "src/Foundation/Application.php"
since: "1.0.0"
stability: "stable"
order: 2
summary: "One consuming plugin or theme. Owns its container, its hooks and its providers; nothing is static, so two plugins never share state (ADR-0007)."
related: { adr: [7, 13, 18, 20] }
prev: { title: "Foundation", path: "index.md" }
next: { title: "ServiceProvider", path: "ServiceProvider.md" }
up: { title: "Foundation", path: "index.md" }
---

# Application

`final class Codad5\WPToolkit\Foundation\Application`

One consuming plugin or theme. Owns its container, its hooks and its providers; nothing is
static, so two plugins never share state (ADR-0007).

Lifecycle:
1. `create()` — builds the config and container. No WordPress calls.
2. `boot()` — instantiates providers and runs every `register()` straight away.
3. On `init` (or immediately, if `init` already ran): loads the consumer's text domain, then
runs every provider's `boot()`. Translating at this point can't trigger WordPress 6.7's
"translation loading triggered too early" notice.
4. `shutdown()` — removes every hook the application added (plugin deactivation).

Source `src/Foundation/Application.php` · Since 1.0.0 · Decisions: [ADR-0007](../../adr/0007-own-container-no-static-registry.md), [ADR-0013](../../adr/0013-plugins-boot-through-a-syntax-safe-guard.md), [ADR-0018](../../adr/0018-versioned-data-migrations-expand-contract.md), [ADR-0020](../../adr/0020-incompatible-unscoped-copies-refuse-activation.md)

## Constants

| Constant | Value | Description |
| --- | --- | --- |
| `VERSION` | `(set at release)` | |

## Methods

### `onBooted()`

```php
public function onBooted(Closure $callback): void;
```

Run a callback once every provider has booted — immediately if that already happened.

| Parameter | Type | Description |
| --- | --- | --- |
| `$callback` | `Closure(): void` | |

### `isDevelopment()`

```php
public function isDevelopment(): bool;
```

WP_DEBUG on a 'local' or 'development' site: errors surface instead of being contained, and
misconfigurations (such as a route without an access rule) throw at registration.

**Returns** `bool`

### `create()`

```php
public static function create(string $file, array $config): self;
```

| Parameter | Type | Description |
| --- | --- | --- |
| `$file` | `string` | The main plugin file (or the theme's functions.php). |
| `$config` | `array<string, mixed>` | Must contain a 'slug'. Optional: 'text_domain', 'domain_path' (default 'languages'), 'type' ('plugin' or 'theme', default 'plugin'). |

**Returns** `self`

### `providers()`

```php
public function providers(array $providers): self;
```

| Parameter | Type | Description |
| --- | --- | --- |
| `$providers` | `list<class-string<ServiceProvider>>` | |

**Returns** `self`

### `migrations()`

```php
public function migrations(array $migrations): self;
```

Versioned data migrations (ADR-0018), as objects or class names (built by the container, so
they can take services). They run on activation and on `admin_init` while any is pending.

| Parameter | Type | Description |
| --- | --- | --- |
| `$migrations` | `list<Migration\|class-string<Migration>>` | |

**Returns** `self`

### `boot()`

```php
public function boot(): self;
```

Register every provider now; boot them on `init`.

**Returns** `self`

### `isInert()`

```php
public function isInert(): bool;
```

Whether the application refused to start because the loaded WPToolkit doesn't satisfy
`requires_toolkit` (ADR-0020).

**Returns** `bool`

### `shutdown()`

```php
public function shutdown(): void;
```

Remove every hook this application added. Call on plugin deactivation.

### `config()`

```php
public function config(): Config;
```

**Returns** [`Config`](Config.md)

### `identity()`

```php
public function identity(): Identity;
```

**Returns** [`Identity`](Identity.md)

### `container()`

```php
public function container(): Container;
```

**Returns** [`Container`](Container.md)

### `hooks()`

```php
public function hooks(): HookRegistrar;
```

**Returns** [`HookRegistrar`](HookRegistrar.md)

### `isBooted()`

```php
public function isBooted(): bool;
```

**Returns** `bool`

---

← [Previous: Foundation](index.md) · [Up: Foundation](index.md) · [Next: ServiceProvider](ServiceProvider.md) →
Loading
Loading