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

Reanimate Laravel Package

mpociot/reanimate

Reanimate adds a recycle bin to Laravel Eloquent models. Use the Reanimate trait to delete records and restore them later without adding soft-delete columns to your tables—deleted models are stored separately, keeping your schema clean while deletions stay undoable.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source first — it's small. The whole package is roughly a trait plus a restore loop. Before you design anything on top of it, open src/ and confirm three things: the exact trait name and namespace (Mpociot\Reanimate\Reanimate vs. something else), whether cascade relations are declared in an $reanimate property or discovered reflectively, and whether it wraps native SoftDeletes or persists attribute snapshots. That last point is the one that changes your risk profile — a snapshot mechanism means a second copy of your data with its own retention and erasure obligations. Five minutes of reading saves a week of assumption.

Check the constraints before you composer require. Look at composer.json for illuminate/* and PHP ranges and at the last commit date. This was authored in an earlier Laravel era; on Laravel 10+ you may be installing under --ignore-platform-reqs (don't) or vendoring/forking it (fine — it's tens of lines, not hundreds).

composer require mpociot/reanimate
php artisan vendor:publish --provider="Mpociot\Reanimate\ReanimateServiceProvider"
php artisan route:list | grep -i reanimate   # does it register routes at all?

Opt in an explicit allowlist of models. Never put the trait on a shared base model. The capability should exist only where you've consciously decided "delete is reversible here."

class Invoice extends Model
{
    use SoftDeletes;
    use \Mpociot\Reanimate\Reanimate;

    // confirm the property name in source; this is the canonical pattern
    protected $reanimate = ['lineItems', 'attachments'];
}

First use case: one guarded admin action, not a feature. Wire it to a single controller method behind your existing auth and policies. Don't expose package routes directly — they have no opinion about authorization, and the package's entire job is surfacing records you deliberately hid.

Route::post('/admin/invoices/{invoice}/restore', [InvoiceController::class, 'restore'])
    ->middleware(['auth', 'can:restore,invoice'])
    ->withTrashed();   // route model binding ignores trashed rows otherwise
public function restore(Invoice $invoice)
{
    abort_unless($invoice->trashed(), 409);

    DB::transaction(fn () => $invoice->reanimate());

    return back()->with('status', 'Invoice restored.');
}

That's the whole first slice. Ship it, watch the audit log, then decide whether you need the cascade at all.

Implementation Patterns

Wrap every restore in a transaction. A cascade touches a parent plus N children; a partial cascade leaves you in a state nobody designed for. DB::transaction() is the cheapest insurance in the whole integration.

DB::transaction(fn () => $invoice->reanimate());

Authorize at the boundary, not in the model. Three checks, in this order: is the actor allowed to restore this type (policy), is this record in the actor's tenant (explicit scope, not just a global scope), and is the record actually trashed (409, not a silent success). The package supplies none of these.

$invoice = Invoice::withTrashed()
    ->where('tenant_id', $tenant->id)
    ->findOrFail($id);

Hook restored for audit and downstream repair. A restore is a production mutation, not a UI nicety. One listener records actor, model, key, timestamp, request ID — and repairs everything that already treated the delete as final: search index re-entry, cache busting, denormalized counters, outbound webhooks to consumers.

Event::listen('eloquent.restored: *', function ($event, $models) {
    $model = $models[0];
    AuditLog::record('restored', $model, ['actor_id' => auth()->id()]);
    SearchIndex::reindex($model);
    Cache::tags([$model->cacheTag()])->flush();
});

Define cascade semantics explicitly, per relationship. "Restoring an order restores its soft-deleted line items" is a product decision, not a package default. Be deliberate about ordering (children first if FKs matter), and decide up front what happens when a child was hard-deleted or independently restored in the interim. Write a test for each of those three cases.

Bulk restore: loop with per-record outcomes, don't batch. All-or-nothing bulk restore sounds tidy until one row collides with a unique constraint and reverts 200 good restores. Return a partial-failure payload the UI can render.

$results = collect($ids)->map(fn ($id) => rescue(
    fn () => tap(
        Invoice::withTrashed()->findOrFail($id),
        fn ($m) => DB::transaction(fn () => $m->reanimate())
    ),
    fn ($e) => ['id' => $id, 'error' => $e->getMessage()],
    report: true
));

Mirror the cascade on delete. If restoring a parent brings back children, deleting a parent should soft-delete them too. Asymmetric cascade logic is how "delete means X except when it means Y" creeps into a codebase and never leaves.

Eager-load everything the cascade touches. Relation-by-relation restore loops are N+1 generators by construction. Invoice::withTrashed()->with(['lineItems' => fn ($q) => $q->withTrashed()]) before calling reanimate() keeps it to a handful of queries.

Cover it in CI, one feature test per releasable model. Create → delete → restore → assert deleted_at is null, children are back, and the audit row exists. This is also your regression net for the day you replace the package with in-house code.

Gotchas and Tips

  • Route model binding silently 404s on trashed records. ->withTrashed() on the route or a resolveRouteBinding override. This is the #1 "the package is broken" moment and it has nothing to do with the package.
  • restore() on a non-trashed model returns true and does nothing. It's idempotent, so abort_unless($model->trashed(), 409) if you want an honest HTTP response instead of a lying 200.
  • Unique constraints on natural keys will bite. If slug or email was reused after deletion, the restore throws QueryException. Decide up front: block, suffix, or fail loudly — and surface which one happened.
  • The restoring event can return false to abort. Handy for policy hooks; a footgun when an unrelated observer returns false for its own reasons and your restore silently no-ops.
  • updated_at gets bumped on restore. Anything keyed to "last modified" now claims the record changed today, which is wrong for reporting and can reshuffle sort orders.
  • Test cascade coverage against your real schema during the spike, not against the README: UUID/ULID keys, polymorphic relations, multi-tenancy, two-level and three-level cascades. Assume nothing; the package predates several of these patterns being common.
  • Audit your scheduled purges. A retention job that hard-deletes trashed rows older than N days is fine — until a queued payload references a row you just restored, or a restore resets deleted_at and pushes the row out of the purge window unexpectedly.
  • Route caching. If the package registers routes, check php artisan route:list and then re-check with route:cache on. Verify they sit behind your middleware group and that nothing explodes in production.
  • Config quirks. If a config file is published, diff it against the package default and commit it — an unpublished config silently follows upstream defaults on upgrade.
  • Pin the version and vendor or fork it. This is ~122 stars, zero dependents, single maintainer, best-effort upstream. You now own the dependency. Keep the source close and treat "reimplement behind our own controller and policies" as a scoped afternoon, not a migration.
  • Adopt for the cascade, not for the call. If all you need is single-record restore, native restore() plus a policy is already enough — and costs you one fewer dependency on your data-recovery path.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager