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 ( 13a497...cc92a3 )
by Pedro
11:24
created

UpgradeCommand::handle()   C

Complexity

Conditions 13
Paths 45

Size

Total Lines 102
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 58
c 2
b 1
f 1
nc 45
nop 0
dl 0
loc 102
cc 13
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Composer\InstalledVersions;
8
use Illuminate\Console\Command;
9
use Illuminate\Filesystem\Filesystem;
10
use OutOfBoundsException;
11
use RuntimeException;
12
13
class UpgradeCommand extends Command
14
{
15
    use PrettyCommandOutput;
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...
16
    use ExtractsFirstInteger;
17
18
    protected $signature = 'backpack:upgrade
19
                                {version? : Target Backpack version to prepare for (e.g. v7, 7.1, v7-1).}
20
                                {--debug : Show debug information for executed processes.}';
21
22
    protected $description = 'Run opinionated upgrade checks to help you move between Backpack major versions.';
23
24
    protected ?array $availableConfigCache = null;
25
26
    protected array $resolvedConfigCache = [];
27
28
    protected array $renderedUpgradeDescriptions = [];
29
30
    protected array $descriptorChoiceSummaries = [];
31
32
    public function handle(): int
33
    {
34
        try {
35
            $descriptor = $this->determineTargetDescriptor($this->argument('version'));
36
            $config = $this->resolveConfigForDescriptor($descriptor);
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 {$descriptor['label']}.");
46
47
            return Command::INVALID;
48
        }
49
50
        $context = new UpgradeContext($descriptor['version'], addons: $config->addons());
51
52
        $this->infoBlock("Backpack {$descriptor['label']} upgrade assistant", 'upgrade');
53
54
        if ($this->displayDescriptorDescription($descriptor, $config)) {
55
            $this->newLine();
56
        }
57
58
        $results = [];
59
60
        foreach ($stepClasses as $stepClass) {
61
            /** @var Step $step */
62
            $step = new $stepClass($context);
63
64
            $this->progressBlock($step->title());
65
66
            try {
67
                $result = $step->run();
68
            } catch (\Throwable $exception) {
69
                $result = StepResult::failure(
70
                    $exception->getMessage(),
71
                    [
72
                        'Step: '.$stepClass,
73
                    ]
74
                );
75
            }
76
77
            $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
78
79
            $this->printResultDetails($result);
80
81
            if ($result->status->isFailure() && $step->isBlocking()) {
82
                $this->note(
83
                    sprintf(
84
                        'Please solve the issue above, then rerun `php artisan backpack:upgrade %s`.',
85
                        $descriptor['label']
86
                    ),
87
                    'red',
88
                    'red'
89
                );
90
91
                return Command::FAILURE;
92
            }
93
94
            if ($this->shouldOfferFix($step, $result)) {
95
                $question = trim($step->fixMessage($result));
96
                $question = $question !== '' ? $question : 'Apply automatic fix?';
97
                $applyFix = $this->confirm('  '.$question, false);
98
99
                if ($applyFix) {
100
                    $this->progressBlock('Applying automatic fix');
101
                    $fixResult = $step->fix($result);
102
                    $this->closeProgressBlock(strtoupper($fixResult->status->label()), $fixResult->status->color());
103
                    $this->printResultDetails($fixResult);
104
105
                    if (! $fixResult->status->isFailure()) {
106
                        $this->progressBlock('Re-running '.$step->title());
107
108
                        try {
109
                            $result = $step->run();
110
                        } catch (\Throwable $exception) {
111
                            $result = StepResult::failure(
112
                                $exception->getMessage(),
113
                                [
114
                                    'Step: '.$stepClass,
115
                                ]
116
                            );
117
                        }
118
119
                        $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
120
                        $this->printResultDetails($result);
121
                    }
122
                }
123
            }
124
125
            $results[] = [
126
                'step' => $stepClass,
127
                'result' => $result,
128
            ];
129
        }
130
131
        $expectedVersionInstalled = $this->hasExpectedBackpackVersion($context, $config);
132
133
        return $this->outputSummary($descriptor['label'], $results, $expectedVersionInstalled, $config);
134
    }
135
136
    protected function outputSummary(
137
        string $versionLabel,
138
        array $results,
139
        bool $expectedVersionInstalled = false,
140
        ?UpgradeConfigInterface $config = null
141
    ): int {
142
        $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

142
        $resultsCollection = collect(/** @scrutinizer ignore-type */ $results);
Loading history...
143
144
        $hasFailure = $resultsCollection->contains(function ($entry) {
145
            /** @var StepResult $result */
146
            $result = $entry['result'];
147
148
            return $result->status->isFailure();
149
        });
150
151
        $warnings = $resultsCollection->filter(function ($entry) {
152
            /** @var StepResult $result */
153
            $result = $entry['result'];
154
155
            return $result->status === StepStatus::Warning;
156
        });
157
158
        $this->newLine();
159
        $this->infoBlock('Summary', 'done');
160
161
        $this->note(sprintf('Checked %d upgrade steps.', count($results)), 'gray');
162
163
        if ($hasFailure) {
164
            $this->note('At least one step reported a failure. Review the messages above before continuing.', 'red', 'red');
165
        }
166
167
        if ($warnings->isNotEmpty()) {
168
            $this->note(sprintf('%d step(s) reported warnings.', $warnings->count()), 'yellow', 'yellow');
169
        }
170
171
        if (! $hasFailure && $warnings->isEmpty()) {
172
            $this->note('All checks passed, you are ready to continue with the manual steps from the upgrade guide.', 'green', 'green');
173
        }
174
175
        $postUpgradeCommands = [];
176
177
        if ($config !== null) {
178
            $postUpgradeCommands = ($config)::postUpgradeCommands();
179
        }
180
181
        if ($expectedVersionInstalled && ! $hasFailure && ! empty($postUpgradeCommands)) {
182
            $this->note("Now that you have {$versionLabel} installed, don't forget to run the following commands:", 'green', 'green');
183
184
            foreach ($postUpgradeCommands as $command) {
185
                $this->note($command);
186
            }
187
        }
188
189
        $this->newLine();
190
191
        return $hasFailure ? Command::FAILURE : Command::SUCCESS;
192
    }
193
194
    protected function printResultDetails(StepResult $result): void
195
    {
196
        $color = match ($result->status) {
197
            StepStatus::Passed => 'green',
198
            StepStatus::Warning => 'yellow',
199
            StepStatus::Failed => 'red',
200
            StepStatus::Skipped => 'gray',
201
        };
202
203
        if ($result->summary !== '') {
204
            $this->note($result->summary, $color, $color);
205
        }
206
207
        foreach ($result->details as $detail) {
208
            $this->note($detail, 'gray');
209
        }
210
211
        $this->newLine();
212
    }
213
214
    protected function shouldOfferFix(Step $step, StepResult $result): bool
215
    {
216
        if (! $this->input->isInteractive()) {
217
            return false;
218
        }
219
220
        if (! in_array($result->status, [StepStatus::Warning, StepStatus::Failed], true)) {
221
            return false;
222
        }
223
224
        return $step->canFix($result);
225
    }
226
227
    protected function resolveConfigForDescriptor(array $descriptor): UpgradeConfigInterface
228
    {
229
        $descriptorKey = $descriptor['key'] ?? null;
230
231
        if ($descriptorKey !== null && isset($this->resolvedConfigCache[$descriptorKey])) {
232
            return $this->resolvedConfigCache[$descriptorKey];
233
        }
234
235
        $configProviderClass = sprintf('%s\\%s\\UpgradeCommandConfig', __NAMESPACE__, $descriptor['namespace']);
236
237
        if (! class_exists($configProviderClass)) {
238
            $this->manuallyLoadConfigDirectory($descriptor);
239
        }
240
241
        if (! class_exists($configProviderClass)) {
242
            throw new RuntimeException(sprintf(
243
                'Missing upgrade config provider for Backpack %s. Please create %s.',
244
                $descriptor['label'],
245
                $configProviderClass
246
            ));
247
        }
248
249
        $provider = $this->laravel
250
            ? $this->laravel->make($configProviderClass)
251
            : new $configProviderClass();
252
253
        if (! $provider instanceof UpgradeConfigInterface) {
254
            throw new RuntimeException(sprintf(
255
                'Upgrade config provider [%s] must implement %s.',
256
                $configProviderClass,
257
                UpgradeConfigInterface::class
258
            ));
259
        }
260
261
        $steps = $provider->steps();
262
263
        if (! is_array($steps)) {
0 ignored issues
show
introduced by
The condition is_array($steps) is always true.
Loading history...
264
            throw new RuntimeException(sprintf(
265
                'Upgrade config provider [%s] must return an array of step class names.',
266
                $configProviderClass
267
            ));
268
        }
269
270
        if ($descriptorKey !== null) {
271
            $this->resolvedConfigCache[$descriptorKey] = $provider;
272
        }
273
274
        return $provider;
275
    }
276
277
    protected function determineTargetDescriptor(mixed $requestedVersion): array
278
    {
279
        $available = $this->availableVersionDescriptors();
280
281
        if (empty($available)) {
282
            throw new RuntimeException('No upgrade configurations were found. Please create one under '.basename(__DIR__).'.');
283
        }
284
285
        $normalizedRequested = $this->normalizeVersionKey(
286
            is_string($requestedVersion) ? $requestedVersion : null
287
        );
288
289
        if ($normalizedRequested !== null) {
290
            if (isset($available[$normalizedRequested])) {
291
                return $available[$normalizedRequested];
292
            }
293
294
            $knownTargets = implode(', ', array_map(
295
                fn (array $descriptor) => $descriptor['label'],
296
                $this->sortDescriptors($available)
297
            ));
298
299
            throw new RuntimeException(sprintf(
300
                'Unknown upgrade target [%s]. Available targets: %s.',
301
                (string) $requestedVersion,
302
                $knownTargets !== '' ? $knownTargets : 'none'
303
            ));
304
        }
305
306
        $currentKey = $this->detectCurrentVersionKey($available);
307
308
        if ($this->input->isInteractive()) {
309
            $sorted = $this->sortDescriptors($available);
310
311
            if (count($sorted) === 1) {
312
                $singleDescriptor = $sorted[0];
313
                $singleConfig = $this->resolveConfigForDescriptor($singleDescriptor);
314
315
                if (! $this->descriptorHasSteps($singleConfig)) {
316
                    if ($this->displayDescriptorDescription($singleDescriptor, $singleConfig)) {
317
                        $this->newLine();
318
                    }
319
                }
320
321
                return $singleDescriptor;
322
            }
323
324
            $choices = [];
325
            $defaultChoice = null;
326
            $summaries = [];
327
328
            foreach ($sorted as $descriptor) {
329
                $config = $this->resolveConfigForDescriptor($descriptor);
330
                $hasSteps = $this->descriptorHasSteps($config);
331
                $summary = $hasSteps ? $this->descriptorChoiceSummary($descriptor, $config) : null;
332
333
                if (! $hasSteps) {
334
                    if ($this->displayDescriptorDescription($descriptor, $config)) {
335
                        $this->newLine();
336
                    }
337
338
                    continue;
339
                }
340
341
                $isCurrent = $currentKey !== null && $descriptor['key'] === $currentKey;
342
                $label = $this->buildChoiceLabel($descriptor, $isCurrent);
343
344
                $choices[$label] = $descriptor['key'];
345
346
                if ($isCurrent && $defaultChoice === null) {
347
                    $defaultChoice = $label;
348
                }
349
350
                $summaries[] = [
351
                    'label' => $label,
352
                    'summary' => $summary ?? 'Automated checks are available for this version.',
353
                    'is_current' => $isCurrent,
354
                ];
355
            }
356
357
            if (empty($choices)) {
358
                throw new RuntimeException('No upgrade targets with automated checks are available.');
359
            }
360
361
            $this->outputDescriptorSummaryList($summaries);
362
363
            if ($defaultChoice === null) {
364
                $defaultChoice = array_key_first($choices);
365
            }
366
367
            $selectedLabel = $this->choice(
368
                'Select the Backpack upgrade path you want to run',
369
                array_keys($choices),
370
                $defaultChoice
371
            );
372
373
            $selectedKey = $choices[$selectedLabel] ?? null;
374
375
            if ($selectedKey !== null && isset($available[$selectedKey])) {
376
                return $available[$selectedKey];
377
            }
378
379
            throw new RuntimeException('Invalid upgrade target selection.');
380
        }
381
382
        if ($currentKey !== null && isset($available[$currentKey])) {
383
            return $available[$currentKey];
384
        }
385
386
        $sorted = $this->sortDescriptors($available, 'desc');
387
388
        return $sorted[0];
389
    }
390
391
    protected function displayDescriptorDescription(array $descriptor, ?UpgradeConfigInterface $config = null): bool
392
    {
393
        $key = $descriptor['key'] ?? null;
394
395
        if ($key === null) {
396
            return false;
397
        }
398
399
        if (isset($this->renderedUpgradeDescriptions[$key])) {
400
            return false;
401
        }
402
403
        try {
404
            $config ??= $this->resolveConfigForDescriptor($descriptor);
405
        } catch (\Throwable $exception) {
406
            return false;
407
        }
408
409
        $this->renderedUpgradeDescriptions[$key] = true;
410
411
        $description = $config->upgradeCommandDescription();
412
413
        if ($description === null) {
414
            return false;
415
        }
416
417
        $this->executeUpgradeCommandDescription($description);
418
419
        return true;
420
    }
421
422
    protected function descriptorHasSteps(UpgradeConfigInterface $config): bool
423
    {
424
        return count($config->steps()) > 0;
425
    }
426
427
    protected function descriptorChoiceSummary(array $descriptor, UpgradeConfigInterface $config): ?string
428
    {
429
        $key = $descriptor['key'] ?? null;
430
431
        if ($key === null) {
432
            return null;
433
        }
434
435
        if (array_key_exists($key, $this->descriptorChoiceSummaries)) {
436
            return $this->descriptorChoiceSummaries[$key];
437
        }
438
439
        if (! $config instanceof UpgradeConfigSummaryInterface) {
440
            return $this->descriptorChoiceSummaries[$key] = null;
441
        }
442
443
        $summary = $config->upgradeCommandSummary();
444
445
        if (! is_string($summary)) {
446
            return $this->descriptorChoiceSummaries[$key] = null;
447
        }
448
449
        $summary = trim($summary);
450
451
        if ($summary === '') {
452
            return $this->descriptorChoiceSummaries[$key] = null;
453
        }
454
455
        return $this->descriptorChoiceSummaries[$key] = $summary;
456
    }
457
458
    protected function outputDescriptorSummaryList(array $summaries): void
459
    {
460
        if (empty($summaries)) {
461
            return;
462
        }
463
464
        $this->newLine();
465
        $this->line('  <fg=blue>Available upgrade paths</>');
466
467
        foreach ($summaries as $entry) {
468
            $labelColor = $entry['is_current'] ? 'green' : 'yellow';
469
            $label = sprintf('<fg=%s>%s</>', $labelColor, $entry['label']);
470
            $this->line(sprintf('    %s <fg=gray>—</> %s', $label, $entry['summary']));
471
        }
472
473
        $this->newLine();
474
    }
475
476
    protected function executeUpgradeCommandDescription(?callable $description): void
477
    {
478
        if ($description === null) {
479
            return;
480
        }
481
482
        try {
483
            $description($this);
484
        } catch (\ArgumentCountError|\TypeError $exception) {
485
            if ($description instanceof \Closure) {
486
                $description->call($this);
487
488
                return;
489
            }
490
491
            $description();
492
        }
493
    }
494
495
    protected function availableVersionDescriptors(): array
496
    {
497
        if ($this->availableConfigCache !== null) {
498
            return $this->availableConfigCache;
499
        }
500
501
        $filesystem = new Filesystem();
502
503
        $descriptors = [];
504
505
        foreach ($filesystem->directories(__DIR__) as $directory) {
506
            $basename = basename($directory);
507
            $normalizedKey = $this->normalizeDirectoryKey($basename);
508
509
            if ($normalizedKey === null) {
510
                continue;
511
            }
512
513
            $configPath = $directory.DIRECTORY_SEPARATOR.'UpgradeCommandConfig.php';
514
515
            if (! $filesystem->exists($configPath)) {
516
                continue;
517
            }
518
519
            $segments = $this->versionKeySegments($normalizedKey);
520
521
            if (empty($segments)) {
522
                continue;
523
            }
524
525
            $descriptors[$normalizedKey] = [
526
                'key' => $normalizedKey,
527
                'directory' => $basename,
528
                'namespace' => str_replace('-', '_', $normalizedKey),
529
                'label' => $normalizedKey,
530
                'version' => ltrim($normalizedKey, 'v'),
531
                'segments' => $segments,
532
                'comparable' => $this->segmentsToComparable($segments),
533
            ];
534
        }
535
536
        return $this->availableConfigCache = $descriptors;
537
    }
538
539
    protected function normalizeDirectoryKey(string $directory): ?string
540
    {
541
        $trimmed = strtolower($directory);
542
543
        if (preg_match('/^v\d+(?:[-_]\d+)*$/', $trimmed) !== 1) {
544
            return null;
545
        }
546
547
        return str_replace('_', '-', $trimmed);
548
    }
549
550
    protected function normalizeVersionKey(?string $version): ?string
551
    {
552
        if ($version === null) {
553
            return null;
554
        }
555
556
        $trimmed = trim(strtolower($version));
557
558
        if ($trimmed === '') {
559
            return null;
560
        }
561
562
        preg_match_all('/\d+/', $trimmed, $matches);
563
564
        if (empty($matches[0])) {
565
            return null;
566
        }
567
568
        return 'v'.implode('-', $matches[0]);
569
    }
570
571
    protected function sortDescriptors(array $descriptors, string $direction = 'asc'): array
572
    {
573
        $list = array_values($descriptors);
574
575
        usort($list, function (array $a, array $b) use ($direction) {
576
            $comparison = version_compare($a['comparable'], $b['comparable']);
577
578
            return $direction === 'desc' ? -$comparison : $comparison;
579
        });
580
581
        return $list;
582
    }
583
584
    protected function buildChoiceLabel(array $descriptor, bool $isCurrent): string
585
    {
586
        $label = $descriptor['label'];
587
588
        if ($isCurrent) {
589
            $label .= ' (current)';
590
        }
591
592
        return $label;
593
    }
594
595
    protected function detectCurrentVersionKey(array $available): ?string
596
    {
597
        $installedPretty = $this->installedBackpackPrettyVersion();
598
599
        if ($installedPretty === null) {
600
            return null;
601
        }
602
603
        foreach ($this->possibleKeysForVersion($installedPretty) as $candidate) {
604
            if (isset($available[$candidate])) {
605
                return $candidate;
606
            }
607
        }
608
609
        return null;
610
    }
611
612
    protected function installedBackpackPrettyVersion(): ?string
613
    {
614
        try {
615
            if (! InstalledVersions::isInstalled('backpack/crud')) {
616
                return null;
617
            }
618
        } catch (OutOfBoundsException $exception) {
619
            return null;
620
        }
621
622
        try {
623
            $version = InstalledVersions::getPrettyVersion('backpack/crud');
624
625
            if ($version === null) {
626
                $version = InstalledVersions::getVersion('backpack/crud');
627
            }
628
629
            return $version ?: null;
630
        } catch (OutOfBoundsException $exception) {
631
            return null;
632
        }
633
    }
634
635
    protected function possibleKeysForVersion(string $version): array
636
    {
637
        preg_match_all('/\d+/', $version, $matches);
638
639
        $segments = $matches[0] ?? [];
640
641
        if (empty($segments)) {
642
            return [];
643
        }
644
645
        $keys = [];
646
647
        for ($length = count($segments); $length > 0; $length--) {
648
            $slice = array_slice($segments, 0, $length);
649
            $keys[] = 'v'.implode('-', $slice);
650
        }
651
652
        return $keys;
653
    }
654
655
    protected function versionKeySegments(string $key): array
656
    {
657
        preg_match_all('/\d+/', $key, $matches);
658
659
        if (empty($matches[0])) {
660
            return [];
661
        }
662
663
        return array_map('intval', $matches[0]);
664
    }
665
666
    protected function segmentsToComparable(array $segments): string
667
    {
668
        return implode('.', array_map(static function ($segment) {
669
            return (string) (int) $segment;
670
        }, $segments));
671
    }
672
673
    protected function manuallyLoadConfigDirectory(array $descriptor): void
674
    {
675
        $filesystem = new Filesystem();
676
        $basePath = __DIR__.DIRECTORY_SEPARATOR.$descriptor['directory'];
677
678
        $configPath = $basePath.DIRECTORY_SEPARATOR.'UpgradeCommandConfig.php';
679
680
        if ($filesystem->exists($configPath)) {
681
            require_once $configPath;
682
        }
683
684
        $stepsPath = $basePath.DIRECTORY_SEPARATOR.'Steps';
685
686
        if (! $filesystem->isDirectory($stepsPath)) {
687
            return;
688
        }
689
690
        foreach ($filesystem->allFiles($stepsPath) as $file) {
691
            /** @var \Symfony\Component\Finder\SplFileInfo $file */
692
            if (strtolower($file->getExtension()) !== 'php') {
693
                continue;
694
            }
695
696
            require_once $file->getRealPath();
697
        }
698
    }
699
700
    protected function hasExpectedBackpackVersion(UpgradeContext $context, UpgradeConfigInterface $config): bool
701
    {
702
        $targetConstraint = $config::backpackCrudRequirement();
703
        $targetMajor = $this->extractFirstInteger($targetConstraint);
704
705
        $composerConstraint = $context->composerRequirement('backpack/crud');
706
707
        if ($composerConstraint === null) {
708
            return false;
709
        }
710
711
        $composerMajor = $this->extractFirstInteger($composerConstraint);
712
713
        if ($targetMajor !== null && ($composerMajor === null || $composerMajor < $targetMajor)) {
714
            return false;
715
        }
716
717
        $installedMajor = $context->packageMajorVersion('backpack/crud');
718
719
        if ($installedMajor === null) {
720
            return false;
721
        }
722
723
        if ($targetMajor !== null && $installedMajor < $targetMajor) {
724
            return false;
725
        }
726
727
        return true;
728
    }
729
}
730