UpdateNNTmux::performDatabaseUpdate()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
3
namespace App\Console\Commands;
4
5
use Blacklight\Tmux;
6
use Illuminate\Console\Command;
7
use Illuminate\Support\Facades\App;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Facades\File;
10
use Illuminate\Support\Facades\Process;
11
use Symfony\Component\Console\Helper\ProgressBar;
12
13
class UpdateNNTmux extends Command
14
{
15
    /**
16
     * The name and signature of the console command.
17
     */
18
    protected $signature = 'nntmux:all
19
                            {--skip-git : Skip git operations}
20
                            {--skip-composer : Skip composer update}
21
                            {--skip-npm : Skip npm operations}
22
                            {--skip-db : Skip database migrations}
23
                            {--force : Force update even if up-to-date}';
24
25
    /**
26
     * The console command description.
27
     */
28
    protected $description = 'Update NNTmux installation with improved performance and error handling';
29
30
    /**
31
     * @var bool Whether the app was in maintenance mode before we started
32
     */
33
    private bool $wasInMaintenance = false;
34
35
    /**
36
     * @var bool Whether tmux was running before we started
37
     */
38
    private bool $tmuxWasRunning = false;
39
40
    /**
41
     * @var ProgressBar Progress bar for tracking operations
42
     */
43
    private ProgressBar $progressBar;
44
45
    /**
46
     * Execute the console command.
47
     */
48
    public function handle(): int
49
    {
50
        $this->info('🚀 Starting NNTmux update process...');
51
52
        // Initialize progress tracking
53
        $totalSteps = $this->calculateTotalSteps();
54
        $this->progressBar = $this->output->createProgressBar($totalSteps);
55
        $this->progressBar->start();
56
57
        try {
58
            // Prepare environment
59
            $this->prepareEnvironment();
60
61
            // Execute update steps
62
            $this->executeUpdateSteps();
63
64
            // Finalize
65
            $this->finalizeUpdate();
66
67
            $this->progressBar->finish();
68
            $this->newLine(2);
69
            $this->info('✅ NNTmux update completed successfully!');
70
71
            return Command::SUCCESS;
72
73
        } catch (\Exception $e) {
74
            $this->progressBar->finish();
75
            $this->newLine(2);
76
            $this->error('❌ Update failed: '.$e->getMessage());
77
            $this->restoreEnvironment();
78
79
            return Command::FAILURE;
80
        }
81
    }
82
83
    /**
84
     * Calculate total steps for progress tracking
85
     */
86
    private function calculateTotalSteps(): int
87
    {
88
        $steps = 3; // prepare, finalize, cleanup
89
90
        if (! $this->option('skip-git')) {
91
            $steps++;
92
        }
93
        if (! $this->option('skip-composer')) {
94
            $steps++;
95
        }
96
        if (! $this->option('skip-npm')) {
97
            $steps += 2;
98
        } // install + build
99
        if (! $this->option('skip-db')) {
100
            $steps++;
101
        }
102
103
        $steps++; // env merge
104
105
        return $steps;
106
    }
107
108
    /**
109
     * Prepare the environment for updates
110
     */
111
    private function prepareEnvironment(): void
112
    {
113
        $this->info('🔧 Preparing environment...');
114
115
        // Check if app is in maintenance mode
116
        $this->wasInMaintenance = App::isDownForMaintenance();
117
        if (! $this->wasInMaintenance) {
118
            $this->call('down', [
119
                '--render' => 'errors::maintenance',
120
                '--retry' => 120,
121
                '--secret' => config('app.key'),
122
            ]);
123
        }
124
125
        // Check if tmux is running
126
        $tmux = new Tmux;
127
        $this->tmuxWasRunning = $tmux->isRunning();
128
        if ($this->tmuxWasRunning) {
129
            $this->call('tmux-ui:stop', ['--kill' => true]);
130
        }
131
132
        $this->progressBar->advance();
133
    }
134
135
    /**
136
     * Execute the main update steps
137
     */
138
    private function executeUpdateSteps(): void
139
    {
140
        // Git operations
141
        if (! $this->option('skip-git')) {
142
            $this->performGitUpdate();
143
        }
144
145
        // Composer operations
146
        if (! $this->option('skip-composer')) {
147
            $this->performComposerUpdate();
148
        }
149
150
        // Database migrations
151
        if (! $this->option('skip-db')) {
152
            $this->performDatabaseUpdate();
153
        }
154
155
        // NPM operations
156
        if (! $this->option('skip-npm')) {
157
            $this->performNpmOperations();
158
        }
159
160
        // Clear caches and perform maintenance
161
        $this->performMaintenanceTasks();
162
    }
163
164
    /**
165
     * Perform git update with better error handling
166
     */
167
    private function performGitUpdate(): void
168
    {
169
        $this->info('📥 Updating from git repository...');
170
171
        $gitResult = $this->call('nntmux:git');
172
173
        if ($gitResult !== 0) {
174
            throw new \Exception('Git update failed');
175
        }
176
177
        $this->progressBar->advance();
178
    }
179
180
    /**
181
     * Perform composer update with optimization
182
     */
183
    private function performComposerUpdate(): void
184
    {
185
        $this->info('📦 Updating composer dependencies...');
186
187
        $composerResult = $this->call('nntmux:composer');
188
189
        if ($composerResult !== 0) {
190
            throw new \Exception('Composer update failed');
191
        }
192
193
        $this->progressBar->advance();
194
    }
195
196
    /**
197
     * Perform database updates
198
     */
199
    private function performDatabaseUpdate(): void
200
    {
201
        $this->info('🗄️ Updating database...');
202
203
        $dbResult = $this->call('nntmux:db');
204
205
        if ($dbResult !== 0) {
206
            throw new \Exception('Database update failed');
207
        }
208
209
        $this->progressBar->advance();
210
    }
211
212
    /**
213
     * Perform NPM operations with parallel processing where possible
214
     */
215
    private function performNpmOperations(): void
216
    {
217
        // Check if package.json has changed
218
        $packageLockExists = File::exists(base_path('package-lock.json'));
219
        $shouldInstall = ! $packageLockExists || $this->option('force');
220
221
        if ($shouldInstall) {
222
            $this->info('📦 Installing npm packages...');
223
224
            $process = Process::timeout(600)
225
                ->path(base_path())
226
                ->run('npm ci --silent');
227
228
            if (! $process->successful()) {
229
                // Fallback to npm install if ci fails
230
                $process = Process::timeout(600)
231
                    ->path(base_path())
232
                    ->run('npm install --silent');
233
234
                if (! $process->successful()) {
235
                    throw new \Exception('NPM install failed: '.$process->errorOutput());
236
                }
237
            }
238
        }
239
240
        $this->progressBar->advance();
241
242
        // Build assets
243
        $this->info('🔨 Building assets...');
244
245
        $buildProcess = Process::timeout(600)
246
            ->path(base_path())
247
            ->run('npm run build');
248
249
        if (! $buildProcess->successful()) {
250
            throw new \Exception('Asset build failed: '.$buildProcess->errorOutput());
251
        }
252
253
        $this->progressBar->advance();
254
    }
255
256
    /**
257
     * Perform maintenance tasks
258
     */
259
    private function performMaintenanceTasks(): void
260
    {
261
        // Clear view cache
262
        $this->info('🧹 Clearing view cache...');
263
        $this->call('view:clear');
264
265
        // Merge environment variables
266
        $this->info('⚙️ Merging environment configuration...');
267
        $this->mergeEnvironmentConfig();
268
        $this->progressBar->advance();
269
    }
270
271
    /**
272
     * Merge environment configuration with improved error handling
273
     */
274
    private function mergeEnvironmentConfig(): void
275
    {
276
        try {
277
            $envExamplePath = base_path('.env.example');
278
            $envPath = base_path('.env');
279
280
            if (! File::exists($envExamplePath)) {
281
                $this->warn('  ⚠ .env.example not found, skipping environment merge');
282
283
                return;
284
            }
285
286
            if (! File::exists($envPath)) {
287
                $this->warn('  ⚠ .env not found, skipping environment merge');
288
289
                return;
290
            }
291
292
            $envExampleVars = $this->parseEnvFile($envExamplePath);
293
            $envVars = $this->parseEnvFile($envPath);
294
295
            $missingKeys = array_diff_key($envExampleVars, $envVars);
296
297
            if (empty($missingKeys)) {
298
                $this->line('  ✓ No new environment variables to merge');
299
300
                return;
301
            }
302
303
            $this->addMissingEnvVars($envPath, $missingKeys);
304
            $this->line('  ✓ Merged '.count($missingKeys).' new environment variables');
305
306
        } catch (\Exception $e) {
307
            $this->warn('  ⚠ Environment merge failed: '.$e->getMessage());
308
        }
309
    }
310
311
    /**
312
     * Parse environment file into key-value pairs
313
     */
314
    private function parseEnvFile(string $path): array
315
    {
316
        $content = File::get($path);
317
        $vars = [];
318
319
        foreach (preg_split("/\r\n|\n|\r/", $content) as $line) {
320
            $line = trim($line);
321
322
            if (empty($line) || str_starts_with($line, '#')) {
323
                continue;
324
            }
325
326
            if (preg_match('/^([^=]+)=(.*)$/', $line, $matches)) {
327
                $key = trim($matches[1]);
328
                $value = $matches[2];
329
                $vars[$key] = $value;
330
            }
331
        }
332
333
        return $vars;
334
    }
335
336
    /**
337
     * Add missing environment variables to .env file
338
     */
339
    private function addMissingEnvVars(string $envPath, array $missingKeys): void
340
    {
341
        $content = File::get($envPath);
342
343
        if (! str_ends_with($content, "\n")) {
344
            $content .= "\n";
345
        }
346
347
        $content .= "\n# New settings added from .env.example on ".now()->toDateTimeString()."\n";
348
349
        foreach ($missingKeys as $key => $value) {
350
            $content .= "$key=$value\n";
351
        }
352
353
        File::put($envPath, $content);
354
    }
355
356
    /**
357
     * Finalize the update process
358
     */
359
    private function finalizeUpdate(): void
360
    {
361
        $this->info('🏁 Finalizing update...');
362
363
        // Clear application caches
364
        Cache::flush();
365
366
        // Restore application state
367
        $this->restoreEnvironment();
368
369
        $this->progressBar->advance();
370
    }
371
372
    /**
373
     * Restore the original environment state
374
     */
375
    private function restoreEnvironment(): void
376
    {
377
        // Restore maintenance mode state
378
        if (! $this->wasInMaintenance && App::isDownForMaintenance()) {
379
            $this->call('up');
380
        }
381
382
        // Restore tmux state
383
        if ($this->tmuxWasRunning) {
384
            $this->call('tmux-ui:start');
385
        }
386
    }
387
}
388