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 Previously Deleted Laravel Package

romanzipp/laravel-previously-deleted

Laravel package for handling previously deleted Eloquent models. It helps you detect records that were soft-deleted in the past and use that information when validating, restoring, or creating new entries, helping avoid duplicate or conflicting data.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're adopting first. This is a 3-star package with zero dependents and a last release of March 2020 — that's Laravel 6/7 era. Before anything else, open composer.json in the repo and read the require block for illuminate/* and php constraints. On Laravel 9+ you will very likely need to fork it, drop the version pins, or just reimplement the ~20 lines of logic yourself (see Gotchas). Treat it as a reference implementation, not a dependency you build a feature on.

The mental model: the package snapshots a model's attributes into a JSON column at the moment of deletion, so you can still answer "what did this record look like before it was deleted?" after the fact. It is not a full audit log and it does not resurrect records.

Minimal setup:

  1. Install (adjust for your Laravel version, or vendor the trait into app/Support/):

    composer require romanzipp/laravel-previously-deleted
    
  2. Add the column. Either publish/run the package migration or add it yourself — a nullable JSON column on the target table:

    Schema::table('users', function (Blueprint $table) {
        $table->json('previously_deleted')->nullable();
    });
    
  3. Add the trait to the model (verify the exact FQCN against the repo's src/ — the README is thin):

    use RomanZipp\PreviouslyDeleted\Traits\HasPreviouslyDeletedAttributes;
    
    class User extends Model
    {
        use SoftDeletes;
        use HasPreviouslyDeletedAttributes;
    }
    
  4. Pair it with SoftDeletes. This is the single most important thing to understand: the snapshot lives in a column on the row. If you hard-delete, the row and the snapshot both vanish. The package only makes sense on models where the row survives.

First use case: a user closes their account. You anonymise the live columns (name, email, phone → null/deleted-user-123) for GDPR, but support needs to answer "which email was on this account when it was closed?" three months later. The previously_deleted JSON column holds the pre-anonymisation values while the live row holds the scrubbed ones.

Implementation Patterns

Pattern 1 — Anonymise on delete, keep the snapshot. The canonical flow. Snapshot happens in the deleting event; your scrubbing happens after.

// In the model, alongside the trait
protected static function booted()
{
    static::deleting(function (User $user) {
        $user->name  = 'Deleted user';
        $user->email = null;
        $user->phone = null;
        $user->saveQuietly(); // don't re-fire events
    });
}

Order matters — the package's boot<Name>() trait hook registers its listener during boot, so it runs before listeners you register in booted(). Verify this with a test rather than trusting it.

Pattern 2 — Forward the snapshot into your own audit table. The JSON column is a staging area, not a queryable audit trail. If you need to search across deleted records or keep history beyond one deletion, listen for the deleted event and copy out:

static::deleted(function (User $user) {
    AuditEntry::create([
        'auditable_type' => User::class,
        'auditable_id'   => $user->id,
        'event'          => 'deleted',
        'payload'        => $user->previously_deleted, // decoded array/cast
        'actor_id'       => auth()->id(),
    ]);
});

This is the pattern I'd actually recommend for anything compliance-adjacent.

Pattern 3 — Restore-time diffing. On undelete, compare the snapshot against current state to decide whether to rehydrate:

public function restore(?array $only = null): void
{
    $snapshot = $this->previously_deleted ?? [];

    $this->forceFill(
        collect($snapshot)->only($only ?? array_keys($snapshot))->all()
    )->save();

    $this->forceFill(['previously_deleted' => null])->saveQuietly();
}

Never blindly rehydrate every column — schema changes between delete and restore will bite you, and any admin edits made to the soft-deleted row will be silently clobbered.

Pattern 4 — Surface it behind authorisation. Expose the snapshot through a policy-gated admin endpoint, not a resource by default:

// UserResource
'deleted_snapshot' => $this->when(
    Gate::allows('viewDeletedSnapshot', $this),
    fn () => $this->previously_deleted
),

Treat the snapshot as PII — it contains the exact data you were asked to forget.

Integration tips

  • Wrap the whole delete-and-snapshot flow in a single DB transaction (DB::transaction(fn () => $user->delete())) so you never end up with a half-scrubbed row.
  • If you already use spatie/laravel-activitylog or owen-it/laravel-auditing, you don't need this package — those do the job with more maintenance behind them.
  • Cast the column to array/json in $casts if the package doesn't do it for you.

Gotchas and Tips

It's a dead package — plan accordingly. Zero dependents, no release since March 2020. No Laravel 9/10/11 support, no PHP 8.1+ guarantees, no security patches. If you adopt it, fork it into app/Support/ or a private namespace and own the code. The entire behaviour is small enough to reimplement:

trait HasPreviouslyDeletedAttributes
{
    public static function bootHasPreviouslyDeletedAttributes(): void
    {
        static::deleting(function (Model $model) {
            $model->forceFill([
                'previously_deleted' => $model->getOriginal(),
            ])->saveQuietly();
        });
    }
}

Add an $exceptPreviouslyDeleted array to strip password, remember_token, and any 2FA secrets before storing, and you've replaced the package with something you control.

Hard deletes silently lose everything. forceDelete(), ->delete() on a model without SoftDeletes, or a query-builder ->delete() bypass the model events entirely. The column is gone with the row. Test forceDelete() explicitly — it's the classic surprise.

Bulk operations skip the trait. User::where(...)->delete() never boots models and never fires deleting. Same for truncate(). Snapshotting only happens on instance deletes.

Don't let it be mass-assignable. The snapshot column must never be fillable from request input, or a client can forge their own "previous" values. Keep it in $guarded (the default) and only ever write it via forceFill.

Snapshot size and row bloat. getOriginal() captures every attribute, including long text and binary-ish columns. On a wide table this bloats every soft-deleted row. If the package supports an attribute allow-list, configure it; if not, override the trait. Also watch MySQL's max_allowed_packet and JSON column size limits on large models.

JSON support by driver. MySQL 5.7+/Postgres are fine. SQLite stores JSON as text — casting still works, but whereJsonContains and friends won't. If your test suite runs on SQLite and prod on Postgres, assert on the decoded array, not on JSON path queries.

Debugging checklist when the column is empty after delete():

  1. Is the trait actually on the model? (class_uses_recursive($model))
  2. Does the model use SoftDeletes? (hard delete = no row)
  3. Was the delete an instance delete or a query-builder delete?
  4. Did another deleting listener return false and abort?
  5. Did your own deleting listener run after the snapshot and overwrite the column?

Restore doesn't undo itself. Laravel's restore() flips deleted_at and nothing else. The snapshot stays populated until you clear it, which means a re-deleted model will overwrite the previous snapshot with the restored state. Decide deliberately whether that's the behaviour you want; I'd clear the column on restore.

Extension points are thin. Expect the package to offer: a trait, a migration stub, and maybe a configurable column name. Anything more (per-model attribute filtering, retention windows, encryption of the snapshot) you'll be writing yourself. Encrypting the column is worth considering given it holds the PII you were trying to remove.

Verdict: interesting as a pattern, not viable as a dependency in 2024+. Use the README as inspiration and spend ten minutes writing your own trait plus a test for forceDelete(), bulk delete, and restore. You'll get a smaller surface area and won't inherit an unmaintained package on a code path that touches deletions.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log