blax-software / laravel-workkit
Laravel collection of helpers and utilities to reduce redundant code over multiple projects.
Requires
- php: >=8.0
- laravel/framework: *
- laravel/sanctum: *
- spatie/once: *
Requires (Dev)
- laravel/pint: ^1.22
This package is auto-updated.
Last update: 2026-07-28 09:22:12 UTC
README
Laravel Workkit
A Laravel collection of helpers and utilities to reduce redundant code over multiple projects.
Features
- ๐พ Streaming DB backups โ
mysqldump | xz | opensslin one pipe, APP_KEY-encrypted, zero DB bytes held in PHP memory (multi-GB safe) - โป๏ธ Safe restores โ
--freshwipes a dirty/partial schema before importing, after taking + verifying a recovery snapshot; failed imports never leave you stranded - ๐ Backup verification โ
workkit:db:verifyproves a backup decrypts + is a complete xz stream without a database, so "is it corrupt?" gets a real answer - ๐งน Retention pruning โ age-based cleanup that always keeps the N newest, so prune can never delete your last recovery point
- ๐ Stats overview โ
workkit:statsprints per-model row counts, the largest tables, total DB size + a queue/backup summary - ๐ฉบ Queue health โ
workkit:queue:healthreports per-queue depth/age/failed jobs and exits non-zero on breach (cron-friendly) - ๐ Variable pagination โ a
#[VariablePaginatable]attribute +request()->perPage()macro for per-route, user-overridable page sizes - ๐งฉ Reusable middleware & traits โ bearer-token auth, force-JSON responses,
HasExpiration,HasMeta,HasMetaTranslation, and small service helpers
Quick Start
composer require blax-software/laravel-workkit
php artisan workkit:db:backup # storage/backups/db_mysql_<timestamp>.sql.xz.enc php artisan workkit:db:verify # is the newest backup intact? php artisan workkit:db:restore --fresh # wipe + restore the newest backup
Backups are encrypted with a key derived from your app's APP_KEY; a backup is
restorable only by a deployment that knows the same APP_KEY.
Database Backups
A streaming, compressed, encrypted MySQL backup/restore suite. The dump, compression and encryption happen in a single shell pipe, so PHP never holds the database content in memory regardless of dump size.
Commands
| Command | What it does |
|---|---|
workkit:db:backup |
Stream mysqldump โ xz โ openssl into storage/backups, write a .meta.json sidecar (size, sha256, params). |
workkit:db:restore |
Stream openssl โ xz โ mysql. Verifies the source first; --fresh wipes the target after a verified safety snapshot. |
workkit:db:verify |
Prove a backup decrypts and is a complete xz stream โ no database touched. |
workkit:db:prune-backups |
Delete backups older than the retention window, always keeping the N newest. |
Backup
php artisan workkit:db:backup # default connection php artisan workkit:db:backup --connection=mysql --xz-level=6 php artisan workkit:db:backup --verify # confirm the backup right after writing
Restore
# Newest restorable backup into the default connection (prompts unless --force) php artisan workkit:db:restore # A specific file php artisan workkit:db:restore --file=db_mysql_2026-06-28_09-21-49.sql.xz.enc # Wipe the target first โ the ONLY thing that fixes a restore failing with # errno 150 / error 3780 against a dirty or partially-migrated schema. php artisan workkit:db:restore --fresh
--fresh is deliberately careful, because dropping every table is irreversible:
- The source backup is verified before anything is touched โ a file that can't be decrypted never triggers a wipe.
- A pre-restore safety snapshot of the current database is taken and
verified (skip with
--no-safety-backup; the restore aborts if the snapshot can't be written, rather than wiping with no recovery point). - Only then are all tables + views dropped โ via the same
mysqlclient and database the import uses, so the wipe and the import provably hit the same server (aurlDSN /unix_socket/ read-write split can't make them diverge). - If the import fails after the wipe, the command prints the exact
workkit:db:restore --file=<snapshot>command to recover.
Interactively, --fresh asks you to type the database name to confirm; under
--force it proceeds non-interactively for deploy scripts.
Why
--freshand not just FK checks? A leftover table with an incompatible column type makes MySQL raiseerrno 150/ error3780atCREATE TABLEeven withFOREIGN_KEY_CHECKS=0(which restores already set). The only fix is an empty target. If a restore fails this way, the command tells you to re-run with--fresh.
Verify
php artisan workkit:db:verify # newest backup php artisan workkit:db:verify --file=db_mysql_2026-06-28_09-21-49.sql.xz.enc php artisan workkit:db:verify --all # every backup in the directory
Verify proves integrity (decrypts with this host's APP_KEY + a complete,
non-truncated xz stream), flags a missing mysqldump completion marker
(source-truncation), and compares the on-disk sha256 against the .meta.json
sidecar. It does not prove restorability โ a valid backup of the wrong or
empty database still verifies "OK".
Prune
php artisan workkit:db:prune-backups --dry-run php artisan workkit:db:prune-backups --days=30 --keep-min=5
The --keep-min floor (default 5) is always kept regardless of age, so an
aggressive --days can never leave you with zero backups. Schedule it:
Schedule::command('workkit:db:prune-backups')->daily();
Configuration
php artisan vendor:publish --tag=workkit-config
| Key | Env | Default | Purpose |
|---|---|---|---|
backup.path |
WORKKIT_BACKUP_PATH |
storage/backups |
Where backups live |
backup.retention_days |
WORKKIT_BACKUP_RETENTION_DAYS |
30 |
Age cutoff for prune |
backup.retention_min_keep |
WORKKIT_BACKUP_RETENTION_MIN_KEEP |
5 |
Newest backups always kept |
backup.xz_level |
WORKKIT_BACKUP_XZ_LEVEL |
3 |
xz compression level (0โ9) |
Requirements
The host needs mysqldump, mysql, xz, openssl and bash on PATH โ
standard on any reasonable Linux server. A non-empty APP_KEY is required
(backups are unrecoverable without the key that produced them).
Restoring without the package
The output is plain openssl enc -salt format, so any host with the same
APP_KEY can restore it directly:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -pass env:WK_KEY \ -in db_mysql_2026-06-28_09-21-49.sql.xz.enc | xz -d | mysql <database> # WK_KEY = your APP_KEY with the "base64:" prefix stripped
Ops & Observability
Stats overview
php artisan workkit:stats # models + largest tables + DB size + queue/backup summary php artisan workkit:stats --exact # true COUNT(*) per table (heavier) instead of the estimate php artisan workkit:stats --models --json # just per-model counts, machine-readable php artisan workkit:stats --tables --limit=30
Model row counts are estimated from information_schema on MySQL (instant);
pass --exact for a real COUNT(*). Models are auto-discovered from
app/Models โ override with config('workkit.stats.models') (an explicit FQCN
list) or models_path. Table/DB sizes are MySQL-only and degrade to "n/a"
elsewhere.
Queue health
php artisan workkit:queue:health # human table; exits 0 / non-zero php artisan workkit:queue:health --json || notify-ops # cron / monitoring probe php artisan workkit:queue:health --max-age=10 --max-depth=500 --max-failed=50
For the database queue driver it reports per-queue pending / due / delayed /
reserved counts and the oldest due job's age, plus failed jobs (total +
last N hours). It flags a queue STALLED โ due work older than
max_age_minutes with nothing reserved (the worker is probably down) โ and
exits non-zero on any breach so it slots straight into cron or a health
probe. Thresholds live under config('workkit.queue.*'), with per-queue depth
overrides for intentionally-slow queues. Schedule it:
Schedule::command('workkit:queue:health')->everyFiveMinutes();
Changelog
See CHANGELOG.md.