Backup and Restore Command for Symfony Migrations

Safeguard Your Database before running Migrations

Patric
5 min readOct 1, 2024

As Symfony developers, we understand that database migrations are a crucial part of application development. They help keep our database schema in sync with our application code. However, migrations can sometimes fail, leaving your database in an inconsistent state. To address this risk, we can create a custom command to back up the database before running migrations. In this article, we’ll explore a Symfony command that implements this idea — SafeMigrateCommand.

Overview of the SafeMigrateCommand

The SafeMigrateCommand class extends the base Symfony Command class and introduces a structured way to manage database migrations while ensuring that a backup is created before any changes are applied. If a migration fails, the command will restore the database from the backup, thereby safeguarding your data.

Here’s the structure of the command:

  1. Backup the Database: Before running migrations, we create a backup.
  2. Run Migrations: Execute the migration commands using Doctrine.
  3. Restore if Necessary: If an error occurs during migration, restore the database from the backup.

Now, let’s dive into the implementation step by step.

Step 1: Class Setup

First, we define our command class and its properties:

namespace App\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;

class SafeMigrateCommand extends Command
{
protected static $defaultName = 'app:safe-migrate';

In this code snippet:

  • Namespace and Use Statements: We import necessary Symfony components, including the command base class and utilities for input/output handling.
  • Command Name: We define a default command name for executing this command, which is app:safe-migrate.

Step 2: Properties Initialization

Next, we declare properties that will be used throughout the command:

private string $projectDir;
private string $databaseName;
private string $databaseUser;
private string $databasePassword;
private string $backupFilePath;
private SymfonyStyle $io;

public function __construct(string $projectDir, string $databaseName, string $databaseUser, string $databasePassword)
{
parent::__construct();
$this->projectDir = $projectDir; // Inject project root directory
$this->databaseName = $databaseName;
$this->databaseUser = $databaseUser;
$this->databasePassword = $databasePassword;
$this->backupFilePath = $this->projectDir . '/backups/backup.sql'; // Backup path
}

Explanation:

  • Database Credentials: We initialize the properties for the database connection, which include the database name, user, and password.
  • Backup File Path: The path where the backup will be stored is also defined here.
  • SymfonyStyle: This is used for console styling, providing an easy way to print styled messages to the console.

Step 3: Configuring the Command

The configure method sets up the command description and options:

protected function configure(): void
{
$this
->setDescription('Run Doctrine migrations with a backup. If migration fails, restores the previous state.');
}

Explanation:

  • Description: Describes what the command does.
  • Options: Allows an optional --backup flag to control whether a backup should be created.

Step 4: Executing the Command

The execute method is where the main logic resides:

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
// Step 1: Create database snapshot
$this->io->section('Creating database snapshot...');
if (!$this->createDatabaseBackup()) {
$this->io->error('Failed to create database snapshot. Aborting migration.');
return Command::FAILURE;
}
$this->io->success('Database snapshot created successfully.');

Explanation:

  • SymfonyStyle Initialization: We initialize $io to format console output.
  • Database Backup Creation: We invoke the createDatabaseBackup method to create a snapshot of the database before proceeding. If this fails, we log an error and abort the process.

Step 5: Running the Migration

Next, we run the migration commands:

    // Step 2: Run Doctrine migration
$this->io->section('Running Doctrine migrations...');
try {
$migrateCommand = $this->getApplication()->find('doctrine:migrations:migrate');
$migrateCommand->run($input, $output);
} catch (\Exception $e) {
$this->io->error('Migration failed: ' . $e->getMessage());
$this->io->section('Restoring database from snapshot...');

// Step 3: Restore database if migration fails
if ($this->restoreDatabaseBackup()) {
$this->io->success('Database restored successfully.');
} else {
$this->io->error('Failed to restore the database from snapshot.');
}
return Command::FAILURE;
}

Explanation:

  • Running Migrations: We find and execute the Doctrine migration command.
  • Error Handling: If the migration fails, we catch the exception and log an error. The database is then restored using the restoreDatabaseBackup method.

Step 6: Backup and Restore Methods

Now let’s look at how we handle backups and restores:

private function createDatabaseBackup(): bool
{
$backupDir = dirname($this->backupFilePath);
if (!is_dir($backupDir)) {
if (!mkdir($backupDir, 0755, true) && !is_dir($backupDir)) {
return false; // Failed to create directory
}
}
// Command to dump MySQL database
$command = [
'mysqldump',
'-u' . $this->databaseUser,
'-p' . $this->databasePassword,
$this->databaseName,
'--result-file=' . $this->backupFilePath,
];
$this->io->text('Running database backup command: ' . implode(' ', $command));
return $this->runCommand($command);
}

Explanation:

  • Directory Creation: Before backing up, we check if the backup directory exists and create it if not.
  • Database Dump: We use mysqldump to create a SQL file of the current database state. The command is built and logged to the console for visibility.

Similarly, the restoreDatabaseBackup method is defined as follows:

private function restoreDatabaseBackup(): bool
{
$command = [
'mysql',
'-u' . $this->databaseUser,
'-p' . $this->databasePassword,
$this->databaseName,
'-e',
'source ' . $this->backupFilePath,
];

$this->io->text('Running database restore command: ' . implode(' ', $command));
return $this->runCommand($command);
}

Explanation:

  • Database Restore: We utilize the mysql command to restore the database from the backup file. The command is also logged for debugging purposes.

Step 7: Running Commands

The runCommand method handles the execution of any command:

private function runCommand(array $command): bool
{
$process = new Process($command);
$process->setTimeout(3600); // 1 hour timeout for large migrations

try {
$process->mustRun();
return true;
} catch (\Exception $e) {
$this->io->error('Error running command: ' . $e->getMessage());
return false;
}
}

Explanation:

  • Process Management: We utilize the Symfony Process component to execute commands safely. We set a timeout of one hour to accommodate large migrations and log any errors that occur during execution.

Service Configuration

To use the SafeMigrateCommand in your Symfony application, you need to register it as a service. You can do this by adding the following configuration to your config/services.yaml file:

services:
App\Command\SafeMigrateCommand:
arguments:
$projectDir: '%kernel.project_dir%'
$databaseName: '%env(DATABASE_NAME)%'
$databaseUser: '%env(DATABASE_USER)%'
$databasePassword: '%env(DATABASE_PASSWORD)%'

Environment Variables Setup

Make sure to define the required environment variables in your .env file. This file is where you can store sensitive information such as database credentials. Here’s an example of how your .env file might look:

# .env
DATABASE_NAME=my_database
DATABASE_USER=my_user
DATABASE_PASSWORD=my_password

Make sure to replace my_database, my_user, and my_password with your actual database name, user, and password.

Here you can review the complete Command.

Conclusion

With the SafeMigrateCommand, you have a robust solution for managing database migrations in Symfony while protecting your data through automated backups. This command encapsulates best practices, ensuring that even if something goes wrong during the migration, you have a safety net to fall back on.

To use the command, simply run:

php bin/console app:safe-migrate

This command not only enhances the safety of your database migrations but also reinforces the importance of data integrity during the development lifecycle. By adopting such practices, you can minimize downtime and maintain a smooth workflow, allowing you to focus on building great applications with Symfony.

--

--

Patric

Loving web development and learning something new. Always curious about new tools and ideas.