Writing Console Commands
This guide covers how to author your own console commands in Glueful, built on Symfony Console with dependency-injection integration.
Overview
Glueful's console is a Symfony Console application wired to the framework's DI container and ApplicationContext.
Key Features
- Symfony Console Integration: commands use
#[AsCommand],configure(), andexecute() - Dependency Injection: commands resolve services from the container
- Styling helpers: SymfonyStyle-backed
success(),error(),warning(),info()and friends - Interactive helpers: confirmations, choices, prompts with validation, auto-completion, progress bars, and spinners
- Production Safety:
isProduction(),confirmProduction(),confirmDestructive()
Console Architecture
- Application (
Glueful\Console\Application): the console application, created by thegluefulentry script - BaseCommand (
Glueful\Console\BaseCommand): base class with container, context, and output helpers - Discovery: core commands are discovered from the framework's
Console/Commandsdirectory; app and extension commands are registered by service providers - Command manifest: in production the discovered core command list is cached and self-heals when it goes stale
Console Architecture
Application Class
You don't construct the console yourself. The php glueful entry script boots the framework and hands its container to the console:
use Glueful\Framework;
use Glueful\Console\Application as ConsoleApplication;
$framework = Framework::create($basePath);
$app = $framework->boot();
$console = new ConsoleApplication($app->getContainer());
exit($console->run());
When two packages register the same command name, the last one wins and the console logs which class replaced which.
BaseCommand Class
Commands extend BaseCommand:
use Glueful\Console\BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MyCommand extends BaseCommand
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->info('Starting operation...');
$this->success('Operation completed!');
return self::SUCCESS;
}
}
Output Methods:
$this->success($message)- Green success message$this->error($message)- Red error message$this->warning($message)- Yellow warning message$this->info($message)- Info message$this->note($message)- Highlighted note$this->tip($message)- "Tip:" prefixed message$this->line($message)- Plain text line$this->jsonForDisplay($value)- JSON-encode a value for display
Interactive Methods:
$this->confirm($question, $default)- Yes/no confirmation$this->ask($question, $default)- Text input with optional default$this->secret($question)- Hidden input for passwords$this->choice($question, $choices, $default)- Single choice$this->prompt($question, $default, $validator)- Text input with a validation callback$this->promptRequired($question, $default)- Re-asks until the answer is non-empty$this->multiChoice($question, $choices, $defaults)- Multiple selection$this->suggest($question, $suggestions, $default)- Input with auto-completion$this->isInteractive()- Whether input is interactive (prompts fall back to defaults otherwise)
Progress Methods:
$this->table($headers, $rows)- Formatted table$this->createProgressBar($max)- Symfony progress bar$this->progressBar($steps, $callback)- Progress bar passed to a callback$this->createEnhancedProgressBar($max)- Glueful progress bar with message formats$this->withProgress($items, $callback, $message)- Iterate items with a progress bar$this->createSpinner($message, $style)- Spinner for indeterminate work$this->withSpinner($callback, $message)- Run a callback behind a spinner, returns its result$this->withSpinnerSuccess($callback, $message, $successMessage)- Same, then prints a success line
Utility Methods:
$this->getService($serviceId)- Resolve a service from the container$this->getServiceDynamic($serviceId)- Resolve a service by a runtime id$this->getContainer()- The DI container$this->getContext()- TheApplicationContext(pass it toconfig(),db(),app(), …)$this->isProduction()- WhetherAPP_ENVis production$this->confirmProduction($operation)- Extra confirmation in production$this->confirmDestructive($message)- Warning + confirmation; in non-interactive mode returns the--forceoption (so define one)
Custom Commands
Creating Custom Commands
<?php
namespace App\Console\Commands;
use App\Services\MyService;
use Glueful\Console\BaseCommand;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'my:command',
description: 'Custom command description'
)]
class MyCommand extends BaseCommand
{
protected function configure(): void
{
$this->setHelp('Detailed help text for the command')
->addArgument('name', InputArgument::REQUIRED, 'Name argument')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip the production confirmation');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
$force = (bool) $input->getOption('force');
if (!$force && !$this->confirmProduction('execute custom operation')) {
return self::FAILURE;
}
try {
$this->info("Starting operation for: {$name}");
$myService = $this->getService(MyService::class);
$this->withProgress(range(1, 10), function (int $i) use ($myService): void {
$myService->doSomething($i);
}, 'Processing');
$this->success('Operation completed successfully!');
return self::SUCCESS;
} catch (\Exception $e) {
$this->error('Operation failed: ' . $e->getMessage());
return self::FAILURE;
}
}
}
Registering Custom Commands
App and extension commands are registered from a service provider's boot(). For an app, use a provider listed in config/serviceproviders.php (for example App\Providers\AppServiceProvider); for an extension, its own provider. Both are Glueful\Extensions\ServiceProvider subclasses:
use Glueful\Bootstrap\ApplicationContext;
use Glueful\Extensions\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(ApplicationContext $context): void
{
// Option A: discover every #[AsCommand] class in a directory (recursive, skips abstract classes)
$this->discoverCommands('App\\Console\\Commands', base_path($context, 'app/Console/Commands'));
// Option B: list classes explicitly
$this->commands([
\App\Console\Commands\MyCommand::class,
]);
}
}
Both helpers do nothing outside the CLI. Commands are taken from the container when it has a definition for the class (declare it in services() if its constructor needs injected dependencies); otherwise a BaseCommand subclass is constructed with the container and context.
Code that already holds the console application can also call $console->registerCommandClass(MyCommand::class), which resolves the class from the container and skips names that are already registered.
Command Templates
There's no make:command / generate:command. Use the scaffold commands for the classes a command usually works with:
php glueful scaffold:job SendDigestEmail
php glueful scaffold:middleware EnsureAdmin
php glueful scaffold:test UserControllerTest
php glueful scaffold:controller ReportController # writes app/Controllers (App\Controllers)
scaffold:controller ends by printing the services() line to add to your app provider so the router can resolve the new controller. A command class has no scaffold; write it by hand as shown above and register it with discoverCommands() or commands().
Scheduled work doesn't need a custom command either: define it in config/schedule.php and run php glueful queue:scheduler (run, work, list, status, health). See Scheduling.
Best Practices
Command Design
- Use Descriptive Names: follow the
group:actionpattern - Provide Good Help: include descriptions and examples
- Handle Errors Gracefully: catch exceptions and return
FAILUREwith a clear message - Production Safety: confirm destructive operations, and support
--forcefor non-interactive runs - Progress Feedback: use
withProgress()or a spinner for long-running work
New core commands appear in production automatically after a framework upgrade: the cached command manifest is stamped with the framework version and rediscovered when stale. php glueful commands:cache prebuilds it (--status shows it, --clear removes it).
Input Validation
protected function execute(InputInterface $input, OutputInterface $output): int
{
$email = $input->getOption('email');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Invalid email address provided');
return self::INVALID;
}
$file = $input->getArgument('file');
if (!file_exists($file)) {
$this->error("File not found: {$file}");
return self::FAILURE;
}
return self::SUCCESS;
}
Interactive Commands
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->confirm('Do you want to continue?', false)) {
$this->info('Operation cancelled');
return self::SUCCESS;
}
$action = $this->choice('What would you like to do?', ['create', 'update', 'delete'], 'create');
// Re-asks until a non-empty value is given
$name = $this->promptRequired('Enter name');
// Validated input: the validator throws to reject the answer
$port = $this->prompt('Port', '8080', function (?string $value): string {
if (!ctype_digit((string) $value)) {
throw new \RuntimeException('Port must be a number');
}
return (string) $value;
});
return self::SUCCESS;
}
Output Formatting
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->table(
['ID', 'Name', 'Status'],
[
[1, 'John Doe', 'Active'],
[2, 'Jane Smith', 'Inactive'],
]
);
// Progress over a collection
$this->withProgress(range(1, 100), function (int $item): void {
usleep(100_000);
});
// Indeterminate work
$count = $this->withSpinner(fn () => $this->countRecords(), 'Counting records...');
return self::SUCCESS;
}
Error Handling
use Glueful\Validation\ValidationException;
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$result = $this->performOperation();
if (!$result) {
$this->warning('Operation completed with warnings');
return self::SUCCESS;
}
$this->success('Operation completed successfully');
return self::SUCCESS;
} catch (ValidationException $e) {
$this->error('Validation failed: ' . $e->getMessage());
return self::INVALID;
} catch (\Exception $e) {
$this->error('Unexpected error: ' . $e->getMessage());
if ($output->isVerbose()) {
$this->line($e->getTraceAsString());
}
return self::FAILURE;
}
}
Service Integration
Resolve services in execute() (or a constructor that calls parent::__construct() first). The database connection is the Glueful\Database\Connection service:
use Glueful\Database\Connection;
class MyCommand extends BaseCommand
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$db = $this->getService(Connection::class); // or db($this->getContext())
$myService = $this->getService(MyService::class);
$users = $db->table('users')->where('status', 'active')->get();
$myService->processUsers($users);
return self::SUCCESS;
}
}
Configuration and Environment
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->isProduction() && !$input->getOption('force')) {
if (!$this->confirmProduction('perform this operation')) {
return self::FAILURE;
}
}
// Helpers take the ApplicationContext
$timeout = config($this->getContext(), 'app.timeout', 30);
$debug = config($this->getContext(), 'app.debug', false);
return self::SUCCESS;
}
The console gives you a consistent base for app and extension tooling: styled output, safe prompts, progress feedback, and full access to the container.
Memory Management
Glueful's memory-management tooling — the memory manager, alerting, efficient iterators, object pool, chunked DB processing, lazy container — plus a note on tracing and metrics.
Glueful Extensions
Learn how to create, configure, and ship Glueful extensions using the ServiceProvider pattern, Composer discovery, declared migrations, and production caching.