saij / wp-app-framework
Framework for standalone full site editing themes for WordPress.
Requires
- php: >=8.5
- twig/twig: ^3.30
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- php-stubs/wordpress-stubs: ^7.1
- phpstan/phpdoc-parser: ^2.3
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^13.4
- szepeviktor/phpstan-wordpress: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP/JS framework for building standalone full site editing themes, child themes and plugins for WordPress 7 (PHP 8.5).
- Composer package:
saij/wp-app-framework - PHP namespace:
Saij\WpAppFramework - JS import name:
@saij/wp-app-framework, runtime global:window.wpAppFramework[slug](one per framework instance)
The framework is a library that themes, child themes and plugins (its consumers) include via Composer. It is neither a theme nor a plugin and registers no blocks itself. Each consumer runs its own framework instance; blocks, REST routes and settings are named after the consumer's namespace (extra.wp-app-framework.namespace), technical names – script handles, JS global, cache directory, init action – after its slug, so a theme and several plugins with different framework versions run side by side.
Documentation
saijsaij.github.io/wp-app-framework – getting started, guides for every part of the API, build and release. It is published per minor version; next documents the unreleased state of main.
Status
In development (0.x): framework core, REST routes, WP-CLI commands, Twig, server-side blocks, block supports, settings, editor runtime, front-end scripts, release build, activation and uninstallation, plugins and child themes. Breaking changes are listed in Upgrading.
Source layout
src/ is organized by feature: each directory holds everything of one area – the attributes consumers use, the build-time collectors and the runtime classes.
| Directory | Contents |
|---|---|
Theme.php, ChildTheme.php, Plugin.php |
Boot and paths of the active theme, child theme or of a plugin |
Instance.php |
Boot of one framework instance (internal) |
Post/ |
#[PostType], #[PostMeta], #[Taxonomy], post objects (AbstractPost, Post, Page), core taxonomies (Category, Tag), PostFormat, collector (build), post store and registry, registration, rewrite rules and meta boxes (runtime), template function post() |
Query/ |
PostQuery (Event::query()), conditions (Terms, Any, All, FieldCondition), field classes in Field/ (also for table columns, with Nullable… variants), #[MetaFields] with its build check, the field class check and generator shared with tables, sort clauses for meta fields |
Table/ |
#[Table] and the column attributes, row objects (AbstractRow), RowQuery (LogEntry::query()), #[TableFields], collectors (build), table store and registry, schema sync and codec (runtime) |
Project/ |
Kind, slug, namespace and settings of a consumer from its composer.json (build) |
Api/ |
#[PublicApi], #[PluginApi], #[ThemeApi] attributes, collector and signature checks (build), hooks between instances and has_plugin() (runtime) |
Hook/ |
#[Hook] attribute, collector (build), definition, registrar (runtime) |
Rest/ |
#[RestController], #[RestRoute], #[Param] attributes, HttpMethod/Format enums, Permission/ (PublicAccess, Capability, Callback), RestException, collector (build), definition, registrar (runtime) |
WpCli/ |
#[CliCommand], #[CliSubcommand], #[Argument], #[Option] attributes, CliOutput, CliException, collector (build), definition, registrar of the WP-CLI commands (runtime) |
Twig/ |
#[TemplateFunction], #[TemplateFilter] attributes, collector (build), definition, TemplateRenderer and runtime loader (runtime), override finder (build) |
Block/ |
#[Block], #[Supports], #[PlainText], #[RichText], #[Image], #[Content], #[Label], #[Hidden] attributes, Labeled interface, BlockCategory/Align enums, collector (build), definition, registrar and template functions (runtime); Support/ (#[BlockSupport], #[UseSupport], SupportGroup, collector) |
Editor/ |
Editor template functions (field(), inner_blocks(), is_editor()), render context, batch render and preview routes and editor assets (runtime) |
View/ |
Loading of the front-end scripts (view scripts) of blocks and block supports and the marker of their root elements (runtime) |
Setting/ |
#[SettingsGroup] attribute, collector (build), settings store, template global and settings page (runtime) |
Style/ |
Loading of the consumer's built stylesheet (runtime) |
Cache/ |
#[Cache] attribute, tags (Tag/), variants (Vary/), collector (build), CacheStore with backends (Backend/), tag versions and their hooks (runtime) |
Cron/ |
#[CronEvent], #[CronTask] attributes, CronInterval/Weekday enums, collector (build), definitions, CronScheduler, registrar and start times (runtime) |
Lifecycle/ |
#[OnActivation], #[OnDeactivation], #[OnUninstall] attributes, collector and uninstall.php check (build), lifecycle registrar and uninstaller (runtime) |
Filesystem/ |
Atomic file writes, removal of directory trees (build and runtime) |
Asset/ |
Scripts next to classes (ScriptKind: editor and view scripts, build) and registration of scripts and styles from the consumer's build (runtime) |
Value/ |
#[ListOf], #[Url] and reference attributes, Link with LinkKind and link_attributes(), schema derivation of typed values (build), value factory (runtime) |
DependencyInjection/ |
#[NoService] attribute, service candidates and resolver (build), container (runtime) |
Manifest/ |
Manifest builder, generator and loader, checks shared by entry point collectors; Discovery/ (PSR-4 class discovery), DevCache/ (development mode cache) |
Console/ |
Command line entry point of bin/wp-app-framework (and bin/wp-app-framework-prefixed.php) |
Release/ |
Release build: project copy, Strauss settings, Twig patch, prefix check, archive (build only) |
New areas get their own directory following the same pattern. Tests in tests/Unit/ mirror this layout.
The editor runtime and its styles live in assets/ (editor.js entry, editor.scss styles of the inspector, settings.js and settings.scss of the settings page, meta-box.js and meta-box.scss of the meta boxes, _controls.scss styles of the controls they share, canvas.scss styles of the editor canvas, runtime/ modules, tests in runtime/test/; view.js entry of the view runtime with its modules and tests in view/); webpack.config.js is the helper for the consumer's build (@saij/wp-app-framework/webpack), tested in assets/test/. tools/ holds the generators of the API reference (run by the documentation build). types/ declares the JS API of both imports for IDEs, checked by tsc over types/check/ (npm run lint:types); assets/test/public-api.test.js checks the runtime objects against the declarations. The documentation website lives in docs/ (see Development).
Conventions
- The theme API must be fully resolvable by IDEs: enums and classes instead of magic strings, first-class callables instead of method names, required information as required attribute arguments.
- Class properties are never public; access goes through getters with a
getprefix (is/hasfor booleans). Attribute classes such as#[Hook]and value classes (block attribute classes) are the exceptions and expose public readonly properties; test doubles of WordPress classes (tests/Stubs/) mirror the public properties of WordPress. - Code, comments and documentation are in English.
Development
Themes and plugins require this repository by version (see Releases and CI). During development, wp-dev-env replaces the installed package by a symlink to this checkout (.env: WP_PACKAGE_LINKS=saij/wp-app-framework=wp-app-framework), so changes take effect immediately without a release build; lock file and composer.json of the consumer stay unchanged. The consumer checkouts and this checkout share the same parent directory; see theme-dev-test and plugin-dev-test, the test theme and test plugin of the framework, for examples. To test a release with unreleased framework changes: composer release -- --package-source=saij/wp-app-framework=../wp-app-framework in the consumer.
A local WordPress for development is provided by wp-dev-env; tests and the checks below run in wp-test-env (fresh WordPress instances, the tools image with PHP, Composer and Node), checked out next to this repository.
All checks run in the tools image of wp-test-env (../wp-test-env/bin/tools -w wp-app-framework …) and must pass without errors or warnings.
PHP: unit tests (PHPUnit), static analysis (PHPStan, level max, with the WordPress stubs of szepeviktor/phpstan-wordpress) and code style (PHP-CS-Fixer, PER Coding Style). WordPress is not loaded in unit tests; minimal test doubles of the WordPress classes and functions with the signatures of WordPress live in tests/Stubs/ and are loaded by the PHPUnit bootstrap only, so the analysis checks the tests against the real signatures. The WP-CLI double (tests/Stubs/wp-cli.php, signatures of WP-CLI 2.12.0) is also the signature file of the analysis, because php-stubs/wp-cli-stubs cannot be installed together with the WordPress 7 stubs. Fixtures that are wrong on purpose (build errors under test) carry @phpstan-ignore with the reason.
composer install composer test composer lint # PHPStan and PHP-CS-Fixer composer fix # applies the code style
JavaScript and styles of the runtime: unit tests (Vitest), ESLint and Stylelint (configurations of @wordpress/scripts):
npm ci npm test npm run lint # lint:js and lint:style npm run format
package-lock.json pins these development tools. Themes do not use it: npm ignores the lock files of dependencies, so the theme's own lock file decides which versions it builds with.
Everything that needs WordPress, a browser or a complete consumer build – the successful manifest build, REST routes, Twig in WordPress, blocks on the front end and in the editor – is tested in theme-dev-test; plugins and their interplay with the theme in plugin-dev-test.
Documentation
The documentation website lives in docs/ (Rspress; pages in docs/content/, sidebar in docs/rspress.config.mjs). It is not part of the package (export-ignore). Every change of the consumer API or of observable behavior updates its pages in the same change.
npm --prefix docs ci npm --prefix docs run build # docs/build/, fails on broken links and anchors npm --prefix docs run dev # local preview with reload
GitHub Actions publish it to the gh-pages branch: main as next/, each release tag as its minor version (docs/scripts/publish.mjs).
License
GPL-2.0-or-later, like WordPress.