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 installThe 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.18Add 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-autoloadRe-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. - 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_| Argument | Description |
|---|---|
name (positional) |
Migration name; should start with create_ and end with table |
--prefix |
Optional table prefix |
Run all pending migrations.
wp kirki migrateDrop all plugin tables 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 UserCreate a controller in app/Http/Controllers/.
wp kirki make:controller UserControllerSupports optional flags for API or resource controllers (see wp help kirki make:controller).
Create a form request class.
wp kirki make:request StoreUserRequestCreate a service provider class.
wp kirki make:provider AppServiceProviderCreate a seeder in database/seeders/.
wp kirki make:seeder DatabaseSeederCreate a generic class under app/.
wp kirki make:class ExampleServiceSupports an optional folder argument (see wp help kirki make:class).
GPL-2.0-or-later