sameoldnick/laravel-backup-manager
Laravel package for managing application backups, offering configurable backup routines and storage destinations to help automate database and file backups within Laravel projects.
This package wraps backup creation, retention/cleanup and destination (filesystem) management behind services, jobs, artisan commands and optional Inertia/React UI responders. v3.0.0 was a major release (cleanup schedules dispatched as jobs, destination-scoped schedules, moved public APIs); v3.1.1 is a small patch that fixes scheduler environment parsing — pick it up if you run per-environment schedules.
composer require sameoldnick/laravel-backup-manager:^3.1. A ^2.0 constraint will otherwise pin you to the old release while you chase confusing class-not-found errors. Staying on ^3.0 still works, but you'll miss the explode() fix for comma-separated scheduler environments.php artisan vendor:publish --tag=backup-manager-config --force
php artisan vendor:publish --tag=backup-manager-migrations --force
php artisan migrate
The cleanup_schedule_filesystem_configuration pivot migration is required — cleanup schedule destination scoping silently has nowhere to store its selection without it.config/backup-manager.php — the jobs.backup and jobs.cleanup blocks (connection, queue, group, delay, middleware, after-commit) are the first place to look when jobs behave unexpectedly.backup:clean; they dispatch CleanupJob. If your default queue connection isn't sync, php artisan queue:work (or Horizon) is now mandatory for scheduled cleanups to run at all.// instead of waiting on the artisan command
PerformBackupService::dispatchBackupJob(disks: ['s3-archive']);
Or dispatch CleanupJob / BackupJob directly onto a specific queue. This is the fastest way to confirm config, destination resolution and queue wiring all work before touching the UI layer.Where to look first: the published config file, the responder stubs (they encode the new view-data shape), and the React UI guide in the docs which documents selectedDestinationIds.
Queueable operations instead of commands. Anything that used to be Artisan::call('backup:run') or backup:clean inside a controller should now be a dispatch. BackupJob and CleanupJob accept an explicit disks list, which is how you get per-tenant or per-destination backups:
BackupJob::dispatch(disks: [$tenant->disk_name])
->onConnection(config('backup-manager.jobs.backup.connection'))
->onQueue(config('backup-manager.jobs.backup.queue'));
Prefer PerformBackupService::dispatchBackupJob() when you want the package to apply its own configured queue options rather than hand-chaining them.
Centralise queue configuration, don't scatter it. Put connection/queue/group/delay/middleware/after-commit in config/backup-manager.php and let the scheduler and jobs read from there. Use after-commit where a backup should only be queued once the surrounding DB transaction succeeds (e.g. dispatching from an observer after creating a tenant record).
Destination resolution through one path. Both runners now resolve destinations via the base Runner::createBackupDestinations(), and BackupDestinationsService::getAvailableDestinations() is the single public entry point (the old BackupSchedulesService::getAvailableDestinations() is gone). Build any custom picker or API endpoint on top of that service rather than querying FilesystemConfiguration yourself, so "available" means the same thing everywhere.
Use the collection helpers for filtering instead of hand-written where('is_active', true) chains:
$configurations->getActive(); // active
$configurations->getValid(); // passes validation
$configurations->getActiveAndValid(); // what you usually want for a picker
$configurations->toDiskNames(); // names for the runner
Schedule destination scoping (the headline v3 feature). Cleanup schedules can be attached to specific filesystem configurations. In the store/update request, submit destination_ids:
'destination_ids' => ['array', 'max:255', new SelectableDestinationId()],
Semantics matter:
[]) — attaches nothing, so the schedule falls back to the app's configured destinations.SelectableDestinationId rule allows already-attached ids); a never-attached inactive id is rejected.Responder / view-data integration. Responder implementations now receive richer view data:
renderCreateCleanupSchedule(CreateCleanupScheduleViewData $data) — note the new argument, this is the breaking signature change.EditCleanupScheduleViewData and EditBackupScheduleViewData expose selectedDestinationIds.CreateCleanupScheduleData / UpdateCleanupScheduleData carry destination_ids to your form layer.Pre-select the picker from $data->selectedDestinationIds, and render the options from $data->configurations. Keep those two concerns separate — see the gotcha below.
Per-environment scheduling. Configure which environments run the scheduler and let the package resolve the list, rather than branching on app()->environment() in your own service provider:
'environments' => 'local, staging, production', // comma-separated, trimmed
Since v3.1.1 this string is explode()d properly with whitespace trimmed and empty segments discarded, so 'production, staging,' and ' production ,staging' both behave as expected.
Factories for tests. If you use BackupFactory::failed() or BackupFileFactory::uploadedFile() with closures, the closures now receive the factory instance, so state-dependent fakes are straightforward.
destination_ids pre-selection trap. Do not build the edit screen's pre-selection by filtering $data->configurations on is_active. That collection intentionally still contains attached-but-deactivated destinations so they remain visible in the picker. If you filter them out, the next save silently detaches them. Pre-select from selectedDestinationIds; render from configurations.destination_ids when the user actually interacted with the picker, otherwise an unrelated cron-expression edit will wipe the destination selection.^3.1. Prior to v3.1.1 the environment list was mangled (implode() instead of explode()), so a multi-environment value like 'staging, production' didn't produce the environments you expected — schedules either ran nowhere or in the wrong places. If your scheduler "worked in one env but not others", upgrade before you debug anything else. Empty entries and stray whitespace are now ignored, so trailing commas are safe (but don't rely on them).NoValidDisksException is a feature, not a bug. An explicitly empty disks array now throws instead of quietly backing up everything. If you pass a user-supplied disk list that could be empty, guard it before dispatching — the alternative (backing up every disk to every destination) is much worse./broadcasting/auth returning 403 after php artisan optimize, remove it.BackupFile::file_exists is cached per instance. It now reuses the same storage lookup for meta instead of hitting the disk twice. That's a performance win, but if you delete or upload a file and then read file_exists off an already-hydrated model in the same request, re-fetch the model rather than trusting the cached value.php artisan config:cache on top of an un-republished config file means jobs.* (and any new scheduler keys) simply don't exist — republish with --force, then php artisan config:clear before debugging anything else.phpstan.neon.dist ships with the package and the analyse/lint composer scripts have memory limits set. If you extend BackupFile, BackupMonitor, FilesystemConfigurationSFTP or the schedule models, mirror the documented @property annotations so your own PHPStan run doesn't regress.Runner::createBackupDestinations() (now on the base class, shared by backup and cleanup runners) if destination resolution needs app-specific rules — it keeps both runners consistent, which is exactly what the refactor was for.dragonmantank/cron-expression is constrained to ^3.6; if your app pins ^3.3 elsewhere, resolve it before upgrading or Composer will hand you a confusing conflict alongside the package upgrade.How can I help you explore Laravel packages today?