Skip to content

Commit 3eac9b7

Browse files
committed
add command
1 parent 9d6c2ba commit 3eac9b7

15 files changed

+1323
-0
lines changed

src/BackpackServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class BackpackServiceProvider extends ServiceProvider
2828
app\Console\Commands\CreateUser::class,
2929
app\Console\Commands\PublishBackpackMiddleware::class,
3030
app\Console\Commands\PublishView::class,
31+
app\Console\Commands\Upgrade\UpgradeCommand::class,
3132
app\Console\Commands\Addons\RequireDevTools::class,
3233
app\Console\Commands\Addons\RequireEditableColumns::class,
3334
app\Console\Commands\Addons\RequirePro::class,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Backpack\CRUD\app\Console\Commands\Upgrade;
4+
5+
abstract class Step
6+
{
7+
public function __construct(protected UpgradeContext $context)
8+
{
9+
}
10+
11+
abstract public function title(): string;
12+
13+
public function description(): ?string
14+
{
15+
return null;
16+
}
17+
18+
abstract public function run(): StepResult;
19+
20+
protected function context(): UpgradeContext
21+
{
22+
return $this->context;
23+
}
24+
25+
public function canFix(StepResult $result): bool
26+
{
27+
return false;
28+
}
29+
30+
public function fix(StepResult $result): StepResult
31+
{
32+
return StepResult::skipped('No automatic fix available.');
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Backpack\CRUD\app\Console\Commands\Upgrade;
4+
5+
class StepResult
6+
{
7+
public function __construct(
8+
public readonly StepStatus $status,
9+
public readonly string $summary,
10+
public readonly array $details = [],
11+
public readonly array $context = []
12+
) {
13+
}
14+
15+
public static function success(string $summary, array $details = [], array $context = []): self
16+
{
17+
return new self(StepStatus::Passed, $summary, $details, $context);
18+
}
19+
20+
public static function warning(string $summary, array $details = [], array $context = []): self
21+
{
22+
return new self(StepStatus::Warning, $summary, $details, $context);
23+
}
24+
25+
public static function failure(string $summary, array $details = [], array $context = []): self
26+
{
27+
return new self(StepStatus::Failed, $summary, $details, $context);
28+
}
29+
30+
public static function skipped(string $summary, array $details = [], array $context = []): self
31+
{
32+
return new self(StepStatus::Skipped, $summary, $details, $context);
33+
}
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Backpack\CRUD\app\Console\Commands\Upgrade;
4+
5+
enum StepStatus: string
6+
{
7+
case Passed = 'passed';
8+
case Warning = 'warning';
9+
case Failed = 'failed';
10+
case Skipped = 'skipped';
11+
12+
public function label(): string
13+
{
14+
return match ($this) {
15+
self::Passed => 'done',
16+
self::Warning => 'warn',
17+
self::Failed => 'fail',
18+
self::Skipped => 'skip',
19+
};
20+
}
21+
22+
public function color(): string
23+
{
24+
return match ($this) {
25+
self::Passed => 'green',
26+
self::Warning => 'yellow',
27+
self::Failed => 'red',
28+
self::Skipped => 'gray',
29+
};
30+
}
31+
32+
public function isFailure(): bool
33+
{
34+
return $this === self::Failed;
35+
}
36+
37+
public function isWarning(): bool
38+
{
39+
return $this === self::Warning;
40+
}
41+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
<?php
2+
3+
namespace Backpack\CRUD\app\Console\Commands\Upgrade;
4+
5+
use Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput;
6+
use Illuminate\Console\Command;
7+
8+
class UpgradeCommand extends Command
9+
{
10+
use PrettyCommandOutput;
11+
12+
protected $signature = 'backpack:upgrade
13+
{version=7 : Target Backpack version to prepare for.}
14+
{--stop-on-failure : Stop executing once a step fails.}
15+
{--format=cli : Output format (cli, json).}
16+
{--debug : Show debug information for executed processes.}';
17+
18+
protected $description = 'Run opinionated upgrade checks to help you move between Backpack major versions.';
19+
20+
public function handle(): int
21+
{
22+
$format = $this->outputFormat();
23+
24+
if (! in_array($format, ['cli', 'json'], true)) {
25+
$this->errorBlock(sprintf('Unknown output format "%s". Supported formats: cli, json.', $format));
26+
27+
return Command::INVALID;
28+
}
29+
30+
$version = (string) $this->argument('version');
31+
$majorVersion = $this->extractMajorVersion($version);
32+
33+
$stepClasses = $this->resolveStepsForMajor($majorVersion);
34+
35+
if (empty($stepClasses)) {
36+
$this->errorBlock("No automated checks registered for Backpack v{$majorVersion}.");
37+
38+
return Command::INVALID;
39+
}
40+
41+
$context = new UpgradeContext($majorVersion);
42+
43+
$this->infoBlock("Backpack v{$majorVersion} upgrade assistant", 'upgrade');
44+
45+
$results = [];
46+
47+
foreach ($stepClasses as $stepClass) {
48+
/** @var Step $step */
49+
$step = new $stepClass($context);
50+
51+
$this->progressBlock($step->title());
52+
53+
try {
54+
$result = $step->run();
55+
} catch (\Throwable $exception) {
56+
$result = StepResult::failure(
57+
$exception->getMessage(),
58+
[
59+
'Step: '.$stepClass,
60+
]
61+
);
62+
}
63+
64+
$this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
65+
66+
$this->printResultDetails($result);
67+
68+
if ($this->shouldOfferFix($step, $result)) {
69+
$applyFix = $this->confirm(' Apply automatic fix?', false);
70+
71+
if ($applyFix) {
72+
$this->progressBlock('Applying automatic fix');
73+
$fixResult = $step->fix($result);
74+
$this->closeProgressBlock(strtoupper($fixResult->status->label()), $fixResult->status->color());
75+
$this->printResultDetails($fixResult);
76+
77+
if (! $fixResult->status->isFailure()) {
78+
$this->progressBlock('Re-running '.$step->title());
79+
80+
try {
81+
$result = $step->run();
82+
} catch (\Throwable $exception) {
83+
$result = StepResult::failure(
84+
$exception->getMessage(),
85+
[
86+
'Step: '.$stepClass,
87+
]
88+
);
89+
}
90+
91+
$this->closeProgressBlock(strtoupper($result->status->label()), $result->status->color());
92+
$this->printResultDetails($result);
93+
}
94+
}
95+
}
96+
97+
$results[] = [
98+
'step' => $stepClass,
99+
'result' => $result,
100+
];
101+
102+
if ($this->option('stop-on-failure') && $result->status->isFailure()) {
103+
break;
104+
}
105+
}
106+
107+
return $this->outputSummary($majorVersion, $results);
108+
}
109+
110+
protected function outputSummary(string $majorVersion, array $results): int
111+
{
112+
$format = $this->outputFormat();
113+
114+
$hasFailure = collect($results)->contains(function ($entry) {
115+
/** @var StepResult $result */
116+
$result = $entry['result'];
117+
118+
return $result->status->isFailure();
119+
});
120+
121+
$warnings = collect($results)->filter(function ($entry) {
122+
/** @var StepResult $result */
123+
$result = $entry['result'];
124+
125+
return $result->status === StepStatus::Warning;
126+
});
127+
128+
if ($format === 'json') {
129+
$payload = [
130+
'version' => $majorVersion,
131+
'results' => collect($results)->map(function ($entry) {
132+
/** @var StepResult $result */
133+
$result = $entry['result'];
134+
135+
return [
136+
'step' => $entry['step'],
137+
'status' => $result->status->value,
138+
'summary' => $result->summary,
139+
'details' => $result->details,
140+
];
141+
})->values()->all(),
142+
];
143+
144+
$this->newLine();
145+
$this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
146+
147+
return $hasFailure ? Command::FAILURE : Command::SUCCESS;
148+
}
149+
150+
$this->newLine();
151+
$this->infoBlock('Summary', 'done');
152+
153+
$this->note(sprintf('Checked %d upgrade steps.', count($results)), 'gray');
154+
155+
if ($hasFailure) {
156+
$this->note('At least one step reported a failure. Review the messages above before continuing.', 'red', 'red');
157+
}
158+
159+
if ($warnings->isNotEmpty()) {
160+
$this->note(sprintf('%d step(s) reported warnings.', $warnings->count()), 'yellow', 'yellow');
161+
}
162+
163+
if (! $hasFailure && $warnings->isEmpty()) {
164+
$this->note('All checks passed, you are ready to continue with the manual steps from the upgrade guide.', 'green', 'green');
165+
}
166+
167+
$this->newLine();
168+
169+
return $hasFailure ? Command::FAILURE : Command::SUCCESS;
170+
}
171+
172+
protected function printResultDetails(StepResult $result): void
173+
{
174+
$color = match ($result->status) {
175+
StepStatus::Passed => 'green',
176+
StepStatus::Warning => 'yellow',
177+
StepStatus::Failed => 'red',
178+
StepStatus::Skipped => 'gray',
179+
};
180+
181+
if ($result->summary !== '') {
182+
$this->note($result->summary, $color, $color);
183+
}
184+
185+
foreach ($result->details as $detail) {
186+
$this->note($detail, 'gray');
187+
}
188+
189+
$this->newLine();
190+
}
191+
192+
protected function shouldOfferFix(Step $step, StepResult $result): bool
193+
{
194+
if ($this->outputFormat() === 'json') {
195+
return false;
196+
}
197+
198+
if (! $this->input->isInteractive()) {
199+
return false;
200+
}
201+
202+
if (! in_array($result->status, [StepStatus::Warning, StepStatus::Failed], true)) {
203+
return false;
204+
}
205+
206+
return $step->canFix($result);
207+
}
208+
209+
protected function outputFormat(): string
210+
{
211+
$format = strtolower((string) $this->option('format'));
212+
213+
return $format !== '' ? $format : 'cli';
214+
}
215+
216+
protected function resolveStepsForMajor(string $majorVersion): array
217+
{
218+
return match ($majorVersion) {
219+
'7' => [
220+
v7\Steps\EnsureLaravelVersionStep::class,
221+
v7\Steps\EnsureBackpackCrudRequirementStep::class,
222+
v7\Steps\EnsureMinimumStabilityStep::class,
223+
v7\Steps\EnsureFirstPartyAddonsAreCompatibleStep::class,
224+
v7\Steps\CheckShowOperationComponentStep::class,
225+
v7\Steps\CheckOperationConfigFilesStep::class,
226+
v7\Steps\CheckThemeTablerConfigStep::class,
227+
v7\Steps\DetectDeprecatedWysiwygUsageStep::class,
228+
v7\Steps\DetectEditorAddonRequirementsStep::class,
229+
],
230+
default => [],
231+
};
232+
}
233+
234+
protected function extractMajorVersion(string $version): string
235+
{
236+
if (preg_match('/^(\d+)/', $version, $matches)) {
237+
return $matches[1];
238+
}
239+
240+
return $version;
241+
}
242+
}

0 commit comments

Comments
 (0)