| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
BetterWPCLI is a small, zero-dependencies, PHP library that helps you build enterprise WordPress command-line applications.
BetterWPCLI does not replace or take over any functionality of the wp runner.
Instead, it sits between WP-CLI and your custom commands.
We developed this library for the WordPress related components of the Snicco project due to the following reasons:
❌ WP-CLI has no native support for dependency-injection and does not support lazy-loading of commands.
❌ WP-CLI encourages command configuration in meta language or by hard-coded string names.
❌ WP-CLI has inconsistent and unconfigurable handling of writing to STDOUT and STDERR.
❌ WP-CLI has no error handling. Thrown exceptions go directly to the global shutdown handler (wp_die) and show up in the terminal as the dreaded "There has been a critical error on this website.Learn more about troubleshooting WordPress." . Thus, they also go to STDOUT instead of STDER.
❌ WP-CLI can detect ANSI support only for STDOUT and not individually for both STDOUT and STDERR.
❌ WP-CLI commands are hard to test because its encouraged to use the static WP_CLI class directly in your command instead of using some Input/Output abstraction.
❌ WP-CLI does not play nicely with static analysers like psalm and phpstan.
BetterWPCLI aims to solve all of these problems while providing you many additional features.
BetterWPCLI is specifically designed to be usable in distributed code like public plugins.
BetterWPCLI is distributed via composer.
composer require snicco/better-wp-cliAll commands extend the Command class.
One Command class is responsible for handling exactly ONE command and defining its own synopsis.
The command class reproduces the example command described in the WP-CLI commands cookbook .
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputFlag;
use Snicco\Component\BetterWPCLI\Synopsis\InputOption;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;
class ExampleCommand extends Command {
// You can set an explicit command name.
// If a command name is not set explicitly, it's determined from the class name.
protected static string $name = 'example';
// The short description of the command that will be shown
// when running "wp help example"
protected static string $short_description = 'Prints a greeting'
// If a long description is not set explicitly it will default to
// the short_description property.
protected static string $long_description = '## EXAMPLES' . "\n\n" . 'wp example hello Newman'
public function execute(Input $input, Output $output) : int {
$name = $input->getArgument('name'); // (string) Always a string
$type = $input->getOption('flag'); // (string) Always a string
$honk = $input->getFlag('honk'); // (bool) Always a boolean
// outputs a message followed by a "\n"
$output->writeln("$type: Hello $name!");
// Writes directly to the output stream without newlines
//$output->write('You are about');
//$output->write(' to honk');
// Writes to "\n" chars
//$output->newLine(2);
if($honk) {
$output->writeln("Honk");
}
// (This is equivalent to returning int(0))
return Command::SUCCESS;
// (This is equivalent to returning int(1))
// return Command::FAILURE;
// (This is equivalent to returning int(2))
// return Command::INVALID
}
public static function synopsis() : Synopsis{
return new Synopsis(
new InputArgument(
'name',
'The name of the person to great',
InputArgument::REQUIRED
),
// You can combine options by using bit flags.
// new InputArgument(
// 'some-other-arg',
// 'This is another arg',
// InputArgument::REQUIRED | InputArgument::REPEATING
//),
new InputOption(
'type',
'Whether or not to greet the person with success or error.',
InputOption::OPTIONAL,
'success',
['success', 'error']
),
new InputFlag('honk')
);
}
}The Synopsis value object helps you to create the command synopsis using a clear PHP API.
The Synopsis has a rich set of validation rules that are only implicit in the WP-CLI. This helps you prevent certain gotchas right away like:
A Synopsis consists of zero or more positional arguments, options or flags.
These a represented by their respective classes:
The Command class has several inbuilt flags that you can use in your commands.
You can automatically add them to all your commands by adding them to the parent synopsis.
This is totally optional.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;
class MyCommand extends Command {
public static function synopsis() : Synopsis{
return parent::synopsis()->with([
new InputArgument(
'name',
'The name of the person to great',
InputArgument::REQUIRED
),
]);
}
}This will add the following command synopsis:
Commands are registered by using the WPCLIApplication class.
if(!defined('WP_CLI')) {
return;
}
use Snicco\Component\BetterWPCLI\WPCLIApplication;
use Snicco\Component\BetterWPCLI\CommandLoader\ArrayCommandLoader;
// The namespace will be prepended to all your commands automatically.
$command_namespace = 'snicco';
// The command loader is responsible for lazily loading your commands.
// The second argument is a callable that should return an instance of
// a command by its name. This should typically be a call to your dependency injection container.
// This array can come from a configuration file.
$command_classes = [
ExampleCommand::class,
FooCommand::class,
BarCommand::class,
];
$container = /* Your dependency injection container or another factory class */
$factory = function (string $command_class) use ($container) {
return $container->get($command_class);
}
$command_loader = new ArrayCommandLoader($command_classes, $factory);
$application = new WPCLIApplication($command_namespace);
$application->registerCommands();Console input is abstracted away through an Input interface.
All commands will receive an instance of Input that holds all the passed arguments.
use Snicco\Component\BetterWPCLI\Input\Input
use Snicco\Component\BetterWPCLI\Output\Output
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputFlag;
use Snicco\Component\BetterWPCLI\Synopsis\InputOption;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;
// ...
public static function synopsis(): Synopsis
{
return new Synopsis(
new InputArgument(
'role',
'The role that should be assigned to the users',
),
new InputArgument(
'ids',
'A list of user ids that should be assigned to passed role',
InputArgument::REQUIRED | InputArgument::REPEATING
),
new InputFlag(
'notify',
'Send the user an email about his new role'
),
new InputOption(
'some-option',
),
);
}
// ...
public function execute(Input $input, Output $output): int
{
$output->writeln([
'Changing user roles',
'===================',
]);
// Arguments are retrieved by their name.
$role = $input->getArgument('role'); // (string)
// The second argument is returned if the option/argument was not passed.
$option = $input->getOption('some-option', 'some-default-value'); // (string)
$users = $input->getRepeatingArgument('ids'); // (string[]) and array of ids.
$notify = $input->getFlag('notify', false);
foreach($users as $id) {
// assign role here
if($notify) {
// send email here
}
}
return Command::SUCCESS;
}Console output is abstracted away through an Output interface.
All commands will receive an instance of Output.
Its recommend that you write use this class in your commands to write to the output stream.
This way your commands will stay testable as you can just substitute this Output interface with a test double.
However, there is nothing preventing you from using the WP_CLI class in your commands.
use Snicco\Component\BetterWPCLI\Input\Input
use Snicco\Component\BetterWPCLI\Output\Output
// ...
protected function execute(Input $input, Output $output): int
{
// outputs multiple lines to the console (adding "\n" at the end of each line)
$output->writeln([
'Starting the command',
'============',
'',
]);
// outputs a message followed by a "\n"
$output->writeln('Doing something!');
// outputs a message without adding a "\n" at the end of the line
$output->write('You are about to ');
$output->write('do something here');
// Outputs 3 "\n" chars.
$output->newLine(3);
// You can also use the WP_CLI class.
// WP_CLI::debug('doing something');
return Command::SUCCESS;
}BetterWPCLI has a concept of verbosity levels to allow the user to choose how detailed the command output should be.
See: default flags for instructions of adding the flags to your commands.
WP-CLI has a similar concept but only allows you to choose between quiet (no output) and debug (extremely verbose output including wp-cli internals.)
BetterWPCLI has the following five verbosity levels which can be either set per command or by using a SHELL_VERBOSITY environment value.
(Command line arguments have a higher priority then SHELL_VERBOSITY and --debug and --quiet overwrite all values unique to BetterWPCLI.)
| CLI option | SHELL_VERBOSITY | PHP constant |
|---|---|---|
| --quiet (wp-cli flag) | -1 | Verbosity::QUIET |
| (none) | 0 | Verbosity::NORMAL |
| --v | 1 | Verbosity::VERBOSE |
| --vv | 2 | Verbosity::VERY_VERBOSE |
| --vvv or --debug (wp-cli flag) | 3 | Verbosity::DEBUG |
It is possible to print specific information only for specific verbosity levels.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Verbosity;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
class AssignUserRoles extends Command {
public function execute(Input $input,Output $output) : int{
$output->writeln('Always printed', Verbosity::QUIET);
$output->writeln('only printed for verbosity normal and above', Verbosity::NORMAL);
$output->writeln('only printed for verbosity verbose and above', Verbosity::VERBOSE);
$output->writeln('only printed for verbosity very-verbose and above', Verbosity::VERY_VERBOSE);
$output->writeln('only printed for verbosity debug', Verbosity::DEBUG);
return Command::SUCCESS;
}
// .. synopsis defined here.
}BetterWPCLI provides you a utility class SniccoStyle that you can instantiate in your commands.
This class contains many helpers methods for creating rich console output. The style is based on the styling of the symfony/console package.
Color support is automatically detected based on the operating system, whether the command is piped and the provided flags like: --no-color, --no-ansi. See: default flags.
This class will write to STDERR unless you configure it not too.
You should use the Output instance to write important information to STDOUT.
Important information is information that could in theory be piped into other commands.
If your command does not output such information just return Command::SUCCESS and don't output anything. Silence is Golden.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
// ...
protected function execute(Input $input, Output $output): int
{
$io = new SniccoStyle($input, $output);
// ...
// Not so important information
//$io->title('Command title);
$output->writeln('Some important command output that should be piped.');
return Command::SUCCESS;
}The title() method should be used once at the start of a command.
$io->title('This is the command title');The section() method can be used to separate multiple coherent sections of a command
$io->section('This is a new section');The info() method can be used signalize successful completion of a section.
$io->info('This is an info');The note() method can be used to draw extra attention to the message. Use this sparingly.
$io->note('This is a note');The text() method output regular text without colorization.
// Passing an array is optional.
$io->text(['This is a text', 'This is another text']);The success() method should be used once at the end of a command.
// Passing an array is optional.
$io->success(['This command', 'was successful']);The warning() method should be used once at the end of a command.
// Passing an array is optional.
$io->warning(['This command', 'displays a warning']);The error() method should be used once at the end of a command if it failed.
// Passing an array is optional.
$io->error(['This command', 'did not work']);The SniccoStyle class provides several methods to get more information from the user.
If the command was run with the --no-interaction flag the default answer will be used automatically. See: default flags.
All output produced by interactive questions is written to STDERR.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
// ...
protected function execute(Input $input, Output $output): int
{
$io = new SniccoStyle($input, $output);
// The second argument is the default value
if(!$io->confirm('Are you sure that you want to continue', false)) {
$io->warning('Command aborted');
return Command::SUCCESS;
}
// Proceed
return Command::SUCCESS;
}use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
// ...
protected function execute(Input $input, Output $output): int
{
$io = new SniccoStyle($input, $output);
$domain = $io->ask('Please tell use your company domain', 'snicco.io');
$output->writeln('Your domain is: '. $domain);
}You can also ask a question and hide the response.
This will be done by changing the stty mode of the terminal.
If stty is not available, it will fall back to visible input unless you configure it otherwise.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Question\Question;
// ...
protected function execute(Input $input, Output $output): int
{
$io = new SniccoStyle($input, $output);
// This will fall back to visible input if stty is not available.
// e.g. on Windows
$secret = $io->askHidden('What is your secret?')
$question = (new Question('What is your secret'))
->withHiddenInput()
->withFallbackVisibleInput(false);
// This will throw an exception if hidden input can not be ensured.
$secret = $io->askQuestion($question);
//
}You can validate the provided answer of the user. If the validation fails the user will be presented with the same question again.
You can also set a maximum amount of attempts. If the maximum attempts are exceeded an InvalidAnswer exception will be thrown.
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Question\Question;
use Snicco\Component\BetterWPCLI\Exception\InvalidAnswer;
// ...
protected function execute(Input $input, Output $output): int
{
$io = new SniccoStyle($input, $output);
$validator = function (string $answer) :void {
if(strlen($answer) < 5) {
throw new InvalidAnswer('The name must have at least 6 characters.');
}
};
$attempts = 2;
$question = new Question('Please enter a name', 'default_name', $validator, $attempts);
$answer = $io->askQuestion($question);
}BetterWPCLI comes with very solid exception/error-handling.
This behaviour is however totally isolated and only applies to YOUR commands. Core commands or commands by other plugins are not affected in any way.
If your command throws an uncaught exception two things will happen:
This is how exceptions are displayed with different verbosity levels:
VERBOSTIY::NORMAL:
VERBOSTIY::VERBOSE:
VERBOSTIY::VERY_VERBOSE and above:
You can disable catching exceptions although this is not recommended.
use Snicco\Component\BetterWPCLI\WPCLIApplication;;
$command_loader = new ArrayCommandLoader($command_classes, $factory);
$application = new WPCLIApplication($command_namespace);
// This disables exception handling.
//All exceptions are now handled globally by WordPress again.
$application->catchException(false);
$application->registerCommands();By default a StdErrLogger is used to log exceptions using error_log.
This class is suitable for usage in distributed code as it will log exceptions to the location configured in WP_DEBUG_LOG. If you want to use a custom logger you have to pass it as the third argument when creating your WPCLIApplication.
The Logger will create a log record for all uncaught exceptions during your command lifecycle + all commands that return a non-zero exit code.
In a normal WP-CLI command errors such as notices, warnings and deprecations are not handled at all. Instead, they bubble up to the global PHP error handler.
It is a best practice to treat notices and warnings as exceptions.
BetterWPCLI will promote all errors during YOUR command to instances of ErrorException.
The following code:
use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
// ...
protected function execute(Input $input, Output $output): int
{
$arr = ['foo'];
$foo = $arr[1];
//
return Command::SUCCESS;
}will throw an exception and exit with code 1.
By default, all error including deprecations are promoted to exceptions.
If you find this to strict for your production environment you can customize the behaviour.
use Snicco\Component\BetterWPCLI\WPCLIApplication;;
$command_loader = new ArrayCommandLoader($command_classes, $factory);
$application = new WPCLIApplication($command_namespace);
// This is the default setting
$application->throwExceptionsAt(E_ALL);
// Throw exceptions for all errors expect deprecations.
$application->throwExceptionsAt(E_ALL - E_DEPRECATED - E_USER_DEPRECATED);
// This disables the behaviour entirely (NOT RECOMMENDED)
$application->throwExceptionsAt(0);
$application->registerCommands();This package comes with dedicated testing utilities which are in a separate package snicco/better-wp-cli-testing.
use Snicco\Component\BetterWPCLI\Testing\CommandTester;
$tester = new CommandTester(new CreateUserCommand());
$tester->run(['calvin', 'calvin@snicco.io'], ['send-email' => true]);
$tester->assertCommandIsSuccessful();
$tester->assertStatusCode(0);
$tester->seeInStdout('User created!');
$tester->dontSeeInStderr('Fail');This repository is a read-only split of the development repo of the Snicco project.
This is how you can contribute.
Please report issues in the Snicco monorepo.
If you discover a security vulnerability within BetterWPAPI, please follow our disclosure procedure.
Inspecting the source of the symfony console symfony/console was invaluable to developing this library.
| Back | FazBrowse Home | New Git URL |