A local WordPress runtime for developing themeum/framework. It runs a minimal sample plugin (slug framework) that consumes the library through a Composer path repository with symlink: true and then scopes it with PHP-Scoper (prefix Themeum). The plugin authors against the prefixed namespace Themeum\Framework\, exactly as a shipped plugin would.
All Docker tooling lives at the repository root (../docker-compose.yml, ../Makefile, ../docker/). This directory is the sample plugin itself.
- Docker (Docker Desktop or OrbStack)
- A free port block (defaults:
20200,20201,20202)
- The application boots on the WordPress
inithook viabootstrap/app.php. - The options prefix is applied (
use_prefix('framework')). - Hooks are wired from
config/hooks.phpand service providers frombootstrap/providers.php. - A REST route is registered:
GET /wp-json/framework/v1/ping. - The
is_dev_mode()macro resolves fromWP_DEBUG.
Run all commands from the repository root (..), where the Makefile lives.
cp .env.example .env # adjust ports if needed
make up # start nginx, php, mariadb, phpmyadmin
make example-install # example/composer.json — plugin + php-scoper deps (in container)
make scope # generate the prefixed Themeum\Framework\ tree
make init # one-shot: download + install WP, activate the pluginThen open:
- Site: http://localhost:20200
- Admin: http://localhost:20200/wp-admin (user
admin, passworddemo) - phpMyAdmin: http://localhost:20202
- REST check:
curl http://localhost:20200/wp-json/framework/v1/ping
Expected response:
{ "status": "ok", "dev_mode": true, "prefix": "framework" }Always run
make example-install(not host Composer) for the example plugin. The path repository URL (/var/www/html/framework-library) is a container path; the symlink only resolves inside Docker. Usemake library-installfor the repo-rootcomposer.json(PHPUnit, PHPCS, etc.).
make scopeis required before first activation. The plugin authors against the prefixedThemeum\Framework\namespace, which only exists after php-scoper generateslibraries/themeum/framework/. Activating the plugin before scoping will fatal on the missing scoped tree.
The library is symlinked into vendor/themeum/framework (unscoped Framework\), but the plugin runs against the scoped copy in libraries/themeum/framework/ (Themeum\Framework\). So after editing any file under ../src/, regenerate the scoped tree:
make scopeThis re-runs php-scoper and refreshes libraries/themeum/framework/. No container restart is needed; the next request picks up the change. Re-run make example-install only when you change the example plugin's own composer.json.
make wp-cli CMD="plugin list"
make wp-cli CMD="option get siteurl"The framework registers its own WP-CLI commands (migrate, make:model, make:controller, make:migration, db:seed, migrate:fresh) once the application boots under WP-CLI.
Xdebug is installed but disabled by default. To enable step debugging:
- Set
XDEBUG_MODE=debugin.env. docker compose restart php.- Start Listen for Xdebug in the IDE (port
9003, keyPHPSTORM). - Trigger a session explicitly:
- Web: use an Xdebug browser extension, or append
?XDEBUG_TRIGGER=PHPSTORMto the URL. - WP-CLI:
make xdebug-wp CMD="kirki migrate"(orXDEBUG_TRIGGER=PHPSTORM make wp CMD="...").
- Web: use an Xdebug browser extension, or append
If the IDE pauses on
Symfony\Component\Console\Exception\RuntimeException(e.g.The "--once" option does not exist), that is Symfony Console probing argv during WP-CLI/Composer bootstrap—not your app failing. Press Continue (F5), or rely on theignoreExceptionsentries in.vscode/launch.json.
The stack uses PHP 8.2 (Kirki Ecommerce uses 7.4). The framework declares >=7.0, so this is intentionally a wider compatibility surface: PHP 8.x deprecations in src/ will surface as notices with WP_DEBUG=true.
This playground runs the library exactly as a shipped plugin would: prefixed with PHP-Scoper. The scoper.config.php mirrors Kirki's setup: prefix Themeum, finder vendor/themeum/framework/src, output libraries/themeum/framework/.
- The path repository symlinks the unscoped library into
vendor/themeum/framework(this is only php-scoper's input). make scoperewrites everyFramework\namespace toThemeum\Framework\(andFramework\app()toThemeum\Framework\app()) intolibraries/themeum/framework/.scoper.config.phpuses a negative-lookahead inexclude-namespacesso onlyFramework\is prefixed.example/composer.jsonautoloads only the scoped copy:
The plugin code references the prefixed namespace throughout: Themeum\Framework\Application, Themeum\Framework\Route, \Themeum\Framework\response(), and the is_dev_mode macro is registered on Themeum\Framework\Application. The unscoped vendor/themeum/framework is never autoloaded alongside the scoped copy.
make scope ensures a placeholder libraries/themeum/framework/helpers.php exists before running php-scoper (so the project autoloader, which references it, can load), then runs php-scoper and composer dump-autoload. libraries/ is gitignored and regenerated on every CI/release run (see .github/workflows/smoke-test.yml).
| Committed | Generated (gitignored) |
|---|---|
framework.php, bootstrap/, config/, routes/, src/ |
vendor/ |
composer.json, scoper.config.php |
composer.lock |
Docker files, Makefile, .env.example |
libraries/ (scoped tree) |
.env, WordPress core, wordpress_data volume |
| Symptom | Likely cause / fix |
|---|---|
Class Themeum\Framework\... not found |
make scope was not run, so libraries/themeum/framework/ is missing. Run make example-install then make scope. |
Failed opening required '.../libraries/themeum/framework/helpers.php' on activation |
The scoped tree has not been generated yet. Run make scope before activating the plugin. |
vendor/themeum/framework is empty or not a symlink |
Composer ran outside Docker; the path URL only resolves in the container. Remove example/vendor and re-run make example-install. |
port is already allocated |
Another stack uses the 20200 block. Change NGINX_HTTP_PORT / MARIADB_PORT / PHPMYADMIN_PORT in .env. |
make init hangs on "Database not ready" |
MariaDB still starting; the script retries automatically. If it persists, check docker compose logs mariadb. |
| REST route returns 404 | Permalinks not flushed. Re-run make init, or make wp-cli CMD="rewrite flush". |
Library edits under ../src/ not reflected |
The plugin runs the scoped copy. Re-run make scope after editing the library. |
Cannot redeclare class Themeum\Framework\... |
Both unscoped vendor/themeum/framework and the scoped libraries/ tree are autoloaded. Only the scoped tree should be in autoload. |
| Xdebug not connecting | Confirm XDEBUG_MODE=debug in .env and docker compose restart php; IDE must listen on 9003. Use ?XDEBUG_TRIGGER=PHPSTORM (web) or make xdebug-wp (CLI). |
IDE stops on Symfony --once / RuntimeException |
Harmless Symfony Console noise while debugging CLI; press Continue, or use Listen for Xdebug (has ignoreExceptions configured). |