Docs LATEST

CLI

Creating and registering custom commands for the Velvet CLI.

Services

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