CLI
Creating and registering custom commands for the Velvet CLI.
Namespace: VelvetCMS\Commands
Command Class #
Base class for all CLI commands.
Definition #
abstract class Command
{
protected array $arguments = [];
protected array $options = [];
abstract public function handle(): int;
abstract public function signature(): string;
abstract public function description(): string;
public static function category(): string;
public function setArguments(array $arguments): void;
public function setOptions(array $options): void;
// Input access
protected function argument(string|int $key, mixed $default = null): mixed;
protected function option(string $key, mixed $default = null): mixed;
// Output helpers
protected function info(string $message): void;
protected function success(string $message): void;
protected function warning(string $message): void;
protected function error(string $message): void;
protected function line(string $message = ''): void;
protected function table(array $headers, array $rows): void;
protected function progressBar(int $total): callable;
// Interactive input
protected function ask(string $question, ?string $default = null): string;
protected function confirm(string $question, bool $default = false): bool;
protected function secret(string $question): string;
protected function choice(string $question, array $choices, mixed $default = null): mixed;
}
Creating a Command #
Extend the Command class and implement the required methods:
use VelvetCMS\Commands\Command;
class GreetCommand extends Command
{
public function signature(): string
{
return 'greet';
}
public function description(): string
{
return 'Greet the user';
}
public static function category(): string
{
return 'Custom';
}
public function handle(): int
{
$name = $this->argument(0, 'World');
$loud = $this->option('loud', false);
$message = "Hello, {$name}!";
if ($loud) {
$message = strtoupper($message);
}
$this->success($message);
return 0;
}
}
CommandRegistry #
Manages command registration and execution.
Definition #
class CommandRegistry
{
public function register(string $name, string $commandClass, array $options = []): void;
public function has(string $name): bool;
public function get(string $name): ?array;
public function all(): array;
public function grouped(): array;
public function run(string $name, array $arguments = [], array $options = []): int;
}
Registering Commands #
Direct Registration #
$registry->register('greet', GreetCommand::class);
// With options
$registry->register('greet', GreetCommand::class, [
'category' => 'Custom',
'hidden' => false,
]);
Via Event #
Register commands during the commands.registering event:
app('events')->listen('commands.registering', function ($registry) {
$registry->register('greet', GreetCommand::class);
$registry->register('import:users', ImportUsersCommand::class);
});
Arguments and Options #
The registry parses CLI input automatically:
- Arguments: Positional values after the command name
- Options:
--key=value,--flag(boolean), or-f(short boolean)
argument() #
Get a positional argument by index.
protected function argument(string|int $key, mixed $default = null): mixed
Example:
// velvet greet John
$name = $this->argument(0); // "John"
$extra = $this->argument(1, null); // null
option() #
Get a named option.
protected function option(string $key, mixed $default = null): mixed
Example:
// velvet greet --loud --times=3
$loud = $this->option('loud', false); // true
$times = $this->option('times', 1); // "3"
Output Helpers #
info() #
Display an informational message (cyan).
$this->info('Processing files...');
// [INFO] Processing files...
success() #
Display a success message (green).
$this->success('Import completed!');
// [✓] Import completed!
warning() #
Display a warning message (yellow).
$this->warning('File already exists, skipping.');
// [!] File already exists, skipping.
error() #
Display an error message (red).
$this->error('Failed to connect to database.');
// [✗] Failed to connect to database.
line() #
Output a plain line.
$this->line('Some text');
$this->line(); // Empty line
table() #
Display data in a formatted table.
protected function table(array $headers, array $rows): void
Example:
$this->table(
['Slug', 'Status', 'Updated'],
[
['welcome', 'published', '2026-01-15'],
['about', 'draft', '2026-01-20'],
['contact', 'published', '2026-01-22'],
]
);
Output:
+----------+-----------+------------+
| Slug | Status | Updated |
+----------+-----------+------------+
| welcome | published | 2026-01-15 |
| about | draft | 2026-01-20 |
| contact | published | 2026-01-22 |
+----------+-----------+------------+
progressBar() #
Create a progress bar for long-running tasks.
protected function progressBar(int $total): callable
Example:
$files = ['a.txt', 'b.txt', 'c.txt'];
$advance = $this->progressBar(count($files));
foreach ($files as $file) {
$this->processFile($file);
$advance();
}
// [========================== ] 50%
// [==================================================] 100%
Interactive Input #
ask() #
Prompt the user for text input.
protected function ask(string $question, ?string $default = null): string
Example:
$name = $this->ask('What is your name?');
$email = $this->ask('Email address', 'user@example.com');
confirm() #
Ask a yes/no question.
protected function confirm(string $question, bool $default = false): bool
Example:
if ($this->confirm('Do you want to continue?')) {
$this->processData();
}
// With default true
if ($this->confirm('Enable caching?', true)) {
$this->enableCache();
}
secret() #
Prompt for sensitive input (hidden typing).
protected function secret(string $question): string
Example:
$password = $this->secret('Enter password');
$apiKey = $this->secret('API Key');
choice() #
Present a list of choices.
protected function choice(string $question, array $choices, mixed $default = null): mixed
Example:
$environment = $this->choice('Select environment', [
'dev' => 'Development',
'staging' => 'Staging',
'prod' => 'Production',
], 'dev');
Exit Codes #
Commands should return an integer exit code:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
General error |
2 |
Misuse of command |
Example:
public function handle(): int
{
try {
$this->doWork();
return 0;
} catch (\Exception $e) {
$this->error($e->getMessage());
return 1;
}
}
Dependency Injection #
Commands support constructor injection via the container:
class SyncCommand extends Command
{
public function __construct(
private readonly PageService $pages,
private readonly CacheDriver $cache
) {}
public function signature(): string
{
return 'sync';
}
public function description(): string
{
return 'Sync content and clear cache';
}
public function handle(): int
{
$pages = $this->pages->list();
foreach ($pages as $page) {
$this->info("Syncing: {$page->slug}");
}
$this->cache->flush();
$this->success('Sync completed!');
return 0;
}
}
Complete Example #
use VelvetCMS\Commands\Command;
use VelvetCMS\Services\PageService;
class ImportCommand extends Command
{
public function __construct(
private readonly PageService $pages
) {}
public function signature(): string
{
return 'import:pages';
}
public function description(): string
{
return 'Import pages from a JSON file';
}
public static function category(): string
{
return 'Content';
}
public function handle(): int
{
$file = $this->argument(0);
if (!$file) {
$this->error('Please provide a file path.');
return 1;
}
if (!file_exists($file)) {
$this->error("File not found: {$file}");
return 1;
}
$data = json_decode(file_get_contents($file), true);
if (!$data) {
$this->error('Invalid JSON file.');
return 1;
}
$overwrite = $this->option('force', false);
$this->info('Importing ' . count($data) . ' pages...');
$advance = $this->progressBar(count($data));
$imported = 0;
$skipped = 0;
foreach ($data as $pageData) {
$slug = $pageData['slug'] ?? null;
if (!$slug) {
$skipped++;
$advance();
continue;
}
if ($this->pages->exists($slug) && !$overwrite) {
$this->warning("Skipping existing page: {$slug}");
$skipped++;
$advance();
continue;
}
$page = Page::fromArray($pageData);
$this->pages->save($page);
$imported++;
$advance();
}
$this->line();
$this->table(['Metric', 'Count'], [
['Imported', $imported],
['Skipped', $skipped],
]);
$this->success('Import completed!');
return 0;
}
}
Usage:
velvet import:pages data/pages.json
velvet import:pages data/pages.json --force