Writing commands
Your own php flo commands are ordinary classes in commands/. Use them for imports, reports, clean-ups and anything else you run by hand or from the scheduler.
Creating a command
php flo make:command SendReports
php flo make:command SendReports --command=reports:send
creates commands/SendReportsCommand.php (namespace App\Commands). Every class there is found automatically; there is nothing to register. The skeleton's commands/LoginUnlockCommand.php, which adds php flo login:unlock, is a complete example.
<?php
declare(strict_types=1);
namespace App\Commands;
use FloCMS\CLI\Console\Command;
use FloCMS\CLI\Console\Input\Argument;
use FloCMS\CLI\Console\Input\Definition;
use FloCMS\CLI\Console\Input\Option;
final class SendReportsCommand extends Command
{
protected string $name = 'reports:send';
protected string $description = 'Email the weekly reports';
protected array $examples = ['php flo reports:send --dry-run'];
protected function configure(Definition $definition): void
{
$definition
->argument('team', Argument::OPTIONAL, 'Only this team', 'all')
->option('dry-run', null, Option::VALUE_NONE, 'Do not send')
->option('to', 't', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'Extra recipients');
}
protected function handle(): int
{
$rows = $this->project()->database()->table('reports')->get();
$bar = $this->progress(count($rows));
foreach ($rows as $row) {
// ...
$bar->advance();
}
$bar->finish();
$this->success(count($rows) . ' reports sent.');
return self::SUCCESS;
}
}
$name, $description and $examples appear in php flo and php flo help reports:send. A longer explanation goes in protected string $help.
handle() returns the exit code: self::SUCCESS (0) or self::FAILURE (1).
Arguments and options
$definition
->argument('email', Argument::REQUIRED, 'The user')
->argument('files', Argument::IS_ARRAY, 'Any number of files') // takes the rest
->option('force', 'f', Option::VALUE_NONE, 'Do not ask') // a flag
->option('limit', null, Option::VALUE_REQUIRED, 'How many', '50') // with a default
->option('format', null, Option::VALUE_OPTIONAL, 'txt or json')
->option('to', 't', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'Repeatable');
| Type | |
|---|---|
Argument::REQUIRED, Argument::OPTIONAL |
One value; optional ones can have a default |
Argument::IS_ARRAY |
All remaining values, as an array |
Option::VALUE_NONE |
A flag: --force |
Option::VALUE_REQUIRED, Option::VALUE_OPTIONAL |
--limit=10 |
Option::VALUE_IS_ARRAY |
Repeatable: --to=a --to=b |
The second argument of option() is a one-letter shortcut, such as -f. Read the values in handle() with $this->argument('email') and $this->option('limit').
Output
| Helper | Prints |
|---|---|
line(), info(), comment() |
Plain, green and yellow text |
success(), warn(), note(), error() |
Messages with a label; warn() and error() go to STDERR, and error() shows even with --quiet |
title(), section() |
Headings |
table($headers, $rows), definitionList(...) |
Tables and key-value lists |
progress($total), spin($message, $work) |
A progress bar, and a spinner while $work runs |
Inline styles work in any text: <info>, <comment>, <error>, <warning>, <success>, <muted>, <b>, and <fg=red;bg=white;options=bold>. Colors switch off automatically when the output isn't a terminal, or when NO_COLOR is set.
Asking questions
$name = $this->ask('Project name', 'my-site');
$email = $this->ask('Email', null, fn (string $v): ?string => str_contains($v, '@') ? null : 'Enter an email address.');
$sure = $this->confirm('Delete all drafts?', false);
$env = $this->choice('Environment', ['local', 'production'], 'local');
$token = $this->secret('API token'); // hidden input
ask() and secret() take a check as the last argument: return a message to ask again, or null to accept the answer.
With --no-interaction, prompts return their default without asking, so commands still work in cron jobs and scripts.
Stopping with an error
$this->fail('The import file is empty.'); // exit code 1, no stack trace
$this->invalid('--limit must be a number.'); // exit code 2, invalid usage
Both stop the command immediately.
Using the application
$this->project()gives you the project:root(),path('storage/logs'),env('KEY'),isProduction()anddatabase(), the sameDatabaseyour models use.$this->project()->boot()loads the application like a page request does, with.env,config/config.phpand the path constants. Call it before using models orConfig.$this->call('cache:clear')runs another command.
Commands, migrations and routes from packages
A Composer package adds commands, migrations and route:list rows through its composer.json. php flo reads vendor/composer/installed.json, so nothing needs to be copied or registered:
"extra": {
"flocms": {
"commands": ["Vendor\\Package\\Console\\SyncCommand"],
"migrations": ["database/migrations"],
"route-providers": ["Vendor\\Package\\Console\\Routes"]
}
}
commandsareFloCMS\CLI\Console\Commandclasses.migrationsare folders thatphp flo migrateruns straight fromvendor/, tracked per package (--package=vendor/package).route-providersimplementFloCMS\CLI\Routing\RouteProviderand add rows toroute:list.
flocms-api registers its api:* commands this way. Your project can use the same keys in its own composer.json. A later registration with the same name wins, so a class in commands/ can replace a built-in command.
Testing commands
ApplicationTester runs commands with the output and exit code captured, and answers prompts for you:
use FloCMS\CLI\Console\Testing\ApplicationTester;
$tester = ApplicationTester::forProject(dirname(__DIR__, 2));
$tester->run(['user:create', '--role=editor'], ['Ada', 'ada@example.com', 'secret-pass', 'secret-pass']);
self::assertSame(0, $tester->getStatusCode());
self::assertStringContainsString('created', $tester->getDisplay());
To test one command on its own, use CommandTester:
use FloCMS\CLI\Console\Testing\CommandTester;
use FloCMS\CLI\Project;
$tester = new CommandTester(new SendReportsCommand(), new Project($root));
$tester->execute(['team' => 'sales', '--dry-run' => true]);
php flo make:test Reports --feature creates a feature test that starts with ApplicationTester.
Shell completion
php flo completion bash >> ~/.bashrc # bash
php flo completion zsh > ~/.zfunc/_flo # zsh
php flo completion fish > ~/.config/fish/completions/flo.fish # fish
php flo completion powershell | Out-String | Invoke-Expression # PowerShell; add to $PROFILE
Command names, aliases and options complete, your own commands and package commands included.