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

UpgradeCommand::availableVersionDescriptors()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 42
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 24
c 1
b 0
f 1
nc 6
nop 0
dl 0
loc 42
rs 8.9137
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 re-run `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
                $options = $step->fixOptions($result);
96
97
                if (! empty($options)) {
98
                    [$choiceMap, $defaultLabel] = $this->normalizeFixOptions($options);
99
100
                    if (! empty($choiceMap)) {
101
                        $question = trim($step->fixMessage($result));
102
                        $question = $question !== '' ? $question : 'Select an automatic fix option';
103
                        $selectedLabel = $this->choice('  '.$question, array_keys($choiceMap), $defaultLabel);
104
                        $selectedOption = $choiceMap[$selectedLabel] ?? null;
105
106
                        if ($selectedOption !== null && $selectedOption !== '') {
107
                            $step->selectFixOption((string) $selectedOption);
108
109
                            $this->progressBlock('Applying automatic fix');
110
                            $fixResult = $step->fix($result);
111
                            $this->closeProgressBlock(strtoupper($fixResult->status->label()), $fixResult->status->color());
112
                            $this->printResultDetails($fixResult);
113
114
                            if (! $fixResult->status->isFailure()) {
115
                                $this->progressBlock('Re-running '.$step->title());
116
117
                                try {
118
                                    $result = $step->run();
119
                                } catch (\Throwable $exception) {
120
                                    $result = StepResult::failure(
121
                                        $exception->getMessage(),
122
                                        [
123
                                            'Step: '.$stepClass,
124
                                        ]
125
                                    );
126
                                }
127
128
                                $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
129
                                $this->printResultDetails($result);
130
                            }
131
                        }
132
                    }
133
                } else {
134
                    $question = trim($step->fixMessage($result));
135
                    $question = $question !== '' ? $question : 'Apply automatic fix?';
136
                    $applyFix = $this->confirm('  '.$question, true);
137
138
                    if ($applyFix) {
139
                        $this->progressBlock('Applying automatic fix');
140
                        $fixResult = $step->fix($result);
141
                        $this->closeProgressBlock(strtoupper($fixResult->status->label()), $fixResult->status->color());
142
                        $this->printResultDetails($fixResult);
143
144
                        if (! $fixResult->status->isFailure()) {
145
                            $this->progressBlock('Re-running '.$step->title());
146
147
                            try {
148
                                $result = $step->run();
149
                            } catch (\Throwable $exception) {
150
                                $result = StepResult::failure(
151
                                    $exception->getMessage(),
152
                                    [
153
                                        'Step: '.$stepClass,
154
                                    ]
155
                                );
156
                            }
157
158
                            $this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
159
                            $this->printResultDetails($result);
160
                        }
161
                    }
162
                }
163
            }
164
165
            $results[] = [
166
                'step' => $stepClass,
167
                'title' => $step->title(),
168
                'result' => $result,
169
            ];
170
        }
171
172
        $expectedVersionInstalled = $this->hasExpectedBackpackVersion($context, $config);
173
174
        $this->outputSummary($descriptor['label'], $results, $expectedVersionInstalled, $config);
175
176
        $this->note('The script has only updated what could be automated. '.PHP_EOL.'    Please run composer update to finish Step 1, then go back to the Upgrade Guide and follow all other steps, to make sure your admin panel is correctly upgraded: https://backpackforlaravel.com/docs/7.x/upgrade-guide#step-2', 'white', 'white');
177
178
        return Command::SUCCESS;
179
    }
180
181
    protected function outputSummary(
182
        string $versionLabel,
183
        array $results,
184
        bool $expectedVersionInstalled = false,
185
        ?UpgradeConfigInterface $config = null
186
    ): int {
187
        $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

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