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
2 changes: 1 addition & 1 deletion .github/instructions/php.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: "Framework-development rules for rtcamp/wp-framework PHP."
## Layout & contracts

- `inc/Contracts/Interfaces/`: `Registrable`, `ConditionallyRegistrable`, `Shareable`, `CLICommand`.
- `inc/Contracts/Abstracts/`: `AbstractModule`, `AbstractPostType`, `AbstractTaxonomy`, `AbstractBlock`, `AbstractShortcode`, `AbstractRESTController`, `AbstractSettingsPage`, `AbstractAdminPage`, `AbstractUserRole`.
- `inc/Contracts/Abstracts/`: `AbstractModule`, `AbstractFeature`, `AbstractPostType`, `AbstractTaxonomy`, `AbstractBlock`, `AbstractShortcode`, `AbstractRESTController`, `AbstractSettingsPage`, `AbstractAdminPage`, `AbstractUserRole`.
- `inc/Contracts/Traits/`: `Loader`, `Singleton`.
- `inc/` root: `Container`, `AssetLoader`, `ComponentLoader`, `TemplateLoader`; `inc/Utils/`: utilities (e.g. `Encryptor`).

Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md — wp-framework

Tool-agnostic brief for AI coding agents (Claude Code, Copilot coding agent, Codex). `rtcamp/wp-framework`: shared base contracts (interfaces, abstracts, traits) and small utilities consumed via Composer by every rtCamp plugin/theme skeleton. **Zero runtime dependencies.** PHP 8.2+, WordPress 6.5+ — the floor is set by the Script Modules API (`wp_register_script_module()`, new in 6.5). The only API used above 6.5 is `wp_register_block_types_from_metadata_collection()` (6.8+), and `AssetLoader::register_block_manifest()` guards it with a per-block fallback for 6.5–6.7.
Tool-agnostic brief for AI coding agents (Claude Code, Copilot coding agent, Codex). `rtcamp/wp-framework`: shared base contracts (interfaces, abstracts, traits) and small utilities consumed via Composer by every rtCamp plugin/theme skeleton. **Zero Composer runtime dependencies.** PHP 8.2+, WordPress 6.5+ — the floor is set by the Script Modules API (`wp_register_script_module()`, new in 6.5). The only API used above 6.5 is `wp_register_block_types_from_metadata_collection()` (6.8+), and `AssetLoader::register_block_manifest()` guards it with a per-block fallback for 6.5–6.7. `Encryptor` requires the OpenSSL PHP extension when used.

## Authoritative rules

Expand All @@ -10,15 +10,15 @@ Tool-agnostic brief for AI coding agents (Claude Code, Copilot coding agent, Cod
## Key principles (full detail in the files above)

- **`inc/Contracts/` is public API.** Interfaces, abstracts, and their method signatures are consumed by every plugin/theme: a signature change breaks all of them. Treat such changes as breaking.
- **Zero runtime deps**: `composer.json` `require` holds only `php`; everything else is `require-dev`.
- **Zero Composer runtime deps**: `composer.json` `require` holds only `php`; everything else is `require-dev`.
- **TDD**: failing PHPUnit test first (`tests/` mirrors `inc/`), then code.
- **Tests run against real WordPress via wp-env** — no WP function mocking. `npm run wp-env start` then `npm run test:php` (a `pretest:php` hook runs `composer install` in the container first). WP-dependent tests extend `rtCamp\WPFramework\Tests\TestCase` (a `WP_UnitTestCase`); pure-logic tests can stay on `PHPUnit\Framework\TestCase`. CI runs a PHP × WP matrix (PHP 8.2+, WP 6.5+).
- `declare( strict_types = 1 );`, full types, `@package`/`@since`, `static::` not `self::`, PSR-4 (`rtCamp\WPFramework\` → `inc/`).
- **When you change a contract, update `ai/framework-php.instructions.md`**: the rules file shipped to consumers and synced into their `.github/` by `bin/sync-ai-instructions.js` (`npm run sync-ai`).

## Structure

`inc/Contracts/{Interfaces,Abstracts,Traits}/` (the consumed contract surface), `inc/` root (`Container`, `AssetLoader`, `ComponentLoader`), `inc/Utils/`. `ai/` holds the canonical consumer instruction doc; `bin/` holds the sync tool.
`inc/Contracts/{Interfaces,Abstracts,Traits}/` (the consumed contract surface), `inc/` root (`Container`, `AssetLoader`, `ComponentLoader`, `TemplateLoader`), `inc/Utils/`. `ai/` holds the canonical consumer instruction doc; `bin/` holds the sync tool.

## This repo also ships tooling for consumers

Expand Down
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Nothing yet.
### Documentation

- Added implementor getting-started and maintainer workflow guides; corrected
Singleton, REST-controller, compatibility, and wp-env test guidance; expanded
loader, cache, feature-selector, timer, and utility API coverage.
- Added `docs/upgrading.md` (versioning promise and the 1.0.0 → 1.0.1 `Singleton`
migration) and `docs/troubleshooting.md` (symptom → cause for the framework's
exceptions, `_doing_it_wrong()` notices, and silent no-ops).
- Documented the real install path: the package is not on public Packagist, so
the consumer needs a VCS `repositories` entry and a `^1.0` constraint.
- Added a worked WP-CLI example to `docs/contracts.md`, the only contract that
had none, and a quick-look snippet to the README.
- Corrected the `Loader::load()` snippet in `docs/architecture.md` to match the
implementation, and the `AbstractSettingsPage` capability note in
`docs/abstracts.md` (the `option_page_capability_*` filter is unconditional).

## [1.0.1] - 2026-07-29

Expand Down
30 changes: 20 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ treated as stable and changes to it are considered breaking.
## Development setup

```bash
# 1. Install PHP dev dependencies
# Host tooling (PHPCS and PHPStan).
composer install

# 2. Bring up WordPress for the integration tests (Docker required)
npm install
npm run wp-env start # starts @wordpress/env
# WordPress integration tests (Docker required).
npm ci
npm run wp-env start
```

## Before you open a PR

Run the full check suite locally — all of it must exit `0`:
Run all three checks locally — all of them must exit `0`:

```bash
composer check # PHPCS (lint) + PHPStan (analyse) + PHPUnit (test)
composer lint
composer analyse
npm run test:php
```

Individual steps:
Expand All @@ -39,17 +41,25 @@ Individual steps:
composer lint # PHPCS against WordPress Coding Standards
composer lint:fix # auto-fix fixable violations
composer analyse # PHPStan static analysis
composer test # PHPUnit
npm run test:php # PHPUnit in the wp-env test container
```

Tests run against real WordPress via `@wordpress/env`. Follow TDD: add a failing
test under `tests/` (which mirrors `inc/`) first, then the implementation.
Tests run against real WordPress via `@wordpress/env`. The `pretest:php` script
installs Composer dependencies inside the container before PHPUnit runs.
`composer test` is the lower-level host command and requires a separately
configured WordPress test suite and database; it is not the default local path.

Follow TDD: add a failing test under `tests/` (which mirrors `inc/`) first, then
the implementation. See [docs/maintainers.md](docs/maintainers.md) for test-case,
contract-change, and documentation guidance.

## Pull request checklist

- [ ] `composer check` passes (lint + analyse + test, all green).
- [ ] `composer lint`, `composer analyse`, and `npm run test:php` pass.
- [ ] New/changed behavior is covered by tests.
- [ ] Any change to `inc/Contracts/` is flagged as breaking in the PR description.
- [ ] Contract changes are reflected in `ai/framework-php.instructions.md`.
- [ ] User-visible behavior is reflected in `README.md` or `docs/`.
- [ ] A `CHANGELOG.md` entry is added under `## [Unreleased]`.
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/).

Expand Down
78 changes: 69 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,63 @@

`wp-framework` is a **library, not a plugin**. It ships contracts (interfaces,
abstracts, traits) plus concrete loaders and utilities; consuming plugins and
themes build their features on top. **Zero runtime dependencies. PHP 8.2+.**
themes build their features on top. It has **zero Composer runtime dependencies**.

Requirements:

- PHP 8.2+
- WordPress 6.5+
- Composer
- The OpenSSL PHP extension when using `Encryptor`
Comment on lines +24 to +27

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clear dependencies, we may want to amend these, but need to keep at the top.


## Install

Not on public Packagist — add the repository to the consuming project's
`composer.json`, then require it with a caret constraint:

```json
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/rtCamp/wp-framework" }
]
}
```

```bash
composer require rtcamp/wp-framework
composer require rtcamp/wp-framework:^1.0
```

(The `repositories` entry is unnecessary when the project already resolves this
package through an rtCamp-hosted Composer registry.)

PSR-4 autoloading: `rtCamp\WPFramework\` → `inc/`.

## Quick look

```php
use rtCamp\WPFramework\Contracts\Abstracts\AbstractPostType;
use rtCamp\WPFramework\Contracts\Traits\Loader;

final class ArticlePostType extends AbstractPostType {
public static function get_slug(): string { return 'article'; }
public function get_singular_label(): string { return __( 'Article', 'acme' ); }
public function get_plural_label(): string { return __( 'Articles', 'acme' ); }
public function get_menu_icon(): string { return 'dashicons-media-document'; }
}

final class Main {
use Loader;

public function boot(): void {
$this->load( [ ArticlePostType::class ] ); // instantiates + registers hooks
}
}
```

A registered, REST-enabled post type with no `register_post_type()` call and no
`init` hook written by hand. Full walkthrough in
[docs/getting-started.md](docs/getting-started.md).

## What's inside

- **Registration core** — the spine every consumer boots through:
Expand All @@ -48,6 +95,9 @@ PSR-4 autoloading: `rtCamp\WPFramework\` → `inc/`.
- `Cache` — typed wrapper over the WP object cache, group-namespaced, optional SWR
- `FeatureSelector` + `FeatureSelectorSettingsPage` — a fail-closed feature-flag
registry and its admin toggle page
- `Logger` — context-prefixed, `WP_DEBUG`-gated logging
- `Timer` — named request-scoped timers and laps
- `Transients` — prefix-namespaced transient storage

The contract surface (`inc/Contracts/`) is the public API: every interface,
abstract, and signature there is consumed by dependents, so changes to it are
Expand All @@ -67,23 +117,33 @@ Start with [docs/index.md](docs/index.md), then:

| Doc | What it covers |
|---|---|
| [getting-started.md](docs/getting-started.md) | Install, bootstrap a plugin or theme, load a module, and share a service. |
| [architecture.md](docs/architecture.md) | How a class becomes a live hook — the `Registrable` → `Loader` → `Container` flow. Read first. |
| [contracts.md](docs/contracts.md) | The interfaces and traits in detail. |
| [abstracts.md](docs/abstracts.md) | Cookbook for the `Abstract*` base classes. |
| [loaders.md](docs/loaders.md) | `AssetLoader`, `ComponentLoader`, `TemplateLoader` and the theme-override hierarchy. |
| [utilities.md](docs/utilities.md) | `Encryptor`, `Cache`, `FeatureSelector`, and `Container`. |
| [utilities.md](docs/utilities.md) | `Encryptor`, `Cache`, feature flags, logging, transients, timers, and `Container`. |
| [upgrading.md](docs/upgrading.md) | What changes between releases and what a consumer has to do about it. |
| [troubleshooting.md](docs/troubleshooting.md) | Symptom → cause for the errors and silent no-ops the framework emits. |
| [ai-review-system.md](docs/ai-review-system.md) | How the AI review instructions are authored here and synced into the skeletons. |
| [maintainers.md](docs/maintainers.md) | Development environment, tests, change checklist, and documentation maintenance. |

## Development

```bash
composer install # PHP dev dependencies
composer check # lint (PHPCS) + analyse (PHPStan) + test (PHPUnit)
composer install
npm ci
npm run wp-env start
composer lint
composer analyse
npm run test:php
```

Tests run against real WordPress via [`@wordpress/env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/)
(`npm install && npm run wp-env start`). TDD: a failing test first (`tests/` mirrors
`inc/`), then the code. Conventions live in [AGENTS.md](AGENTS.md), shared across
all contributors and AI tools.
Tests run against real WordPress via [`@wordpress/env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/).
Use `npm run test:php`, which runs PHPUnit inside the wp-env test container;
`composer test` only works directly when a host WordPress test suite has been
configured. See [docs/maintainers.md](docs/maintainers.md) for the complete
workflow.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion ai/framework-php.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Decision order for a new class, **do NOT default to Singleton**:
2. **`Registrable` + `Shareable`**: only if another class must retrieve it via `get_shared()`.
3. **`Singleton`**: only the `Main` bootstrap.

Extend the framework abstracts; never hand-roll their job: `AbstractModule` and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole}`.
Extend the framework abstracts; never hand-roll their job: `AbstractModule`, `AbstractFeature`, and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole}`.

Flag genuine contract/security violations, not style. Allow any correct implementation.

Expand Down
12 changes: 10 additions & 2 deletions docs/abstracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ Because it's a real `WP_REST_Controller`, all the core helper methods
(`get_items_permissions_check()`, schema helpers, …) are available to override.

> **Implementation note.** The base declares `register_routes()` as a concrete
> method that throws a "not implemented" `Exception` (the message is prefixed with
> the method name) rather than as `abstract`. The
> method that throws a `LogicException` naming the concrete class and method,
> rather than as `abstract`. The
> effect is "you must override it," but the failure surfaces at **runtime** (when
> `rest_api_init` fires), not at class-load time. Always provide your own
> `register_routes()`. (An `abstract` method would catch a missing override at
Expand Down Expand Up @@ -278,6 +278,14 @@ subclass — it re-declares the same menu seams). It registers on **three** hook
base loops it and calls `register_setting( $this->get_option_group(), … )` for
each. The option group defaults to the page slug.

`get_menu_slug()` also defaults to the page slug and can be overridden when the
menu slug depends on instance state. The base always filters
`option_page_capability_{group}` for this page's option group to return
`get_capability()`, so the menu, the render callback, and the `options.php` save
are authorized by the same capability. That matters when `get_capability()` is
lowered from its `manage_options` default: without the filter the page would
render for the lower capability but silently fail to save.

```php
final class SettingsPage extends AbstractSettingsPage {
public static function get_slug(): string { return 'my-plugin-settings'; }
Expand Down
19 changes: 15 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,23 @@ debuggable by reading `Loader::load()` top to bottom.
[`Loader::load()`](../inc/Contracts/Traits/Loader.php) is the heart of the
framework. Given `class-string[]`, for each class it:

1. **Instantiates** it with `new $class_name()` — every loadable class must be
1. **Skips a duplicate** when the same class name already appeared in this load.
2. **Instantiates** it with `new $class_name()` — every loadable class must be
constructible with no arguments (i.e. no *required* constructor parameters;
all-optional is fine).
2. **Registers hooks** if it is `Registrable` — but first, if it is also
3. **Registers hooks** if it is `Registrable` — but first, if it is also
`ConditionallyRegistrable`, it calls `can_register()` and skips registration
when that returns `false`.
3. **Caches the instance** if it is `Shareable`, storing it in a `Container`
4. **Caches the instance** if it is `Shareable`, storing it in a `Container`
keyed by class name.

```php
foreach ( $classes as $class_name ) {
if ( isset( $seen[ $class_name ] ) ) {
continue;
}
$seen[ $class_name ] = true;

$instance = new $class_name();

if ( $instance instanceof Registrable ) {
Expand All @@ -60,6 +66,9 @@ them, not to a global. Call `get_shared( $class_name )` on that same loader to
retrieve one; calling it before `load()` — or for a class that wasn't
`Shareable` — throws a `RuntimeException`.

Because another `load()` replaces the container, use one complete class list per
loader. A second call does not add to the previously shared set.

## Modules: loaders that hold loaders

Most skeletons don't hand the top-level loader a flat list of services. They hand
Expand Down Expand Up @@ -135,7 +144,9 @@ they are not the same thing:
default.
- **`Singleton` trait** — global `ClassName::get_instance()` access with cloning
and deserialization guarded. Use it only when something truly must be a process
global and you can't thread it through a loader.
global and you can't thread it through a loader. A class and its subclasses
share the trait's single storage slot, so do not resolve a child of a singleton
through `get_instance()`.

If you can pass the object in a constructor instead, do that. The
`ComponentLoader`/`TemplateLoader` pattern (a `Shareable` subclass fetched via
Expand Down
Loading