# CLI Commands — `commands/`

Scripts run via `php console <CommandName>`. No HTTP context — output goes to stdout.

## File Naming

`PascalCaseCommand.php` — e.g. `SyncDataCommand.php`

## Running Commands

```bash
php console SyncDataCommand
php console SyncDataCommand --date="2024-10-28" --force
php console CleanupDatabaseCommand --days=30 --dry-run
php console ProcessEmailQueueCommand --limit=50
```

## Command Structure

```php
<?php
// commands/SyncDataCommand.php

echo "=== Data Sync ===\n";

$options  = getopt("", ["date:", "force"]);
$date     = $options['date'] ?? 'today';
$force    = isset($options['force']);

echo "Syncing for: $date\n";

try {
    $records = Db::select("SELECT * FROM sp_sync_queue WHERE synced_at IS NULL");

    foreach ($records as $record) {
        if (!$force) {
            $exists = Db::getRow("SELECT 1 FROM sp_data WHERE external_id = :id LIMIT 1",
                [':id' => $record['external_id']]
            );
            if ($exists) { echo "Skipping {$record['external_id']}\n"; continue; }
        }

        Db::insert('sp_data', [
            'external_id' => $record['external_id'],
            'payload'     => $record['payload'],
            'synced_at'   => date('Y-m-d H:i:s')
        ]);
        echo "Inserted {$record['external_id']}\n";
    }

    echo "Done.\n";

} catch (Exception $e) {
    echo "ERROR: " . $e->getMessage() . "\n";
    exit(1);
}
```

## Error Handling Pattern

Log individual failures and keep processing — exit with code 1 only on fatal errors:

```php
$errors    = [];
$processed = 0;

foreach ($items as $item) {
    try {
        processItem($item);
        $processed++;
        echo ".";
    } catch (Exception $e) {
        $errors[] = ['id' => $item['id'], 'error' => $e->getMessage()];
        echo "E";
    }
}

echo "\n\nProcessed: $processed | Errors: " . count($errors) . "\n";

if (!empty($errors)) {
    foreach ($errors as $err) {
        echo "Item #{$err['id']}: {$err['error']}\n";
    }
    exit(1);
}
```

## Dry-Run Pattern

```php
$dryRun = isset($options['dry-run']);
if ($dryRun) { echo "DRY RUN — no changes will be made\n"; }

$count = Db::getRow("SELECT COUNT(*) as n FROM sp_logs WHERE created_at < :date",
    [':date' => $cutoffDate]
);
echo "Rows to delete: {$count['n']}\n";

if (!$dryRun) {
    Db::delete('sp_logs', 'created_at < :date', [':date' => $cutoffDate]);
}
```

## Cron Setup

```bash
crontab -e
```

```cron
# Every 5 minutes
*/5 * * * * cd /www/wwwroot/appSeedProject && php console ProcessEmailQueueCommand >> /var/log/email-queue.log 2>&1

# Every hour
0 * * * * cd /www/wwwroot/appSeedProject && php console SyncDataCommand >> /var/log/sync.log 2>&1

# Nightly cleanup at 2 AM
0 2 * * * cd /www/wwwroot/appSeedProject && php console CleanupDatabaseCommand --days=30 >> /var/log/cleanup.log 2>&1
```
