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
6 changes: 3 additions & 3 deletions .github/instructions/php.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ 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`, `AbstractFeature`, `AbstractAbility`, `AbstractAbilityRegistrar`.
- `inc/Contracts/Abstracts/`: `AbstractModule`, `AbstractPostType`, `AbstractTaxonomy`, `AbstractBlock`, `AbstractShortcode`, `AbstractRESTController`, `AbstractSettingsPage`, `AbstractAdminPage`, `AbstractUserRole`, `AbstractFeature`, `AbstractAbility`, `AbstractAbilityRegistrar`, `AbstractJob`, `AbstractPlatformLog`, `AbstractPlatformProfile`, `AbstractLocalEnvironment`, `AbstractVulnerabilityProvider`.
- `inc/Contracts/Traits/`: `Loader`, `Singleton`.
- `inc/` root: `Container`, `AssetLoader`, `ComponentLoader`, `TemplateLoader`; `inc/Utils/`: utilities (e.g. `Encryptor`).

Everything under `inc/Contracts/` is a **consumed contract**. New abstracts/interfaces must follow the existing shape (e.g. an `Abstract*` `implements Registrable` and exposes `abstract` methods for the bits that vary).
Everything under `inc/Contracts/` is a **consumed contract**. Most new abstracts follow the `Registrable` shape (`implements Registrable`, `abstract` methods for the bits that vary). The exception is a plain describable/queried-on-demand object with no WordPress hook of its own — the platform-extensibility abstracts (`AbstractPlatformLog`, `AbstractPlatformProfile`, `AbstractLocalEnvironment`, `AbstractVulnerabilityProvider`) are this shape on purpose.

## Mandatory

Expand All @@ -28,4 +28,4 @@ Everything under `inc/Contracts/` is a **consumed contract**. New abstracts/inte
3. 🚩 A new dependency added to `composer.json` `require` (must stay `php`-only; dev tools go in `require-dev`).
4. 🚩 Missing `strict_types`/types/docblocks; PSR-4 mismatch; `self::` where `static::` is required.
5. 🚩 Missing escape/sanitize where the utility touches WordPress output/input; raw `$wpdb` without `prepare()`.
6. 🚩 New abstract/interface that doesn't follow the existing contract shape (e.g. an `Abstract*` not implementing `Registrable`, or duplicating a capability the `Loader`/`Container` already provides).
6. 🚩 New abstract/interface that doesn't follow the existing contract shape, or duplicates a capability the `Loader`/`Container` already provides. Not implementing `Registrable` is fine for a plain describable/queried-on-demand object (see the platform-extensibility precedent above) — only flag it if the class also fires WordPress hooks itself without going through `Registrable`.
70 changes: 70 additions & 0 deletions .stubs/action-scheduler.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* Analysis-only declarations for Action Scheduler (https://actionscheduler.org/).
*
* Not a dependency of this package — see {@see \rtCamp\WPFramework\Contracts\Abstracts\AbstractJob},
* which guards every call behind `is_available()`. Referenced via PHPStan's
* `scanFiles` so those calls resolve regardless of whether a consumer has the
* library installed. Never autoloaded or executed.
*
* @package rtCamp\WPFramework
*/

class ActionScheduler {
/**
* @param string|null $function_name Calling function name, for the developer notice.
*/
public static function is_initialized( ?string $function_name = null ): bool {}
}

/**
* @param string $hook The hook to trigger.
* @param array<mixed> $args Arguments to pass when the hook triggers.
* @param string $group The group to assign this job to.
* @param bool $unique Whether the action should be unique.
* @param int $priority Lower values take precedence.
*
* @return int The action ID. Zero on error.
*/
function as_enqueue_async_action( string $hook, array $args = [], string $group = '', bool $unique = false, int $priority = 10 ): int {}

/**
* @param int $timestamp When the job will run.
* @param string $hook The hook to trigger.
* @param array<mixed> $args Arguments to pass when the hook triggers.
* @param string $group The group to assign this job to.
* @param bool $unique Whether the action should be unique.
* @param int $priority Lower values take precedence.
*
* @return int The action ID. Zero on error.
*/
function as_schedule_single_action( int $timestamp, string $hook, array $args = [], string $group = '', bool $unique = false, int $priority = 10 ): int {}

/**
* @param int $timestamp When the first instance will run.
* @param int $interval_in_seconds How long to wait between runs.
* @param string $hook The hook to trigger.
* @param array<mixed> $args Arguments to pass when the hook triggers.
* @param string $group The group to assign this job to.
* @param bool $unique Whether the action should be unique.
* @param int $priority Lower values take precedence.
*
* @return int The action ID. Zero on error.
*/
function as_schedule_recurring_action( int $timestamp, int $interval_in_seconds, string $hook, array $args = [], string $group = '', bool $unique = false, int $priority = 10 ): int {}

/**
* @param string $hook Name of the hook to search for.
* @param array<mixed>|null $args Arguments to match.
* @param string $group Group of the action to match.
*/
function as_has_scheduled_action( string $hook, ?array $args = null, string $group = '' ): bool {}

/**
* @param string $hook The hook that the job will trigger.
* @param array<mixed> $args Args that would have been passed to the job.
* @param string $group The group the job is assigned to.
*
* @return int|null The scheduled action ID if found, or null.
*/
function as_unschedule_action( string $hook, array $args = [], string $group = '' ): ?int {}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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 runtime deps**: `composer.json` `require` holds only `php`; everything else is `require-dev`. An optional third-party library is a guarded seam, never a dependency — `AbstractJob` reaches Action Scheduler only behind `is_available()`, with a `.stubs/` entry for PHPStan and fakes for tests.
- **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/`).
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ PSR-4 autoloading: `rtCamp\WPFramework\` → `inc/`.
interfaces
- the `Loader` trait (instantiate a list of classes, register their hooks,
cache the shared ones) and the `Container` it stores instances in
- **Twelve `Abstract*` base classes** — one per WordPress registration chore, so
a consumer writes intent instead of boilerplate: `AbstractModule`,
- **Seventeen `Abstract*` base classes** — one per WordPress registration chore,
so a consumer writes intent instead of boilerplate: `AbstractModule`,
`AbstractPostType`, `AbstractTaxonomy`, `AbstractBlock`, `AbstractShortcode`,
`AbstractRESTController`, `AbstractSettingsPage`, `AbstractAdminPage`,
`AbstractUserRole`, `AbstractFeature`, `AbstractAbility`,
`AbstractAbilityRegistrar`
`AbstractAbilityRegistrar`, `AbstractJob`, `AbstractPlatformLog`,
`AbstractPlatformProfile`, `AbstractLocalEnvironment`,
`AbstractVulnerabilityProvider`
- **Asset & render loaders** — `AssetLoader` (scripts/styles/modules +
`*.asset.php` manifests), `ComponentLoader` and `TemplateLoader` (resolve
components/templates across the child-theme → parent-theme → package hierarchy)
Expand Down
4 changes: 2 additions & 2 deletions 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,Feature,Ability,AbilityRegistrar}`.
Extend the framework abstracts; never hand-roll their job: `AbstractModule` and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole,Feature,Ability,AbilityRegistrar,Job,PlatformLog,PlatformProfile,LocalEnvironment,VulnerabilityProvider}`.

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

Expand Down Expand Up @@ -48,7 +48,7 @@ Flag genuine contract/security violations, not style. Allow any correct implemen
2. 🚩 `Singleton`/`::get_instance()` outside `Main` → `Loader`+`Registrable` (or `Shareable`+`get_shared()` if retrieval is genuinely needed). Never a service locator.
3. 🚩 `Shareable` with no real later-retrieval need → plain `Registrable`.
4. 🚩 WP-hooking class not implementing `Registrable` / not loaded via the `Loader`.
5. 🚩 A class calling `register_post_type`/`register_taxonomy`/`register_rest_route`/`add_menu_page`/`add_shortcode`/`register_block_type`/`wp_register_ability` directly instead of extending the matching `Abstract*` (`AbstractPostType`, `AbstractRESTController`, `AbstractAdminPage`, …). Name the abstract to extend.
5. 🚩 A class calling `register_post_type`/`register_taxonomy`/`register_rest_route`/`add_menu_page`/`add_shortcode`/`register_block_type`/`wp_register_ability`/`as_schedule_single_action`/`as_schedule_recurring_action`/`as_enqueue_async_action` directly instead of extending the matching `Abstract*` (`AbstractPostType`, `AbstractRESTController`, `AbstractAdminPage`, `AbstractJob`, …). Name the abstract to extend.
6. 🚩 Missing `strict_types`/types/docblocks; PSR-4 mismatch; `self::` for LSB.
7. 🚩 Missing escape/sanitize/nonce/capability; raw `$wpdb` without `prepare()`; REST without a real `permission_callback`; inline assets; wrong/absent text domain.
8. 🚩 Edit under `vendor/rtcamp/wp-framework` or WordPress core.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
"autoload-dev": {
"psr-4": {
"rtCamp\\WPFramework\\Tests\\": "tests/"
}
},
"exclude-from-classmap": [
"tests/Fixtures/ActionSchedulerFakes.php",
"tests/Fixtures/LoaderFixtures.php"
]
},
"config": {
"allow-plugins": {
Expand Down
Loading
Loading