The Pollora framework provides a declarative way to create WordPress CLI commands using PHP attributes. Commands are automatically discovered and registered with intelligent slug auto-generation.
- Quick Start
- Single Commands
- Command Suites (Subcommands)
- Automatic Slug Generation
- WP-CLI Attributes
- Command Generator
- Examples
- Best Practices
<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use WP_CLI;
#[WpCli]
/**
* A simple hello world command
*/
class HelloWorldCommand
{
public function __invoke(array $arguments, array $options): void
{
WP_CLI::success('Hello, World!');
}
}wp hello-worldThat's it! The command slug is auto-generated from the class name.
For commands without subcommands, use the __invoke() method:
<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use WP_CLI;
#[WpCli]
/**
* Create a new user
*
* ## OPTIONS
*
* <username>
* : The username for the new user
*
* [--role=<role>]
* : The user role
* ---
* default: subscriber
* ---
*
* ## EXAMPLES
*
* wp user-create john --role=editor
*/
class UserCreateCommand
{
public function __invoke(array $arguments, array $options): void
{
$username = $arguments[0] ?? null;
$role = $options['role'] ?? 'subscriber';
if (!$username) {
WP_CLI::error('Username is required.');
return;
}
WP_CLI::success("User '{$username}' created with role '{$role}'");
}
}WP-CLI automatically registers all public methods as subcommands, under the method name as written: listUsers() answers to listUsers. For a kebab-case name, put #[Command] on the method: with no argument it derives list-users from the method name (see Custom Subcommand Names).
<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use WP_CLI;
#[WpCli]
/**
* User management commands
*/
class UserManagementCommand
{
/**
* Create a new user
*/
public function create(array $arguments, array $options): void
{
WP_CLI::success('User created');
}
/**
* Delete a user
*/
public function delete(array $arguments, array $options): void
{
WP_CLI::success('User deleted');
}
/**
* List all users
*/
public function listUsers(array $arguments, array $options): void
{
WP_CLI::success('Listing users...');
}
}wp user-management create
wp user-management delete
wp user-management listUsersUse #[Command] only when you need a custom slug different from the method name:
<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use Pollora\Attributes\WpCli\Command;
use WP_CLI;
#[WpCli]
/**
* User management commands
*/
class UserManagementCommand
{
#[Command('new')] // Custom slug instead of "createUser"
/**
* Create a new user
*/
public function createUser(array $arguments, array $options): void
{
WP_CLI::success('User created');
}
#[Command('rm')] // Custom slug instead of "deleteUser"
/**
* Delete a user
*/
public function deleteUser(array $arguments, array $options): void
{
WP_CLI::success('User deleted');
}
// No #[Command] needed - the slug is the method name, "list"
public function list(array $arguments, array $options): void
{
WP_CLI::success('Listing users...');
}
}wp user-management new # Uses custom slug
wp user-management rm # Uses custom slug
wp user-management list # Uses the method nameThe "Command" suffix is automatically removed:
| Class Name | Generated Slug |
|---|---|
UserManagementCommand |
user-management |
DatabaseCleanupCommand |
database-cleanup |
CacheHelper |
cache-helper |
Without #[Command], WP-CLI uses the method name as written. Only a bare #[Command] (no argument) derives a kebab-case slug from it.
| Method Name | Without #[Command] |
With bare #[Command] |
|---|---|---|
createUser() |
createUser |
create-user |
deleteById() |
deleteById |
delete-by-id |
list() |
list |
list |
Override auto-generation when needed:
#[WpCli('cache')] // Custom command slug
class CacheManagementCommand
{
#[Command('flush')] // Custom subcommand slug
public function clearAllCaches(): void { }
public function warmup(): void { } // Method name: "warmup"
}wp cache flush # Custom slug
wp cache warmup # Method nameDefine command arguments and options:
use Pollora\Attributes\WpCli\Synopsis;
#[WpCli]
#[Synopsis('<name> [--greeting=<greeting>] [--uppercase]')]
class GreetCommand
{
public function __invoke(array $arguments, array $options): void
{
$name = $arguments[0];
$greeting = $options['greeting'] ?? 'Hello';
WP_CLI::success("{$greeting}, {$name}!");
}
}Control when the command is available:
use Pollora\Attributes\WpCli\When;
#[WpCli]
#[When('after_wp_load')] // Execute after WordPress loads (most common)
class MyCommand { }
#[WpCli]
#[When('before_wp_load')] // Execute before WordPress loads
class EarlyCommand { }Execute callbacks before/after the command:
use Pollora\Attributes\WpCli\BeforeInvoke;
use Pollora\Attributes\WpCli\AfterInvoke;
#[WpCli]
#[BeforeInvoke([self::class, 'setup'])]
#[AfterInvoke([self::class, 'cleanup'])]
class MyCommand
{
public function __invoke(array $arguments, array $options): void
{
WP_CLI::success('Command executed');
}
public static function setup(): void
{
WP_CLI::debug('Setting up...');
}
public static function cleanup(): void
{
WP_CLI::debug('Cleaning up...');
}
}Control registration timing:
use Pollora\Attributes\WpCli\IsDeferred;
#[WpCli]
#[IsDeferred(false)] // Register immediately (default is true)
class MyCommand { }<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use Pollora\Attributes\WpCli\Synopsis;
use Pollora\Attributes\WpCli\When;
use Pollora\Attributes\WpCli\BeforeInvoke;
use WP_CLI;
#[WpCli('greet')]
#[When('after_wp_load')]
#[Synopsis('<name> [--greeting=<greeting>] [--format=<format>]')]
#[BeforeInvoke([self::class, 'validate'])]
/**
* Greet someone with style
*
* ## EXAMPLES
*
* wp greet John
* wp greet John --greeting="Bonjour"
*/
class GreetCommand
{
public function __invoke(array $arguments, array $options): void
{
$name = $arguments[0];
$greeting = $options['greeting'] ?? 'Hello';
WP_CLI::success("{$greeting}, {$name}!");
}
public static function validate(): void
{
WP_CLI::debug('Validating input...');
}
}Generate command classes with Artisan:
# Basic command
php artisan pollora:make:wp-cli TestCommand --description="Test command"
# In a theme
php artisan pollora:make:wp-cli ThemeCommand --theme=mytheme --description="Theme utilities"
# In a plugin
php artisan pollora:make:wp-cli PluginCommand --plugin=myplugin --description="Plugin tools"
# In a module
php artisan pollora:make:wp-cli ModuleCommand --module=Portfolio --description="Module tools"The command is also available as pollora:make-wp-cli.
php artisan pollora:make:wp-cli TestCommand --description="Test command" writes app/Cms/Commands/TestCommand.php:
<?php
declare(strict_types=1);
namespace App\Cms\Commands;
use Pollora\Attributes\WpCli;
use WP_CLI;
/**
* Test command
*/
#[WpCli]
class TestCommand
{
/**
* Execute the WP CLI command.
*
* @param array $arguments Positional arguments passed to the command
* @param array $options Named options passed to the command
* @return void
*/
public function __invoke(array $arguments, array $options): void
{
// Add your command logic here
WP_CLI::success('TestCommand executed successfully!');
}
}The class is a single command (wp test: the Command suffix is dropped, the rest is kebab-cased). Add #[Command] methods yourself for subcommands. The folder is Cms/Commands under the app/ directory of the target, and the namespace follows it:
| Target | File | Namespace |
|---|---|---|
| Application | app/Cms/Commands/ |
App\Cms\Commands |
--theme=mytheme |
themes/mytheme/app/Cms/Commands/ |
Theme\Mytheme\Cms\Commands |
--plugin=myplugin |
{WP_PLUGIN_DIR}/myplugin/app/Cms/Commands/ |
Plugin\Myplugin\Cms\Commands |
--module=Portfolio |
Modules/Portfolio/app/Cms/Commands/ |
The module's PSR-4 prefix for app/, else Modules\Portfolio\Cms\Commands |
| Option | Description |
|---|---|
--description, -d |
Command description, written in the class docblock (default: A custom WP CLI command) |
--force, -f |
Overwrite existing files |
--theme |
Generate in specific theme |
--plugin |
Generate in specific plugin |
--module |
Generate in specific module |
--path |
Custom generation path |
<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use WP_CLI;
#[WpCli]
/**
* Clean up database
*/
class DatabaseCleanupCommand
{
public function __invoke(array $arguments, array $options): void
{
$dryRun = isset($options['dry-run']);
if ($dryRun) {
WP_CLI::log('DRY RUN MODE');
}
$spamCount = get_comments(['status' => 'spam', 'count' => true]);
if (!$dryRun && $spamCount > 0) {
global $wpdb;
$wpdb->delete($wpdb->comments, ['comment_approved' => 'spam']);
}
WP_CLI::success("Cleaned {$spamCount} spam comments");
}
}<?php
namespace App\Commands;
use Pollora\Attributes\WpCli;
use Pollora\Attributes\WpCli\Command;
use WP_CLI;
#[WpCli('cache')]
/**
* Cache management commands
*/
class CacheCommand
{
#[Command('flush')]
/**
* Flush all caches
*/
public function flushAll(array $arguments, array $options): void
{
wp_cache_flush();
WP_CLI::success('All caches flushed');
}
public function warmup(array $arguments, array $options): void
{
get_posts(['numberposts' => 10]);
WP_CLI::success('Cache warmed up');
}
}wp cache flush
wp cache warmuppublic function __invoke(array $arguments, array $options): void
{
try {
$this->doSomething();
WP_CLI::success('Done!');
} catch (\Exception $e) {
WP_CLI::error($e->getMessage());
}
}public function __invoke(array $arguments, array $options): void
{
if (empty($arguments[0])) {
WP_CLI::error('First argument is required.');
return;
}
}public function __invoke(array $arguments, array $options): void
{
$items = $this->getItems();
$progress = \WP_CLI\Utils\make_progress_bar('Processing', count($items));
foreach ($items as $item) {
$this->process($item);
$progress->tick();
}
$progress->finish();
}public function __invoke(array $arguments, array $options): void
{
$dryRun = isset($options['dry-run']);
foreach ($items as $item) {
if ($dryRun) {
WP_CLI::log("Would delete: {$item->title}");
} else {
$this->delete($item);
}
}
}The command class is built with its constructor dependencies, and that instance runs the command and every subcommand. This needs pollora/wp-cli 0.1.1 or later (Pollora 13.36): before it, WP-CLI built the class itself, without arguments.
class MyCommand
{
public function __construct(
private MyService $service
) {}
public function __invoke(array $arguments, array $options): void
{
$this->service->doSomething();
}
}Commands are automatically discovered in:
app/Cms/Commands/themes/{theme}/app/Cms/Commands/plugins/{plugin}/app/Cms/Commands/Modules/{Module}/app/Cms/Commands/
Cms/Commands is where the generator writes, not a requirement: Pollora scans the whole app/ directory of the application, of each theme and plugin (src/ when there is no app/) and of each module, and registers every class that carries #[WpCli]. A command generated with --module is therefore discovered.