205 lines
9.0 KiB
Markdown
205 lines
9.0 KiB
Markdown
[](https://github.com/blax-software)
|
||
|
||
# Laravel Workkit
|
||
|
||
[](https://php.net)
|
||
[](https://laravel.com)
|
||
[](LICENSE)
|
||
|
||
A Laravel collection of helpers and utilities to reduce redundant code over multiple projects.
|
||
|
||
## Features
|
||
|
||
- 💾 **Streaming DB backups** — `mysqldump | xz | openssl` in one pipe, APP_KEY-encrypted, zero DB bytes held in PHP memory (multi-GB safe)
|
||
- ♻️ **Safe restores** — `--fresh` wipes a dirty/partial schema before importing, after taking + verifying a recovery snapshot; failed imports never leave you stranded
|
||
- 🔎 **Backup verification** — `workkit:db:verify` proves 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:stats` prints per-model row counts, the largest tables, total DB size + a queue/backup summary
|
||
- 🩺 **Queue health** — `workkit:queue:health` reports 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
|
||
|
||
```bash
|
||
composer require blax-software/laravel-workkit
|
||
```
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
1. The **source backup is verified** before anything is touched — a file that
|
||
can't be decrypted never triggers a wipe.
|
||
2. 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).
|
||
3. Only then are all tables + views dropped — via the **same** `mysql` client
|
||
and database the import uses, so the wipe and the import provably hit the
|
||
same server (a `url` DSN / `unix_socket` / read-write split can't make them
|
||
diverge).
|
||
4. 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 `--fresh` and not just FK checks?** A leftover table with an
|
||
> incompatible column type makes MySQL raise `errno 150` / error `3780` at
|
||
> `CREATE TABLE` even with `FOREIGN_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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```php
|
||
Schedule::command('workkit:db:prune-backups')->daily();
|
||
```
|
||
|
||
### Configuration
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```php
|
||
Schedule::command('workkit:queue:health')->everyFiveMinutes();
|
||
```
|
||
|
||
## Changelog
|
||
|
||
See [CHANGELOG.md](CHANGELOG.md).
|
||
|
||
## Star History
|
||
|
||
<a href="https://www.star-history.com/?repos=blax-software%2Flaravel-workkit&type=date&legend=top-left">
|
||
<picture>
|
||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=blax-software/laravel-workkit&type=date&theme=dark&legend=top-left" />
|
||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=blax-software/laravel-workkit&type=date&legend=top-left" />
|
||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=blax-software/laravel-workkit&type=date&legend=top-left" />
|
||
</picture>
|
||
</a>
|