laravel-workkit/README.md

205 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[![Blax Software OSS](https://raw.githubusercontent.com/blax-software/laravel-workkit/master/art/oss-initiative-banner.svg)](https://github.com/blax-software)
# Laravel Workkit
[![PHP Version](https://img.shields.io/badge/php-%3E%3D8.0-blue)](https://php.net)
[![Laravel](https://img.shields.io/badge/laravel-10.x--13.x-orange)](https://laravel.com)
[![License](https://img.shields.io/badge/license-MIT-green)](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 (09) |
### 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>