Passed
Push — master ( f8b66f...e23e5d )
by Darko
11:58
created

FetchSamples   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 23
eloc 85
c 2
b 1
f 0
dl 0
loc 141
rs 10

1 Method

Rating   Name   Duplication   Size   Complexity  
D handle() 0 130 23
1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\Release;
6
use Illuminate\Console\Command;
7
use Illuminate\Support\Facades\Artisan;
8
9
class FetchSamples extends Command
10
{
11
    protected $signature = 'releases:fetch-samples
12
                                {--category= : Category id or comma-separated list of category ids (required)}
13
                                {--limit=0 : Max number of releases to process (0 = all)}
14
                                {--chunk=500 : Chunk size when iterating releases}
15
                                {--dry-run : Show how many and which GUIDs would be processed without running}
16
                                {--show-output : Display output from each releases:additional invocation}';
17
18
    protected $description = 'Fetch/generate samples by running additional postprocessing (with reset) for releases in the supplied category / categories having jpgstatus = 0.';
19
20
    public function handle(): int
21
    {
22
        $limit = (int) $this->option('limit');
23
        $chunkSize = (int) $this->option('chunk');
24
        $dryRun = (bool) $this->option('dry-run');
25
        $showOutput = (bool) $this->option('show-output');
26
        $categoryOpt = $this->option('category');
27
28
        // Validate the required category option.
29
        if ($categoryOpt === null || trim((string) $categoryOpt) === '') {
30
            $this->info('Category option is empty. Provide --category with one or more numeric category IDs. Command will not run.');
31
32
            return self::SUCCESS;
33
        }
34
35
        // Parse categories: allow comma or whitespace separated values.
36
        $catIds = collect(preg_split('/[\s,]+/', trim((string) $categoryOpt), -1, PREG_SPLIT_NO_EMPTY))
0 ignored issues
show
Bug introduced by
preg_split('/[\s,]+/', t...ds\PREG_SPLIT_NO_EMPTY) of type array<mixed,array>|string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

36
        $catIds = collect(/** @scrutinizer ignore-type */ preg_split('/[\s,]+/', trim((string) $categoryOpt), -1, PREG_SPLIT_NO_EMPTY))
Loading history...
37
            ->map(fn ($v) => trim($v))
38
            ->filter(fn ($v) => ctype_digit($v))
39
            ->map(fn ($v) => (int) $v)
40
            ->unique()
41
            ->values();
42
43
        if ($catIds->isEmpty()) {
44
            $this->info('Category option provided but no valid numeric IDs parsed. Command will not run.');
45
46
            return self::SUCCESS;
47
        }
48
49
        if ($limit < 0) {
50
            $this->error('Limit must be >= 0');
51
52
            return self::FAILURE;
53
        }
54
        if ($chunkSize < 1) {
55
            $this->error('Chunk size must be >= 1');
56
57
            return self::FAILURE;
58
        }
59
60
        // Build base query now
61
        $baseQuery = Release::query()
62
            ->whereIn('categories_id', $catIds->all())
63
            ->where('jpgstatus', 0)
64
            ->orderBy('id');
0 ignored issues
show
Bug introduced by
'id' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

64
            ->orderBy(/** @scrutinizer ignore-type */ 'id');
Loading history...
65
66
        $totalAll = $baseQuery->count();
67
        if ($totalAll === 0) {
68
            $this->info('No matching releases found (categories_id IN ['.implode(',', $catIds->all()).'] AND jpgstatus = 0).');
69
70
            return self::SUCCESS;
71
        }
72
73
        $effectiveTotal = $limit > 0 ? min($limit, $totalAll) : $totalAll;
74
        $this->info('Categories: ['.implode(',', $catIds->all()).']');
75
        $this->info("Found {$totalAll} matching release(s). Processing {$effectiveTotal}.".($dryRun ? ' (dry-run)' : ''));
76
77
        if ($dryRun) {
78
            $previewQuery = clone $baseQuery;
79
            if ($limit > 0) {
80
                $previewQuery->limit($limit);
81
            }
82
            $previewGuids = $previewQuery->pluck('guid');
83
            $this->line('Dry run: GUIDs to process (with --reset):');
84
            foreach ($previewGuids as $g) {
85
                $this->line($g);
86
            }
87
            $this->info('Dry run complete.');
88
89
            return self::SUCCESS;
90
        }
91
92
        $processed = 0;
93
        $failed = 0;
94
        $remaining = $effectiveTotal;
95
96
        $bar = $this->output->createProgressBar($effectiveTotal);
0 ignored issues
show
Bug introduced by
It seems like $effectiveTotal can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Query\Builder; however, parameter $max of Symfony\Component\Consol...le::createProgressBar() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

96
        $bar = $this->output->createProgressBar(/** @scrutinizer ignore-type */ $effectiveTotal);
Loading history...
97
        $bar->start();
98
99
        $query = clone $baseQuery;
100
101
        $query->chunkById($chunkSize, function ($releases) use (&$processed, &$failed, &$remaining, $bar, $showOutput) {
102
            foreach ($releases as $release) {
103
                if ($remaining <= 0) {
104
                    return false; // stop chunking
105
                }
106
107
                $guid = $release->guid;
108
                try {
109
                    // Call the existing single-release additional processing command with this GUID.
110
                    $exitCode = Artisan::call('releases:additional', [
111
                        'guid' => $guid, // pass GUID explicitly
112
                        '--reset' => true,
113
                    ]);
114
                    $subOutput = trim(Artisan::output());
115
116
                    if ($exitCode === 0) {
117
                        $processed++;
118
                        if ($showOutput && $subOutput !== '') {
119
                            $this->getOutput()->writeln("\n<info>{$guid}</info> -> {$subOutput}");
120
                        }
121
                    } else {
122
                        $failed++;
123
                        $this->getOutput()->writeln("\n<error>Non-zero exit code ({$exitCode}) for GUID {$guid}</error>".($showOutput && $subOutput !== '' ? "\n  Output: {$subOutput}" : ''));
124
                    }
125
                } catch (\Throwable $e) {
126
                    $failed++;
127
                    $this->getOutput()->writeln("\n<error>Error processing GUID {$guid}: {$e->getMessage()}</error>");
128
                }
129
130
                $remaining--;
131
                $bar->advance();
132
133
                if ($remaining <= 0) {
134
                    break; // exit foreach to stop further work
135
                }
136
            }
137
138
            if ($remaining <= 0) {
139
                return false; // signal chunkById to stop
140
            }
141
142
            return true; // continue chunking
143
        }, 'id');
144
145
        $bar->finish();
146
        $this->newLine();
147
        $this->info("Processing complete. Success: {$processed}, Failed: {$failed}.");
148
149
        return $failed === 0 ? self::SUCCESS : self::FAILURE;
150
    }
151
}
152