Files
michaelschiemer/docs/console-best-practices.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

12 KiB

Console Command Best Practices

Best practices für Console Command Parameter im Custom PHP Framework.

Grundprinzipien

1. Prefer Typed Parameters over ConsoleInput

Typed Parameters sind der bevorzugte Weg für Console Commands.

Avoid: Manual parsing mit ConsoleInput

#[ConsoleCommand(name: 'user:create')]
public function execute(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
    $email = $input->getArgument('email');
    $age = (int) ($input->getArgument('age') ?? 18);

    // Manuelle Validierung
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ExitCode::FAILURE;
    }
}

Prefer: Typed parameters

#[ConsoleCommand(name: 'user:create')]
public function execute(
    ConsoleOutputInterface $output,
    Email $email,        // Value Object validation
    int $age = 18        // Automatic type conversion
): ExitCode {
    // Email already validated, age already int
}

2. Use Nullable Bool for Optional Flags

Flags sollten als ?bool Parameter definiert werden, nicht als reguläre Parameter.

Nullable Bool Flags

#[ConsoleCommand(name: 'deploy')]
public function execute(
    ConsoleOutputInterface $output,
    string $environment,
    ?bool $dryRun = null,      // --dry-run flag
    ?bool $force = null,        // --force flag
    ?bool $skipTests = null     // --skip-tests flag
): ExitCode {
    if ($dryRun === true) {
        $output->writeLine("DRY RUN MODE");
    }

    if ($force === true) {
        $output->writeLine("Forcing deployment");
    }
}

Usage:

# No flags
php console.php deploy production
# dryRun=null, force=null, skipTests=null

# With flags
php console.php deploy production --dry-run --force
# dryRun=true, force=true, skipTests=null

3. ConsoleOutput only when needed

ConsoleOutputInterface nur verwenden, wenn tatsächlich Output benötigt wird.

With Output

public function execute(ConsoleOutputInterface $output, string $path): ExitCode
{
    $output->writeLine("Processing {$path}...");
    // ... processing
    $output->writeLine("Done!");
    return ExitCode::SUCCESS;
}

Without Output (Silent)

public function execute(string $path): ExitCode
{
    // Silent processing - no output needed
    file_put_contents('/tmp/result.txt', $path);
    return ExitCode::SUCCESS;
}

Parameter Types Übersicht

Framework Parameters (Auto-Injected)

Diese werden automatisch vom Framework bereitgestellt:

ConsoleInput $input              // Variable args, raw access
ConsoleOutputInterface $output   // User feedback

Position: Framework-Parameter können an beliebiger Position stehen.

// ✅ All valid positions
public function execute(ConsoleOutputInterface $output, string $name): ExitCode { }
public function execute(string $name, ConsoleOutputInterface $output): ExitCode { }
public function execute(string $name, int $age, ConsoleOutputInterface $output): ExitCode { }

User Parameters (From Command Line)

Diese werden von Command-Line-Argumenten aufgelöst:

Primitive Types:

string $name           // String argument
int $age               // Integer with validation
float $amount          // Float/decimal number
bool $flag             // Boolean flag (not recommended, use ?bool)

Nullable Bool for Flags:

?bool $verbose = null  // Optional flag: null=not provided, true=--verbose
?bool $force = null    // Optional flag: null=not provided, true=--force

Value Objects:

Email $email          // Automatic Email validation
Url $website          // Automatic URL validation
// Any VO with fromString() or __construct(string)

Enums:

enum Environment: string {
    case DEV = 'development';
    case PROD = 'production';
}

public function execute(Environment $env): ExitCode

Default Values:

string $name = 'Guest'    // Optional with default
int $maxResults = 10      // Optional with default
?bool $verbose = null     // Optional flag

Best Practices

Pattern 1: Simple Command with Required Args

#[ConsoleCommand(name: 'user:greet')]
public function execute(ConsoleOutputInterface $output, string $name): ExitCode
{
    $output->writeLine("Hello, {$name}!");
    return ExitCode::SUCCESS;
}

Usage: php console.php user:greet Alice

Pattern 2: Command with Optional Params

#[ConsoleCommand(name: 'user:list')]
public function execute(
    ConsoleOutputInterface $output,
    int $limit = 10,
    int $offset = 0
): ExitCode {
    $output->writeLine("Showing {$limit} users starting at {$offset}");
    return ExitCode::SUCCESS;
}

Usage:

  • php console.php user:list (limit=10, offset=0)
  • php console.php user:list 20 (limit=20, offset=0)
  • php console.php user:list 20 5 (limit=20, offset=5)

Pattern 3: Command with Flags

#[ConsoleCommand(name: 'deploy')]
public function execute(
    ConsoleOutputInterface $output,
    string $environment,
    ?bool $dryRun = null,
    ?bool $force = null
): ExitCode {
    $output->writeLine("Deploying to {$environment}");

    if ($dryRun === true) {
        $output->writeLine("DRY RUN MODE");
        return ExitCode::SUCCESS;
    }

    if ($force === true) {
        $output->writeLine("FORCING deployment");
    }

    // Actual deployment
    return ExitCode::SUCCESS;
}

Usage:

  • php console.php deploy production
  • php console.php deploy production --dry-run
  • php console.php deploy production --force
  • php console.php deploy production --dry-run --force

Pattern 4: Value Objects for Validation

#[ConsoleCommand(name: 'send-email')]
public function execute(
    ConsoleOutputInterface $output,
    Email $to,
    string $subject
): ExitCode {
    // Email is already validated!
    $output->writeLine("Sending email to {$to->value}");
    $output->writeLine("Subject: {$subject}");
    return ExitCode::SUCCESS;
}

Usage: php console.php send-email user@example.com "Welcome"

Pattern 5: Enums for Type Safety

enum LogLevel: string {
    case DEBUG = 'debug';
    case INFO = 'info';
    case WARNING = 'warning';
    case ERROR = 'error';
}

#[ConsoleCommand(name: 'log:show')]
public function execute(
    ConsoleOutputInterface $output,
    LogLevel $level = LogLevel::INFO
): ExitCode {
    $output->writeLine("Showing {$level->value} logs");
    return ExitCode::SUCCESS;
}

Usage:

  • php console.php log:show (level=info, default)
  • php console.php log:show error
  • php console.php log:show debug

Pattern 6: Variable Arguments (Special Case)

Only use ConsoleInput when you need:

  • Variable number of arguments
  • Raw command-line access
  • Dynamic argument handling
#[ConsoleCommand(name: 'file:process')]
public function execute(
    ConsoleInput $input,
    ConsoleOutputInterface $output
): ExitCode {
    $files = $input->getArguments();

    if (empty($files)) {
        $output->error('No files provided');
        return ExitCode::FAILURE;
    }

    foreach ($files as $file) {
        $output->writeLine("Processing {$file}");
    }

    return ExitCode::SUCCESS;
}

Usage: php console.php file:process file1.txt file2.txt file3.txt ...

When to Use What

Use Typed Parameters (90% of commands)

  • Fixed number of arguments
  • Known parameter types
  • Type validation needed
  • Value Objects or Enums
  • Optional parameters with defaults

Use ConsoleInput (10% of commands)

  • Variable number of arguments
  • Many optional flags (>5)
  • Raw command-line access needed
  • Dynamic argument handling
  • Argument count unknown at design time

Use ConsoleOutput

  • Always when user feedback needed
  • Progress indication
  • Error messages
  • Status updates
  • Not needed for silent commands

Use Nullable Bool Flags

  • Always for optional flags
  • Three-state logic (null/false/true)
  • Clean syntax: --verbose instead of --verbose=true
  • Multiple independent flags

Migration Guide

Old Style → New Style

Before (Old Style):

#[ConsoleCommand(name: 'user:create')]
public function execute(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
    $email = $input->getArgument('email') ?? throw new \Exception('Email required');
    $age = (int) ($input->getArgument('age') ?? 18);

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $output->error('Invalid email');
        return ExitCode::FAILURE;
    }

    $output->writeLine("Creating user: {$email}, age: {$age}");
    return ExitCode::SUCCESS;
}

After (New Style):

#[ConsoleCommand(name: 'user:create')]
public function execute(
    ConsoleOutputInterface $output,
    Email $email,
    int $age = 18
): ExitCode {
    // Email already validated, age already int
    $output->writeLine("Creating user: {$email->value}, age: {$age}");
    return ExitCode::SUCCESS;
}

Benefits:

  • Type safety
  • Automatic validation
  • Less boilerplate
  • Better IDE support
  • Self-documenting

Adding Flags

Before:

public function execute(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
    $dryRun = $input->hasOption('dry-run');
    $force = $input->hasOption('force');

    if ($dryRun) {
        $output->writeLine("DRY RUN");
    }
}

After:

public function execute(
    ConsoleOutputInterface $output,
    ?bool $dryRun = null,
    ?bool $force = null
): ExitCode {
    if ($dryRun === true) {
        $output->writeLine("DRY RUN");
    }
}

Testing Commands

Testing with Typed Parameters

it('creates user with typed parameters', function () {
    $method = new ReflectionMethod(UserCommand::class, 'execute');

    $resolver = new CommandParameterResolver(new MethodSignatureAnalyzer());
    $output = new ConsoleOutput();

    // Test with required args
    $params = $resolver->resolveParameters(
        $method,
        ['alice@example.com', '25'],  // email, age
        null,
        $output
    );

    expect($params[0])->toBeInstanceOf(ConsoleOutputInterface::class);
    expect($params[1])->toBeInstanceOf(Email::class);
    expect($params[2])->toBe(25);
});

Testing Flags

it('handles nullable bool flags correctly', function () {
    $method = new ReflectionMethod(DeployCommand::class, 'execute');

    // Without flags
    $params = $resolver->resolveParameters(
        $method,
        ['production'],  // environment only
        null,
        $output
    );

    expect($params[2])->toBeNull();  // dryRun
    expect($params[3])->toBeNull();  // force

    // With flags
    $params = $resolver->resolveParameters(
        $method,
        ['production', '--dry-run', '--force'],
        null,
        $output
    );

    expect($params[2])->toBeTrue();  // dryRun
    expect($params[3])->toBeTrue();  // force
});

Examples

Siehe src/Framework/Console/Examples/ConsoleParameterBestPracticesCommand.php für vollständige Beispiele aller Patterns.

Run examples:

docker exec php php console.php best:typed username
docker exec php php console.php best:flags production --verbose --force
docker exec php php console.php best:mixed Alice 25 --verified --active
docker exec php php console.php best:complex production --dry-run --skip-tests

Summary

Pattern Use Case Example
Typed Parameters Standard (90%) string $name, int $age = 18
Nullable Bool Flags Optional flags ?bool $verbose = null
Value Objects Validation Email $email, Url $website
Enums Fixed choices Environment $env
ConsoleInput Variable args (10%) ConsoleInput $input
ConsoleOutput User feedback ConsoleOutputInterface $output

Remember:

  • Typed parameters = type safety + validation
  • Nullable bool = clean flag syntax
  • Value Objects = automatic validation
  • ConsoleInput only for special cases
  • Framework parameters are optional and flexible