Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Push — add-update-command ( 60d3c1...c664f4 )
by Pedro
14:19
created

UpgradeCommand   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 155
c 1
b 0
f 1
dl 0
loc 313
rs 8.4
wmc 50

8 Methods

Rating   Name   Duplication   Size   Complexity  
A printResultDetails() 0 18 3
A extractMajorVersion() 0 7 2
A resolveConfigForMajor() 0 34 5
C outputSummary() 0 81 13
A outputFormat() 0 5 2
B hasExpectedBackpackVersion() 0 28 8
A shouldOfferFix() 0 15 4
C handle() 0 99 13

How to fix   Complexity   

Complex Class

Complex classes like UpgradeCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UpgradeCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Backpack\CRUD\app\Console\Commands\Upgrade;
4
5
use Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput;
6
use Backpack\CRUD\app\Console\Commands\Upgrade\Concerns\ExtractsFirstInteger;
7
use Illuminate\Console\Command;
8
use RuntimeException;
9
10
class UpgradeCommand extends Command
11
{
12
    use PrettyCommandOutput, ExtractsFirstInteger;
0 ignored issues
show
introduced by
The trait Backpack\CRUD\app\Consol...its\PrettyCommandOutput requires some properties which are not provided by Backpack\CRUD\app\Consol...\Upgrade\UpgradeCommand: $progressBar, $statusColor, $status
Loading history...
13
14
    protected $signature = 'backpack:upgrade
15
                                {version=7 : Target Backpack version to prepare for.}
16
                                {--stop-on-failure : Stop executing once a step fails.}
17
                                {--format=cli : Output format (cli, json).}
18
                                {--debug : Show debug information for executed processes.}';
19
20
    protected $description = 'Run opinionated upgrade checks to help you move between Backpack major versions.';
21
22
    public function handle(): int
23
    {
24
        $format = $this->outputFormat();
25
26
        if (! in_array($format, ['cli', 'json'], true)) {
27
            $this->errorBlock(sprintf('Unknown output format "%s". Supported formats: cli, json.', $format));
28
29
            return Command::INVALID;
30
        }
31
32
        $version = (string) $this->argument('version');
33
        $majorVersion = $this->extractMajorVersion($version);
34
35
        try {
36
            $config = $this->resolveConfigForMajor($majorVersion);
37
        } catch (RuntimeException $exception) {
38
            $this->errorBlock($exception->getMessage());
39
40
            return Command::INVALID;
41
        }
42
43
        $stepClasses = $config->steps();
44
        if (empty($stepClasses)) {
45
            $this->errorBlock("No automated checks registered for Backpack v{$majorVersion}.");
46
47
            return Command::INVALID;
48
        }
49
50
        $context = new UpgradeContext($majorVersion, addons: $config->addons());
51
52
        $this->infoBlock("Backpack v{$majorVersion} upgrade assistant", 'upgrade');
53
54
        $results = [];
55
56
        foreach ($stepClasses as $stepClass) {
57
            /** @var Step $step */
58
            $step = new $stepClass($context);
59
60
            $this->progressBlock($step->title());
61
62
            try {
63
                $result = $step->run();
64
            } catch (\Throwable $exception) {
65
                $result = StepResult::failure(
66
                    $exception->getMessage(),
67
                    [
68
                        'Step: '.$stepClass,
69
                    ]
70
                );
71
            }
72
73
            $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
74
75
            $this->printResultDetails($result);
76
77
            if ($this->shouldOfferFix($step, $result)) {
78
                $question = trim($step->fixMessage($result));
79
                $question = $question !== '' ? $question : 'Apply automatic fix?';
80
                $applyFix = $this->confirm('  '.$question, false);
81
82
                if ($applyFix) {
83
                    $this->progressBlock('Applying automatic fix');
84
                    $fixResult = $step->fix($result);
85
                    $this->closeProgressBlock(strtoupper($fixResult->status->label()), $fixResult->status->color());
86
                    $this->printResultDetails($fixResult);
87
88
                    if (! $fixResult->status->isFailure()) {
89
                        $this->progressBlock('Re-running '.$step->title());
90
91
                        try {
92
                            $result = $step->run();
93
                        } catch (\Throwable $exception) {
94
                            $result = StepResult::failure(
95
                                $exception->getMessage(),
96
                                [
97
                                    'Step: '.$stepClass,
98
                                ]
99
                            );
100
                        }
101
102
                        $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
103
                        $this->printResultDetails($result);
104
                    }
105
                }
106
            }
107
108
            $results[] = [
109
                'step' => $stepClass,
110
                'result' => $result,
111
            ];
112
113
            if ($this->option('stop-on-failure') && $result->status->isFailure()) {
114
                break;
115
            }
116
        }
117
118
        $expectedVersionInstalled = $this->hasExpectedBackpackVersion($context, $config);
119
120
        return $this->outputSummary($majorVersion, $results, $expectedVersionInstalled, $config);
121
    }
122
123
    protected function outputSummary(
124
        string $majorVersion,
125
        array $results,
126
        bool $expectedVersionInstalled = false,
127
        ?UpgradeConfigInterface $config = null
128
    ): int
129
    {
130
        $format = $this->outputFormat();
131
132
        $resultsCollection = collect($results);
0 ignored issues
show
Bug introduced by
$results of type array 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

132
        $resultsCollection = collect(/** @scrutinizer ignore-type */ $results);
Loading history...
133
134
        $hasFailure = $resultsCollection->contains(function ($entry) {
135
            /** @var StepResult $result */
136
            $result = $entry['result'];
137
138
            return $result->status->isFailure();
139
        });
140
141
        $warnings = $resultsCollection->filter(function ($entry) {
142
            /** @var StepResult $result */
143
            $result = $entry['result'];
144
145
            return $result->status === StepStatus::Warning;
146
        });
147
148
        if ($format === 'json') {
149
            $payload = [
150
                'version' => $majorVersion,
151
                'results' => collect($results)->map(function ($entry) {
152
                    /** @var StepResult $result */
153
                    $result = $entry['result'];
154
155
                    return [
156
                        'step' => $entry['step'],
157
                        'status' => $result->status->value,
158
                        'summary' => $result->summary,
159
                        'details' => $result->details,
160
                    ];
161
                })->values()->all(),
162
            ];
163
164
            $this->newLine();
165
            $this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
166
167
            return $hasFailure ? Command::FAILURE : Command::SUCCESS;
168
        }
169
170
        $this->newLine();
171
        $this->infoBlock('Summary', 'done');
172
173
        $this->note(sprintf('Checked %d upgrade steps.', count($results)), 'gray');
174
175
        if ($hasFailure) {
176
            $this->note('At least one step reported a failure. Review the messages above before continuing.', 'red', 'red');
177
        }
178
179
        if ($warnings->isNotEmpty()) {
180
            $this->note(sprintf('%d step(s) reported warnings.', $warnings->count()), 'yellow', 'yellow');
181
        }
182
183
        if (! $hasFailure && $warnings->isEmpty()) {
184
            $this->note('All checks passed, you are ready to continue with the manual steps from the upgrade guide.', 'green', 'green');
185
        }
186
187
        $postUpgradeCommands = [];
188
189
        if ($config !== null) {
190
            $postUpgradeCommands = ($config)::postUpgradeCommands();
191
        }
192
193
        if ($expectedVersionInstalled && ! $hasFailure && ! empty($postUpgradeCommands)) {
194
            $this->note("Now that you have v{$majorVersion} installed, don't forget to run the following commands:", 'green', 'green');
195
196
            foreach ($postUpgradeCommands as $command) {
197
                $this->note($command);
198
            }
199
        }
200
201
        $this->newLine();
202
203
        return $hasFailure ? Command::FAILURE : Command::SUCCESS;
204
    }
205
206
    protected function printResultDetails(StepResult $result): void
207
    {
208
        $color = match ($result->status) {
209
            StepStatus::Passed => 'green',
210
            StepStatus::Warning => 'yellow',
211
            StepStatus::Failed => 'red',
212
            StepStatus::Skipped => 'gray',
213
        };
214
215
        if ($result->summary !== '') {
216
            $this->note($result->summary, $color, $color);
217
        }
218
219
        foreach ($result->details as $detail) {
220
            $this->note($detail, 'gray');
221
        }
222
223
        $this->newLine();
224
    }
225
226
    protected function shouldOfferFix(Step $step, StepResult $result): bool
227
    {
228
        if ($this->outputFormat() === 'json') {
229
            return false;
230
        }
231
232
        if (! $this->input->isInteractive()) {
233
            return false;
234
        }
235
236
        if (! in_array($result->status, [StepStatus::Warning, StepStatus::Failed], true)) {
237
            return false;
238
        }
239
240
        return $step->canFix($result);
241
    }
242
243
    protected function outputFormat(): string
244
    {
245
        $format = strtolower((string) $this->option('format'));
246
247
        return $format !== '' ? $format : 'cli';
248
    }
249
250
    protected function resolveConfigForMajor(string $majorVersion): UpgradeConfigInterface
251
    {
252
        $configProviderClass = sprintf('%s\\v%s\\UpgradeCommandConfig', __NAMESPACE__, $majorVersion);
253
254
        if (! class_exists($configProviderClass)) {
255
            throw new RuntimeException(sprintf(
256
                'Missing upgrade config provider for Backpack v%s. Please create %s.',
257
                $majorVersion,
258
                $configProviderClass
259
            ));
260
        }
261
262
        $provider = $this->laravel
263
            ? $this->laravel->make($configProviderClass)
264
            : new $configProviderClass();
265
266
        if (! $provider instanceof UpgradeConfigInterface) {
267
            throw new RuntimeException(sprintf(
268
                'Upgrade config provider [%s] must implement %s.',
269
                $configProviderClass,
270
                UpgradeConfigInterface::class
271
            ));
272
        }
273
274
        $steps = $provider->steps();
275
276
        if (! is_array($steps)) {
0 ignored issues
show
introduced by
The condition is_array($steps) is always true.
Loading history...
277
            throw new RuntimeException(sprintf(
278
                'Upgrade config provider [%s] must return an array of step class names.',
279
                $configProviderClass
280
            ));
281
        }
282
283
        return $provider;
284
    }
285
286
    protected function extractMajorVersion(string $version): string
287
    {
288
        if (preg_match('/^(\d+)/', $version, $matches)) {
289
            return $matches[1];
290
        }
291
292
        return $version;
293
    }
294
295
    protected function hasExpectedBackpackVersion(UpgradeContext $context, UpgradeConfigInterface $config): bool
296
    {
297
        $targetConstraint = $config::backpackCrudRequirement();
298
        $targetMajor = $this->extractFirstInteger($targetConstraint);
299
300
        $composerConstraint = $context->composerRequirement('backpack/crud');
301
302
        if ($composerConstraint === null) {
303
            return false;
304
        }
305
306
        $composerMajor = $this->extractFirstInteger($composerConstraint);
307
308
        if ($targetMajor !== null && ($composerMajor === null || $composerMajor < $targetMajor)) {
309
            return false;
310
        }
311
312
        $installedMajor = $context->packageMajorVersion('backpack/crud');
313
314
        if ($installedMajor === null) {
315
            return false;
316
        }
317
318
        if ($targetMajor !== null && $installedMajor < $targetMajor) {
319
            return false;
320
        }
321
322
        return true;
323
    }
324
325
}
326