An elegant PHP framework for WordPress plugins. This guide walks through integrating themeum/framework into a plugin named Kirki.
Contributors working on the library itself can use the local playground in example/README.md.
Maintainers can cut a release with a single command that bumps composer.json, commits, tags, and pushes.
Prerequisites: check out main, ensure the working tree is clean, and fix any failing unit tests before releasing.
composer release -- -v 1.0.6 -m "Fix validation handling"The -- separator is required so Composer forwards -v and -m to the release script instead of treating them as Composer flags.
If -v or -m is omitted, the script prompts for the version and release message interactively:
composer release --
Preview the release steps without making changes:
composer release -- --dry-run -v 1.0.6 -m "Fix validation handling"The script will:
- Validate the version (semver
x.y.z, greater than the currentcomposer.jsonversion) - Confirm you are on
mainwith a clean working tree and no existing tag locally or onorigin - Run
composer test:unit - Update
composer.json, commit with the release message, create an annotated tag (v1.0.6), and push the commit and tag toorigin
Run composer release -- --help for full flag reference.
In your plugin’s composer.json, add a VCS repository and require dev-main:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/themeum/framework"
}
],
"require": {
"php": ">=7.0",
"themeum/framework": "dev-main"
},
"minimum-stability": "dev",
"prefer-stable": true
}From the plugin root:
composer install
The unscoped package lands in vendor/themeum/framework. That copy is input for PHP-Scoper only — do not autoload it in production.
composer require --dev humbug/php-scoper:^0.18.18
Add a scope script to composer.json:
"scripts": { "scope": [ "@php vendor/bin/php-scoper add-prefix --config=scoper.config.php --force" ] }
Create scoper.config.php at the plugin root:
<?php declare(strict_types=1); use Symfony\Component\Finder\Finder; return [ 'prefix' => 'Kirki', 'output-dir' => 'libraries/kirki/framework', 'finders' => [ Finder::create() ->files() ->in(__DIR__ . '/vendor/themeum/framework/src') ->exclude(['test', 'tests', 'Tests']) ->name(['*.php', '*.stub']), ], 'exclude-files' => [], 'exclude-namespaces' => [ '~^$~', '/^(?!Framework($|\\\\))/', ], 'expose-global-classes' => true, 'expose-global-functions' => true, 'expose-global-constants' => true, ];
PHP-Scoper rewrites Framework\ to Kirki\Framework\ and writes the result to libraries/kirki/framework/. The exclude-namespaces regex keeps every namespace except Framework\ unprefixed (Composer vendor code stays on its original PSR-4 paths).
WP-CLI generator stubs live under src/Console/stubs/ as *.stub files. Include them in the finder (->name(['*.php', '*.stub'])) so they are copied into the scoped tree. Stub templates use placeholders such as {{namespace}}, which PHP-Scoper cannot parse; after scoping, run scripts/prefix-scoped-console-stubs.php on libraries/<vendor>/framework/Console/stubs with your prefix (the example plugin does this via scripts/post-scope-console-stubs.php in its scope Composer script).
"autoload": { "psr-4": { "Kirki\\App\\": "app/", "Kirki\\Framework\\": "libraries/kirki/framework/" }, "files": [ "libraries/kirki/framework/helpers.php" ] }
Never register PSR-4 autoload for vendor/themeum/framework alongside the scoped tree.
composer install in the plugin also installs every package themeum/framework requires into the plugin’s vendor/. Those stay unprefixed at runtime; only the scoped tree under libraries/ uses your prefix.
Before the first run, create a placeholder so Composer can load helpers.php:
mkdir -p libraries/kirki/framework printf '<?php\n' > libraries/kirki/framework/helpers.php composer run scope composer dump-autoload
Re-run composer run scope after updating themeum/framework or when library source changes. Add libraries/ to .gitignore and generate the scoped tree in CI or before release.
Do not activate the plugin until scoping completes — missing Kirki\Framework\ classes will fatal.
After installation, create this layout under the plugin root (empty directories are fine until generators fill them):
kirki/
├── kirki.php
├── composer.json
├── scoper.config.php
├── bootstrap/
│ ├── app.php
│ └── providers.php
├── app/ # Kirki\App\
├── config/
│ └── hooks.php
├── routes/
│ └── api.php
├── database/
│ ├── migrations/
│ └── seeders/
├── resources/ # optional
├── libraries/kirki/framework/ # generated — do not commit
└── vendor/
The application resolves these paths by default: app/, bootstrap/, config/, database/, resources/ relative to the plugin base path. Override with use_app_path(), use_config_path(), use_database_path(), use_bootstrap_path(), or use_resource_path() on the application instance if needed.
<?php if (!defined('ABSPATH')) { exit; } if (!defined('KIRKI_PATH')) { define('KIRKI_PATH', plugin_dir_path(__FILE__)); } if (!defined('KIRKI_URL')) { define('KIRKI_URL', plugin_dir_url(__FILE__)); } if (!defined('KIRKI_PREFIX')) { define('KIRKI_PREFIX', 'kirki'); } require_once __DIR__ . '/vendor/autoload.php'; add_action('init', 'kirki_boot_application', 0); function kirki_boot_application() { require_once KIRKI_PATH . 'bootstrap/app.php'; }
Configure and boot the application on the init hook so WordPress is fully loaded.
<?php use Kirki\Framework\Application; return Application::configure(KIRKI_PATH) ->use_routing(KIRKI_PATH . 'routes/api.php') ->use_prefix(KIRKI_PREFIX) ->use_app_mode('development') ->boot();
use_prefix() sets the options key prefix (snake_cased). Access it with app()->prefix().
Return a list of provider classes:
<?php use Kirki\App\Providers\AppServiceProvider; return [ AppServiceProvider::class, ];
Place providers under Kirki\App\Providers\.
Register hook handler classes:
<?php use Kirki\Framework\Wordpress\Hooks\Actions\SampleActionHook; use Kirki\Framework\Wordpress\Hooks\Filters\SampleFilterHook; return [ 'actions' => [ SampleActionHook::class, ], 'filters' => [ SampleFilterHook::class, ], ];
<?php use Kirki\Framework\Http\Request; use Kirki\Framework\Route; use function Kirki\Framework\app; use function Kirki\Framework\response; Route::set_namespace('kirki/v1'); Route::get('/ping', function (Request $request) { return response()->json([ 'status' => 'ok', 'dev_mode' => app()->is_dev_mode(), 'prefix' => app()->prefix(), ]); });
- Migrations — PHP files in
database/migrations/. Run withwp kirki migrate. See docs/migrations.md. - Validation — Rules on form requests and validators provided by the framework.
- Container and facades — Resolve services via
app()and framework facades after boot.
Test documentation will be added in a future release.
Commands register when the application boots under WP-CLI. The command namespace is kirki:
wp kirki <command>
Create a migration file in database/migrations/.
wp kirki make:migration create_users_table wp kirki make:migration create_orders_table --prefix=wp_ wp kirki make:migration add_slug_to_posts --table=posts
The generated class is appended to config/migrations.php automatically.
| Argument | Description |
|---|---|
name (positional) |
Migration name; should start with create_ and end with table |
--prefix |
Optional table prefix |
--table |
Existing table to alter; generates an alter migration |
--create |
Table to create; generates a create migration |
Run all pending migrations. Migrations that have already run are skipped.
wp kirki migrate
Show which migrations have run, and the batch each belongs to.
wp kirki migrate:status
Undo the migrations of the most recent batch.
wp kirki migrate:rollback wp kirki migrate:rollback --step=2
| Flag / option | Description |
|---|---|
--step |
Number of batches to roll back (default 1) |
Drop all plugin tables, clear migration history, and re-run migrations.
wp kirki migrate:fresh wp kirki migrate:fresh --seed wp kirki migrate:fresh --seed --class=DatabaseSeeder
| Flag / option | Description |
|---|---|
--seed |
Run seeders after migrating |
--class |
Seeder class name when seeding |
Run database seeders from database/seeders/.
wp kirki db:seed wp kirki db:seed --class=UsersSeeder wp kirki db:seed --class=UsersSeeder,ProductsSeeder
| Option | Description |
|---|---|
--class |
One or more seeder classes (comma-separated). Omit to discover all seeders. |
Create a model class in app/Models/.
wp kirki make:model User
Create a controller in app/Http/Controllers/.
wp kirki make:controller UserController
Supports optional flags for API or resource controllers (see wp help kirki make:controller).
Create a form request class.
wp kirki make:request StoreUserRequest
Create a service provider class.
wp kirki make:provider AppServiceProvider
Create a seeder in database/seeders/.
wp kirki make:seeder DatabaseSeeder
Create a generic class under app/.
wp kirki make:class ExampleService
Supports an optional folder argument (see wp help kirki make:class).
GPL-2.0-or-later