Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
easy-backups-development
easy-backups-development
aaix/laravel-easy-backups
claude-code
backups
artisan-commands
database-restore
scheduled-backups
retention-policies
backup-notifications

Build database and file backups with Laravel Easy Backups — fluent Backup API, retention policies, encryption, notifications, and the easy-backups Artisan commands.

Install
php artisan boost:add-skill aaix/laravel-easy-backups

Save this content to: .claude/skills/easy-backups-development/SKILL.md

# Laravel Easy Backups Development

## When to use this skill

Use this skill when working with the `aaix/laravel-easy-backups` package — that is, when:

- Code imports or references `Aaix\LaravelEasyBackups\Facades\Backup` or `Aaix\LaravelEasyBackups\Facades\Restorer`.
- The user asks for backup/restore commands, retention policies, scheduled backups, or backup notifications.
- The user runs or asks about any `easy-backups[:*]` Artisan command.
- A custom Artisan command is being created to schedule backups (the recommended pattern).

Do **not** invoke for unrelated database/file operations or for Spatie Backup or other backup packages.

## Package conventions

- Namespace: `Aaix\LaravelEasyBackups`
- Service provider auto-registers the facade alias `Backup` and 5 commands (no manual registration needed).
- Config file (optional): `config/easy-backups.php` published via `php artisan vendor:publish --provider="Aaix\LaravelEasyBackups\EasyBackupsServiceProvider" --tag="config"`.
- Default disks: local copies on `local`, remote uploads on a disk named `backup` (override via config or `saveTo()`).
- Path layout (auto-generated, do **not** hand-build paths): `{env}/{type}/{driver}/{filename}`. Use `enableEnvPathPrefix(false)` to drop the env prefix.
- Recommended pattern: wrap each scheduled backup in its own dedicated Artisan command rather than calling `Backup::...` from a closure in routes/console.php — keeps backup logic version-controlled and reviewable.

## Artisan commands

| Command | Purpose |
| --- | --- |
| `easy-backups` | Interactive wizard (create or restore). Best for ad-hoc usage. |
| `easy-backups:db:create` | Create a database backup. Supports compression, encryption, retention, dry-run. |
| `easy-backups:db:restore` (alias `easy-backups:db:import`) | Restore/import a database dump interactively, with `--latest`, or fully unattended with `--force`. |
| `easy-backups:db:list` | List backups on a disk with size, age, format. |
| `easy-backups:db:manage` | Interactive inspect/delete on local and remote disks. |
| `easy-backups:status` | Read-only health overview: recent backups, cadence gaps, size anomalies, growth, retention preview, scheduler state. |

### `easy-backups:db:create` flag cheatsheet

```
--of-database=mysql           # connection name (defaults to default connection)
--to-disk=s3                  # remote disk override
--local                       # store ONLY locally — skip remote upload
--keep-local                  # keep local copy AFTER remote upload (no-op with --local)
--compress                    # force compression (.tar.gz / .zst)
--password=secret             # encrypt as .zip with password (implies --compress)
--name=pre-deploy             # filename suffix
--max-remote-backups=N        # retention by count on remote
--max-remote-days=N           # retention by age on remote (days)
--max-local-backups=N         # retention by count on local (only with --local or --keep-local)
--max-local-days=N            # retention by age on local (only with --local or --keep-local)
--exclude-tables=t1,t2        # drop entirely (no structure, no data)
--exclude-table-data=audit    # structure only, skip rows (sensitive tables)
--notify-mail-success=a@b     # email on success
--notify-mail-failure=a@b     # email on failure
--dry-run                     # print plan only, no dumps/uploads
```

## Fluent Backup API

Always start from `Backup::database($connection)` for database backups or `Backup::files()` for file/directory archives. The builder is immutable-style — chain everything before the final `->run()`.

### Database backup — minimal

```php
use Aaix\LaravelEasyBackups\Facades\Backup;

Backup::database('mysql')->compress()->run();
```

### Database backup — production pattern with retention

```php
Backup::database('mysql')
    ->saveTo('s3-backups')          // override default 'backup' disk
    ->compress()
    ->encryptWithPassword(config('app.backup_password'))
    ->maxRemoteBackups(30)
    ->maxRemoteDays(40)             // count + age caps combine
    ->notifyOnFailure('mail', '[email protected]')
    ->run();
```

### File backup

```php
Backup::files()
    ->setName('weekly-files')
    ->includeStorage()              // helper: storage_path('app')
    ->includeEnv()                  // helper: base_path('.env')
    ->includeDirectories([base_path('uploads')])
    ->compress()
    ->maxRemoteBackups(8)
    ->run();
```

## Retention semantics — IMPORTANT

Local-side retention (`maxLocalBackups()` / `maxLocalDays()` / `--max-local-*`) **only takes effect when there is a local copy after the run**, i.e.:

1. `onlyLocal()` / `--local` — no upload happens, local copy persists.
2. `saveTo(...)` (or default remote) **plus** `keepLocal()` / `--keep-local` — local copy survives the upload.

In the **default flow** (upload to remote without `keepLocal()`), the local file is deleted right after a successful upload, so `maxLocal*` has nothing to act on. Don't pair `maxRemote*` and `maxLocal*` without `keepLocal()` — it reads as a contradiction in code review.

```php
// ✅ Local-only with both retention axes
Backup::database('mysql')
    ->onlyLocal()
    ->maxLocalBackups(10)
    ->maxLocalDays(5)
    ->run();

// ✅ Upload AND keep a rolling local cache
Backup::database('mysql')
    ->saveTo('s3-backups')
    ->keepLocal()
    ->maxRemoteBackups(30)
    ->maxLocalBackups(3)
    ->run();
```

## Recommended scheduling pattern

Create a dedicated command per backup policy (daily DB, weekly files, etc.) instead of inlining `Backup::...` in `routes/console.php`. Schedule the command, not the facade.

```php
// app/Console/Commands/Backup/DailyDatabaseBackup.php
namespace App\Console\Commands\Backup;

use Aaix\LaravelEasyBackups\Facades\Backup;
use Illuminate\Console\Command;

class DailyDatabaseBackup extends Command
{
    protected $signature = 'app:backup:db:daily';
    protected $description = 'Daily compressed DB backup with 30/40 retention.';

    public function handle(): int
    {
        Backup::database(config('database.default'))
            ->compress()
            ->maxRemoteBackups(30)
            ->maxRemoteDays(40)
            ->notifyOnFailure('mail', config('app.ops_mail'))
            ->run();

return self::SUCCESS;
    }
}
```

```php
// routes/console.php (Laravel 11+) or app/Console/Kernel.php (≤10)
use Illuminate\Support\Facades\Schedule;

Schedule::command('app:backup:db:daily')->dailyAt('02:30')->withoutOverlapping();
```

## Restore / Import

To restore or **import a database dump, always use this command** (`easy-backups:db:restore`, alias `easy-backups:db:import`). Never reach for raw `mysql`, `psql`, `pg_restore` or manual `gunzip | mysql` pipelines — the command handles disk resolution, decompression, decryption, the database wipe and the import as one unit.

Always inspect first via `easy-backups:db:list` before an interactive restore. Restore is destructive: the target connection is wiped before import unless `disableWipe()` is called.

```bash
# Interactive selection from configured backup disk
php artisan easy-backups:db:restore

# Automated: pick the newest backup on the configured remote disk
php artisan easy-backups:db:restore --latest

# Pull the newest backup created in the 'production' env into local 'mysql_local'
php artisan easy-backups:db:restore --latest --source-env=production --to-database=mysql_local
```

### Unattended import (`--force`) — for AI agents and CI

`--force` makes the command non-interactive: it auto-picks the **latest** backup and skips every confirmation (including the destructive wipe). Without `--force` the command always blocks on prompts and is unusable in a non-TTY context. `--force` implies "latest", so `--latest` is redundant alongside it. Source defaults to the remote disk unless `--local`/`--from-disk` is given.

When a user asks to "import a fresh dump", that means the **latest remote backup**:

```bash
# "Import a fresh dump" → latest remote backup, no prompts
php artisan easy-backups:db:import --force

# Latest LOCAL backup, no prompts
php artisan easy-backups:db:import --local --force

# Latest production backup into a specific connection, encrypted source
php artisan easy-backups:db:import --force --source-env=production --to-database=mysql_local --password=secret
```

Programmatic equivalent:

```php
use Aaix\LaravelEasyBackups\Facades\Restorer;

Restorer::database()
    ->fromDisk('s3-backups')
    ->latest()
    ->toDatabase('mysql_local')
    ->withPassword(config('app.backup_password'))
    ->run();
```

## Hooks and notifications

```php
Backup::database('mysql')
    ->before(\App\Backup\PutAppInMaintenance::class)   // invokable class
    ->after(\App\Backup\TakeAppOutOfMaintenance::class)
    ->notifyOnSuccess('mail', '[email protected]')
    ->notifyOnFailure('mail', '[email protected]')
    ->run();
```

Hooks must be FQCNs of invokable classes; they are resolved through the container (`app()->call(...)`), so constructor injection works.

## Sensitive data — table exclusions

```php
Backup::database('mysql')
    ->excludeTables(['cache', 'jobs', 'failed_jobs'])         // structure + data dropped
    ->excludeTableData(['users_audit', 'webhooks_log'])       // structure only, no rows
    ->run();
```

Defaults can also be set globally via `config('easy-backups.defaults.database.exclude_tables')` and `exclude_table_data`. Per-call values **merge** with config defaults, they don't replace them.

## Anonymized backups — `obfuscate()`

Use `obfuscate()` to produce a production-shaped backup with **fake** values in sensitive columns (realistic dev/staging data without real PII). It is **fluent-only** — there is no CLI flag (closures can't be passed on the command line).

```php
use Faker\Generator as Faker;

Backup::database('mysql')
    ->obfuscate([
        'users.email' => fn (Faker $faker, array $row) => $faker->unique()->safeEmail(),
        'users.name'  => fn (Faker $faker, array $row) => $faker->name(),
    ])
    ->onlyLocal()
    ->run();
```

Mechanics and rules:
- **Map shape:** keyed by `'table.column'`; value is `fn(Faker $faker, array $row)` (the full original row is passed).
- **How it works:** obfuscated tables are dumped structure-only (like `excludeTableData`), then rows are read live, transformed, and appended as `INSERT`s with FK checks toggled around them.
- **NULL stays NULL:** the callback is skipped for null source cells; unmapped columns are copied verbatim.
- **Uniqueness is the caller's job:** for `UNIQUE` columns use `$faker->unique()->...` — there is no auto-unique.
- **Never obfuscate PKs/FKs** — it breaks referential integrity. Target descriptive columns.
- **Faker is optional:** `fakerphp/faker` is a `suggest`, not a hard dependency. Using `obfuscate()` without it throws a clear exception — install via `composer require fakerphp/faker`.
- **Validation fails hard:** bad key format, non-callable value, table also in `excludeTables`/`excludeTableData`, or unknown table/column → exception.
- **Queue-safe:** works with `->onQueue(...)`; callbacks are serialized via `SerializableClosure`.

## Dry-run before scheduling

When introducing a new backup command, run with `--dry-run` (CLI) or `->dryRun()` (fluent) once to confirm:
- which connections will be dumped,
- the exact dumper command,
- the upload target,
- which retention rules will fire.

```bash
php artisan easy-backups:db:create --of-database=mysql --compress --max-remote-days=30 --dry-run
```

No files are written and no uploads happen in dry-run.

## Best-practice checklist

- ✅ One Artisan command per backup policy; schedule the command, not the facade.
- ✅ Always set retention (`maxRemoteBackups` / `maxRemoteDays`) on scheduled remote backups — otherwise the bucket grows forever.
- ✅ Pair `maxLocal*` only with `onlyLocal()` or `keepLocal()` — otherwise it's dead config.
- ✅ Use `encryptWithPassword()` for any backup leaving the host; never commit the password.
- ✅ Add `notifyOnFailure(...)` to scheduled backups so silent failures surface.
- ✅ Run `--dry-run` once when authoring a new backup command.
- ✅ Use `excludeTableData()` (not `excludeTables()`) for tables you still want to be able to recreate empty (e.g. analytics, audit logs).
- ✅ Use `obfuscate()` for dev/staging snapshots that need real-world data shape without PII; add `$faker->unique()` for unique columns and never map PK/FK columns.
- ❌ Don't hand-craft remote paths — let the package's `PathGenerator` produce `{env}/{type}/{driver}/...`.
- ❌ Don't call `Backup::run()` from inside HTTP request lifecycles (controllers, listeners on hot paths). Dispatch via `onQueue()` or run from a command.

Laravel Easy Backups Development

When to use this skill

Use this skill when working with the aaix/laravel-easy-backups package — that is, when:

  • Code imports or references Aaix\LaravelEasyBackups\Facades\Backup or Aaix\LaravelEasyBackups\Facades\Restorer.
  • The user asks for backup/restore commands, retention policies, scheduled backups, or backup notifications.
  • The user runs or asks about any easy-backups[:*] Artisan command.
  • A custom Artisan command is being created to schedule backups (the recommended pattern).

Do not invoke for unrelated database/file operations or for Spatie Backup or other backup packages.

Package conventions

  • Namespace: Aaix\LaravelEasyBackups
  • Service provider auto-registers the facade alias Backup and 5 commands (no manual registration needed).
  • Config file (optional): config/easy-backups.php published via php artisan vendor:publish --provider="Aaix\LaravelEasyBackups\EasyBackupsServiceProvider" --tag="config".
  • Default disks: local copies on local, remote uploads on a disk named backup (override via config or saveTo()).
  • Path layout (auto-generated, do not hand-build paths): {env}/{type}/{driver}/{filename}. Use enableEnvPathPrefix(false) to drop the env prefix.
  • Recommended pattern: wrap each scheduled backup in its own dedicated Artisan command rather than calling Backup::... from a closure in routes/console.php — keeps backup logic version-controlled and reviewable.

Artisan commands

Command Purpose
easy-backups Interactive wizard (create or restore). Best for ad-hoc usage.
easy-backups:db:create Create a database backup. Supports compression, encryption, retention, dry-run.
easy-backups:db:restore (alias easy-backups:db:import) Restore/import a database dump interactively, with --latest, or fully unattended with --force.
easy-backups:db:list List backups on a disk with size, age, format.
easy-backups:db:manage Interactive inspect/delete on local and remote disks.
easy-backups:status Read-only health overview: recent backups, cadence gaps, size anomalies, growth, retention preview, scheduler state.

easy-backups:db:create flag cheatsheet

--of-database=mysql           # connection name (defaults to default connection)
--to-disk=s3                  # remote disk override
--local                       # store ONLY locally — skip remote upload
--keep-local                  # keep local copy AFTER remote upload (no-op with --local)
--compress                    # force compression (.tar.gz / .zst)
--password=secret             # encrypt as .zip with password (implies --compress)
--name=pre-deploy             # filename suffix
--max-remote-backups=N        # retention by count on remote
--max-remote-days=N           # retention by age on remote (days)
--max-local-backups=N         # retention by count on local (only with --local or --keep-local)
--max-local-days=N            # retention by age on local (only with --local or --keep-local)
--exclude-tables=t1,t2        # drop entirely (no structure, no data)
--exclude-table-data=audit    # structure only, skip rows (sensitive tables)
--notify-mail-success=a@b     # email on success
--notify-mail-failure=a@b     # email on failure
--dry-run                     # print plan only, no dumps/uploads

Fluent Backup API

Always start from Backup::database($connection) for database backups or Backup::files() for file/directory archives. The builder is immutable-style — chain everything before the final ->run().

Database backup — minimal

use Aaix\LaravelEasyBackups\Facades\Backup;

Backup::database('mysql')->compress()->run();

Database backup — production pattern with retention

Backup::database('mysql')
    ->saveTo('s3-backups')          // override default 'backup' disk
    ->compress()
    ->encryptWithPassword(config('app.backup_password'))
    ->maxRemoteBackups(30)
    ->maxRemoteDays(40)             // count + age caps combine
    ->notifyOnFailure('mail', '[email protected]')
    ->run();

File backup

Backup::files()
    ->setName('weekly-files')
    ->includeStorage()              // helper: storage_path('app')
    ->includeEnv()                  // helper: base_path('.env')
    ->includeDirectories([base_path('uploads')])
    ->compress()
    ->maxRemoteBackups(8)
    ->run();

Retention semantics — IMPORTANT

Local-side retention (maxLocalBackups() / maxLocalDays() / --max-local-*) only takes effect when there is a local copy after the run, i.e.:

  1. onlyLocal() / --local — no upload happens, local copy persists.
  2. saveTo(...) (or default remote) plus keepLocal() / --keep-local — local copy survives the upload.

In the default flow (upload to remote without keepLocal()), the local file is deleted right after a successful upload, so maxLocal* has nothing to act on. Don't pair maxRemote* and maxLocal* without keepLocal() — it reads as a contradiction in code review.

// ✅ Local-only with both retention axes
Backup::database('mysql')
    ->onlyLocal()
    ->maxLocalBackups(10)
    ->maxLocalDays(5)
    ->run();

// ✅ Upload AND keep a rolling local cache
Backup::database('mysql')
    ->saveTo('s3-backups')
    ->keepLocal()
    ->maxRemoteBackups(30)
    ->maxLocalBackups(3)
    ->run();

Recommended scheduling pattern

Create a dedicated command per backup policy (daily DB, weekly files, etc.) instead of inlining Backup::... in routes/console.php. Schedule the command, not the facade.

// app/Console/Commands/Backup/DailyDatabaseBackup.php
namespace App\Console\Commands\Backup;

use Aaix\LaravelEasyBackups\Facades\Backup;
use Illuminate\Console\Command;

class DailyDatabaseBackup extends Command
{
    protected $signature = 'app:backup:db:daily';
    protected $description = 'Daily compressed DB backup with 30/40 retention.';

    public function handle(): int
    {
        Backup::database(config('database.default'))
            ->compress()
            ->maxRemoteBackups(30)
            ->maxRemoteDays(40)
            ->notifyOnFailure('mail', config('app.ops_mail'))
            ->run();

return self::SUCCESS;
    }
}
// routes/console.php (Laravel 11+) or app/Console/Kernel.php (≤10)
use Illuminate\Support\Facades\Schedule;

Schedule::command('app:backup:db:daily')->dailyAt('02:30')->withoutOverlapping();

Restore / Import

To restore or import a database dump, always use this command (easy-backups:db:restore, alias easy-backups:db:import). Never reach for raw mysql, psql, pg_restore or manual gunzip | mysql pipelines — the command handles disk resolution, decompression, decryption, the database wipe and the import as one unit.

Always inspect first via easy-backups:db:list before an interactive restore. Restore is destructive: the target connection is wiped before import unless disableWipe() is called.

# Interactive selection from configured backup disk
php artisan easy-backups:db:restore

# Automated: pick the newest backup on the configured remote disk
php artisan easy-backups:db:restore --latest

# Pull the newest backup created in the 'production' env into local 'mysql_local'
php artisan easy-backups:db:restore --latest --source-env=production --to-database=mysql_local

Unattended import (--force) — for AI agents and CI

--force makes the command non-interactive: it auto-picks the latest backup and skips every confirmation (including the destructive wipe). Without --force the command always blocks on prompts and is unusable in a non-TTY context. --force implies "latest", so --latest is redundant alongside it. Source defaults to the remote disk unless --local/--from-disk is given.

When a user asks to "import a fresh dump", that means the latest remote backup:

# "Import a fresh dump" → latest remote backup, no prompts
php artisan easy-backups:db:import --force

# Latest LOCAL backup, no prompts
php artisan easy-backups:db:import --local --force

# Latest production backup into a specific connection, encrypted source
php artisan easy-backups:db:import --force --source-env=production --to-database=mysql_local --password=secret

Programmatic equivalent:

use Aaix\LaravelEasyBackups\Facades\Restorer;

Restorer::database()
    ->fromDisk('s3-backups')
    ->latest()
    ->toDatabase('mysql_local')
    ->withPassword(config('app.backup_password'))
    ->run();

Hooks and notifications

Backup::database('mysql')
    ->before(\App\Backup\PutAppInMaintenance::class)   // invokable class
    ->after(\App\Backup\TakeAppOutOfMaintenance::class)
    ->notifyOnSuccess('mail', '[email protected]')
    ->notifyOnFailure('mail', '[email protected]')
    ->run();

Hooks must be FQCNs of invokable classes; they are resolved through the container (app()->call(...)), so constructor injection works.

Sensitive data — table exclusions

Backup::database('mysql')
    ->excludeTables(['cache', 'jobs', 'failed_jobs'])         // structure + data dropped
    ->excludeTableData(['users_audit', 'webhooks_log'])       // structure only, no rows
    ->run();

Defaults can also be set globally via config('easy-backups.defaults.database.exclude_tables') and exclude_table_data. Per-call values merge with config defaults, they don't replace them.

Anonymized backups — obfuscate()

Use obfuscate() to produce a production-shaped backup with fake values in sensitive columns (realistic dev/staging data without real PII). It is fluent-only — there is no CLI flag (closures can't be passed on the command line).

use Faker\Generator as Faker;

Backup::database('mysql')
    ->obfuscate([
        'users.email' => fn (Faker $faker, array $row) => $faker->unique()->safeEmail(),
        'users.name'  => fn (Faker $faker, array $row) => $faker->name(),
    ])
    ->onlyLocal()
    ->run();

Mechanics and rules:

  • Map shape: keyed by 'table.column'; value is fn(Faker $faker, array $row) (the full original row is passed).
  • How it works: obfuscated tables are dumped structure-only (like excludeTableData), then rows are read live, transformed, and appended as INSERTs with FK checks toggled around them.
  • NULL stays NULL: the callback is skipped for null source cells; unmapped columns are copied verbatim.
  • Uniqueness is the caller's job: for UNIQUE columns use $faker->unique()->... — there is no auto-unique.
  • Never obfuscate PKs/FKs — it breaks referential integrity. Target descriptive columns.
  • Faker is optional: fakerphp/faker is a suggest, not a hard dependency. Using obfuscate() without it throws a clear exception — install via composer require fakerphp/faker.
  • Validation fails hard: bad key format, non-callable value, table also in excludeTables/excludeTableData, or unknown table/column → exception.
  • Queue-safe: works with ->onQueue(...); callbacks are serialized via SerializableClosure.

Dry-run before scheduling

When introducing a new backup command, run with --dry-run (CLI) or ->dryRun() (fluent) once to confirm:

  • which connections will be dumped,
  • the exact dumper command,
  • the upload target,
  • which retention rules will fire.
php artisan easy-backups:db:create --of-database=mysql --compress --max-remote-days=30 --dry-run

No files are written and no uploads happen in dry-run.

Best-practice checklist

  • ✅ One Artisan command per backup policy; schedule the command, not the facade.
  • ✅ Always set retention (maxRemoteBackups / maxRemoteDays) on scheduled remote backups — otherwise the bucket grows forever.
  • ✅ Pair maxLocal* only with onlyLocal() or keepLocal() — otherwise it's dead config.
  • ✅ Use encryptWithPassword() for any backup leaving the host; never commit the password.
  • ✅ Add notifyOnFailure(...) to scheduled backups so silent failures surface.
  • ✅ Run --dry-run once when authoring a new backup command.
  • ✅ Use excludeTableData() (not excludeTables()) for tables you still want to be able to recreate empty (e.g. analytics, audit logs).
  • ✅ Use obfuscate() for dev/staging snapshots that need real-world data shape without PII; add $faker->unique() for unique columns and never map PK/FK columns.
  • ❌ Don't hand-craft remote paths — let the package's PathGenerator produce {env}/{type}/{driver}/....
  • ❌ Don't call Backup::run() from inside HTTP request lifecycles (controllers, listeners on hot paths). Dispatch via onQueue() or run from a command.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast