php 67 lines · 8 steps

How a database backup command works in Laravel

An Artisan command dumps the database to compressed storage, prunes old copies, and emails admins when it fails.

Explained by highlit
1<?php
2 
3namespace App\Console\Commands;
4 
5use Illuminate\Console\Command;
6use Illuminate\Support\Facades\Mail;
7use Illuminate\Support\Facades\Storage;
8use App\Mail\BackupFailed;
9use Symfony\Component\Process\Process;
10use Symfony\Component\Process\Exception\ProcessFailedException;
11use Throwable;
12 
13class BackupDatabase extends Command
14{
15 protected $signature = 'db:backup {--keep=7 : Number of nightly backups to retain}';
16 
17 protected $description = 'Dump the database to storage and notify admins on failure';
18 
19 public function handle(): int
20 {
21 $connection = config('database.default');
22 $db = config("database.connections.{$connection}");
23 
24 $filename = sprintf('backups/%s-%s.sql.gz', $db['database'], now()->format('Y-m-d_His'));
25 $target = Storage::disk('local')->path($filename);
26 
27 Storage::disk('local')->makeDirectory('backups');
28 
29 $command = sprintf(
30 'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction %s | gzip > %s',
31 escapeshellarg($db['host']),
32 escapeshellarg((string) $db['port']),
33 escapeshellarg($db['username']),
34 escapeshellarg($db['password']),
35 escapeshellarg($db['database']),
36 escapeshellarg($target)
37 );
38 
39 try {
40 $process = Process::fromShellCommandline($command)->setTimeout(600);
41 $process->mustRun();
42 
43 $this->pruneOldBackups((int) $this->option('keep'));
44 $this->info("Backup written to {$filename}");
45 
46 return self::SUCCESS;
47 } catch (ProcessFailedException|Throwable $e) {
48 report($e);
49 
50 Mail::to(config('backup.notify_email'))
51 ->send(new BackupFailed($e->getMessage()));
52 
53 $this->error('Database backup failed: ' . $e->getMessage());
54 
55 return self::FAILURE;
56 }
57 }
58 
59 protected function pruneOldBackups(int $keep): void
60 {
61 $files = collect(Storage::disk('local')->files('backups'))
62 ->sortDesc()
63 ->slice($keep);
64 
65 Storage::disk('local')->delete($files->all());
66 }
67}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping external shell processes in try/catch lets you turn failures into reports and notifications instead of silent crashes.
  2. 2Escaping every shell argument prevents injection when interpolating config values into a command string.
  3. 3Retention is a separate concern: sort backups and slice off everything beyond the keep count to prune.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How a database backup command works in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code