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

Laravel Prunekeeper Laravel Package

helgesverre/laravel-prunekeeper

PruneKeeper gives Laravel apps a central home for model pruning: define retention rules, schedule prune commands, and track what gets cleaned up so your database stays lean without scattered ad-hoc cleanup scripts or guesswork.

View on GitHub
Deep Wiki
Context7

Getting Started

This assessment is written against v2.0.0, the first stable release. Treat the static Prunekeeper::* API and the published config file as the supported surface — v2 deliberately follows the Laravel first-party package conventions (Cache::, Bus::, etc.), so anything you learned from pre-1.0 docs that used instance methods or fluent chaining should be discarded.

  1. Install and check extensions first. The default zip driver needs ext-zip; gzip needs ext-zlib; bzip2 needs ext-bz2; targz needs both ext-phar and ext-zlib. Only ext-zip/ext-zlib are commonly present, so verify your target runtime (local, CI image, prod container) before choosing a driver.
    composer require helgesverre/laravel-prunekeeper
    php -m | grep -E 'zip|zlib|bz2|phar'
    
  2. Publish the config and read it end to end. It is the primary documentation: it declares which models are prunable, the retention callbacks, export behaviour, and prunekeeper.compression.driver.
    php artisan vendor:publish --tag="prunekeeper-config"
    
  3. First use case — prune an append-only table with a backup. Take a activity_log / audit_entries / notifications table, scope it to rows older than 90 days, and export before deleting. Run it with the export enabled but deletion disabled (or against a local copy of prod data) so you can inspect the archive and confirm the row count matches expectations before the first real run.

The order to read code in: config/prunekeeper.php → the Artisan command signature (--compression, dry-run/export flags) → Prunekeeper::makePrunableQuery() if any of your models use SoftDeletes.

Implementation Patterns

Register prunable models in config with a query callback. The callback is where retention policy lives — keep it declarative and free of side effects, because v2 requires these callbacks to return void (they mutate the query they are handed, or resolve one via the helper below). Don't try to return anything for chaining.

Soft deletes: use Prunekeeper::makePrunableQuery() rather than hand-rolling scopes. It builds the base query with the soft-delete semantics applied, so you don't have to remember whether to add whereNull('deleted_at') or onlyTrashed(). If you need to hard-delete already-trashed rows after a grace period, that's a separate prunable entry — keep the two policies (soft-delete grace period vs. permanent purge) in distinct config blocks so their retention windows can't be confused.

Pick the driver per environment, and be explicit. zip locally (universally available, openable in Finder/Explorer), targz or gzip in production where archive size matters for object-storage cost. Set it in config per environment, but pin it in the scheduled command's --compression flag if a silent driver change would break downstream consumers of the archive filenames — the extension changes with the driver.

Schedule it, but don't let it run unobserved.

Schedule::command('prunekeeper:prune')
    ->dailyAt('02:30')
    ->withoutOverlapping()
    ->onOneServer()
    ->runInBackground();

withoutOverlapping matters: a prune run that exceeds its window and starts a second concurrent run is how you get double exports and lock contention on the same rows.

Export-to-storage, then delete, then verify. The pattern worth standardising: prune writes a compressed archive, you retain it for your compliance window (S3 lifecycle rule, etc.), and the DB delete happens after the archive is durably written. v2's "exporters clean up temp files on failure" means a failed export leaves no partial garbage on the local disk — but it also means a failed export should abort the delete, so assert that behaviour in a test rather than assuming it.

Test isolation is now a first-class concern. Prunekeeper::flushState() exists because the package holds static state. Call it in setUp() (or a tearDown()) of any test touching the package, and add a base-test-class hook if you have many:

protected function setUp(): void
{
    parent::setUp();
    Prunekeeper::flushState();
}

Without this you will get order-dependent failures where a prunable model registered in one test leaks into the next.

CI must install the extensions you actually use. Add ext-bz2 (and ext-phar) to the CI image if any environment is configured for bzip2/targz, otherwise the failure surfaces only in the prod deploy path.

Gotchas and Tips

  • Breaking change: config callbacks return void. Any pre-1.0 config that did return $query->where(...) or relied on fluent chaining ($this return) must be rewritten as a statement-only callback. This is the most likely source of a silent upgrade bug, because a returned query is simply ignored rather than erroring loudly.
  • Missing extensions fail at runtime, not install time. Composer won't stop you from configuring bzip2 on a host without ext-bz2; the failure appears at 02:30 in production. Consider an assertion in a service provider boot or a health check that validates the configured driver's requirements.
  • makePrunableQuery() is the soft-delete trap. If you bypass it and write a raw Model::query(), you will silently include or exclude trashed rows depending on the model's global scopes — and the bug looks like "we deleted the wrong number of rows," which is hard to notice until you need the archive.
  • Bulk deletes bypass Eloquent events. Pruning deletes in bulk, so deleting/deleted observers will not fire. Any audit trail, cache invalidation, search-index removal, or S3 cleanup hooked to those events will not run. Handle those explicitly (before/after the run, or via a queued reconciliation job).
  • Temp files are cleaned on failure, not on SIGKILL. An OOM kill or a container stop mid-export leaves the temp file behind. Monitor disk on the host running the export, and keep the prune working directory on a volume that isn't the smallest one in the stack.
  • --compression overrides config. Useful for one-off backfills, dangerous in cron: a flag left in a scheduled command silently diverges from the configured default and changes archive extensions downstream.
  • Chunk your deletes. A single DELETE over millions of rows holds locks and bloats the replication stream. If the package doesn't chunk internally, scope by ID ranges or date windows and run several smaller prunes.
  • Verify the query before the first run. Wrap a count() around the same scoping logic (or use the package's dry-run mode) and compare against a manual SELECT COUNT(*) — off-by-one timezone handling between now() in app timezone and the DB column is the classic way to prune one extra day of data.
  • Foreign keys and cascades. Test pruning a model whose children reference it with cascadeOnDelete in a realistic dataset; the delete can succeed on the parent and surprise you at the child level, or fail entirely on restrict.
  • Extension point to watch: if you need a custom exporter or storage target, the v2 static registration pattern is where to hang it — but any custom callback registered there inherits the void return contract.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib