symandy/database-backup-bundle
Symandy DatabaseBackupBundle provides simple database backup and restore tooling for Symfony apps. Configure your connection, create dumps from the console, and schedule automated backups so your data stays protected and can be recovered quickly when needed with minimal setup.
A blunt framing first, because it changes every step after: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/database-backup.php, no package auto-discovery, no artisan command registered for you. composer require symandy/database-backup-bundle will succeed (Laravel already ships the Symfony components it depends on), but nothing will be wired up. Your job is to decide whether to bridge it into Laravel or lift the dump logic out of it.
Where to look first, in this order, inside vendor/symandy/database-backup-bundle/:
composer.json — the exact autoload namespace, and critically the symfony/* version constraints. Last release was 2023-04, so it targets Symfony 6.2/6.3-era components; check composer why-not symfony/console before promising anything to your team.src/Command/ — the real API surface. Run list-style discovery or just read the classes to get the command names, arguments, and options. This is the contract you'll actually use.src/DependencyInjection/Configuration.php — every config key, its default, and whether it expects a scalar, an array, or a %env()% placeholder. This file is the documentation.Resources/config/services.yaml — the service IDs and constructor arguments you'd have to reproduce if you bridge via the container.Minimal first use case: nightly mysqldump of the app database to S3, with a matching restore path for local debugging. Don't attempt to invoke this from a controller or a queued job. Pick one of two entry points:
config/bundles.php, add a YAML config block, and you're done. This is the intended path and the only one with zero friction.Symfony\Component\Console\Application inside an Artisan command and add the bundle's command classes to it. Roughly:// app/Console/Commands/DbBackup.php
public function handle(): int
{
$console = new \Symfony\Component\Console\Application('db', '1.0');
// see src/Command/ for the real class + constructor deps
$console->add(new BackupCommand(/* connection params, dumper, filesystem */));
$input = new \Symfony\Component\Console\Input\ArrayInput(['command' => 'symandy:database:backup']);
$output = new \Symfony\Component\Console\Output\BufferedOutput();
return $console->run($input, $output) === 0 ? self::SUCCESS : self::FAILURE;
}
The honest assessment: this bundle is thin. If you read src/ and find it's essentially "build a mysqldump argv, shell out via Process, write the file," strongly consider reimplementing that in ~40 lines inside a Laravel command rather than paying the Symfony DI tax forever. Bridge it if you need the restore path or the config-driven multi-connection behavior.
1. Artisan wrapper → Symfony console. The lowest-risk integration: keep the bundle's command classes intact, register them on a standalone Application, and expose a thin Artisan facade. You get Schedule::command() compatibility and --option pass-through without touching the container.
// routes/console.php
Schedule::command('db:backup --connection=mysql')
->dailyAt('02:00')
->withoutOverlapping()
->onFailure(fn () => Log::error('db backup failed'));
2. Service-provider bridge (only if you need the services elsewhere). Translate the bundle's YAML config into a Laravel config array and rebind the services:
public function register(): void
{
$this->app->singleton(BackupManager::class, function ($app) {
$cfg = config('database-backup'); // your config/database-backup.php
return new BackupManager(
$cfg['dumps_path'],
$cfg['connections'][$cfg['default']],
new ProcessBuilder(),
);
});
}
Watch out: the bundle's Symfony config uses %env(DB_HOST)%-style placeholders which do not resolve through Laravel's env(). You'll be doing the substitution yourself.
3. Pipe the dump into Laravel's filesystem instead of a local path. The bundle likely writes to a local directory. The cleanest adaptation is to capture the output (BufferedOutput or the Process stdout) and push it through Storage::disk('s3')->writeStream(). That keeps backups on your existing Laravel disks and lets you reuse retention/rotation logic you already have.
4. Pre-deploy / pre-migration snapshot. Wrap php artisan migrate in a command that runs the backup first, and abort the deploy on a non-zero exit code. This is the single highest-value day-to-day use — far more than nightly cron.
5. CI and E2E fixtures. For integration tests that need a realistic DB, use the restore path to load a known dump into a throwaway schema. This is where multi-connection config (one for fixtures, one for prod) earns its keep.
6. Debugging workflow. Reproduce failures outside Laravel entirely: vendor/bin/… console invocation with -vvv will surface the underlying mysqldump stderr, which your Artisan wrapper will otherwise swallow. Always tee stderr to a log file in the wrapper.
composer require and expect php artisan list to show anything, you'll waste an hour. Register explicitly or not at all.symfony/console Laravel 11 resolved. Check composer why-not before you design around it. If it conflicts, the "lift the logic out" path is no longer optional.mysqldump / pg_dump must be on PATH for whatever user runs PHP-FPM or the scheduler — that's a different user than your shell. Verify with sudo -u www-data which mysqldump.mysqldump and the server produces silent truncation on some MySQL 5.7→8.x combos. Pin or at least assert the client version at command startup.ps. If the bundle passes -p<password> directly, prefer a --defaults-extra-file containing the credentials, created in a tempnam() file and unlinked in a finally. Worth checking src/ for this specifically — many small bundles get it wrong.Process stdout rather than building a string. If it's string-based, set memory_limit explicitly and cap at a few hundred MB, or switch to streaming in your wrapper.%env()% vs env(). Symfony resolves env placeholders at container-build time with its own rules (resolve:, default:, bool: prefixes). Passing Laravel's env() values in as plain strings is usually fine, but "true" staying a string is a classic bug.mysqldump output can be mangled by a non-UTF-8 locale on the host; set LC_ALL=C in the Process env array if you see corrupted non-ASCII data on restore.confirm() prompt for non---force invocations, and hard-refuse when app()->isProduction() unless --force is passed.src/ yourself. Budget for it up front; don't assume "it must support X."Symfony\Component\Process\Process at the boundary; keep one slow integration test that does a real dump/restore against a disposable schema.How can I help you explore Laravel packages today?