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

Db Safedelete Laravel Package

illuminatech/db-safedelete

Illuminatech/db-safedelete adds safe, reversible deletion to Laravel Eloquent models. Instead of permanently removing records, it marks them as deleted, supports restoring them, and helps keep related data and queries consistent. Useful for audit-friendly apps that need recoverable deletes without c...

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and give the model both soft-delete plumbing and the package trait:

composer require illuminatech/db-safedelete
// database/migrations/...
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
    $table->softDeletes(); // required: the fallback writes deleted_at
});
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminatech\DbSafeDelete\SafeDelete;

class Product extends Model
{
    use SoftDeletes;
    use SafeDelete;
}

Where to look first: the package README for the exact trait namespace/method names in your installed version, then the trait source itself — it is small, and the protected failure-detection method is the main extension point.

First use case: a catalog/admin delete button. Deleting an unreferenced product should actually remove the row; deleting one referenced by orders should quietly soft-delete instead of throwing a foreign-key error:

$product->delete();

if ($product->trashed()) {
    return back()->with('status', 'Product archived (still referenced).');
}

return back()->with('status', 'Product permanently removed.');

trashed() is the reliable way to know which path ran: a successful hard delete leaves deleted_at null and exists false; the fallback leaves deleted_at set and exists true.

Implementation Patterns

Standard resource destroy action. Keep controllers dumb; let the model decide:

public function destroy(Product $product)
{
    $product->delete();

    return redirect()->route('products.index')
        ->with('status', $product->trashed() ? 'Archived' : 'Deleted');
}

Explicit purge. When an admin really means "remove it," call forceDelete() directly — the trait's fallback is only on the normal delete path, so forceDelete() will surface the constraint exception (or cascade):

try {
    $product->forceDelete();
} catch (\Illuminate\Database\QueryException $e) {
    // still referenced; decide whether to block or report
}

Restore flow. Because the fallback is a normal soft delete, restore() and withTrashed() work unchanged. Pair delete/restore endpoints so an archived-then-unreferenced record can later be purged with forceDelete().

Customizing what counts as "still referenced." The trait catches the database exception and decides whether to fall back. Override its protected detection method in your model to narrow or broaden the behavior — for example, only treat integrity-constraint violations (MySQL 1451, PostgreSQL 23503, SQLite 787) as fallback-worthy, and rethrow connection/lock errors so they don't get silently converted into soft deletes. Verify the exact method name against the trait source in your installed version; it is the intended extension point.

Testing. Assert both branches with factories: one record with no dependents (expect exists === false), one with a dependent row and a restricting FK (expect trashed() === true). Use RefreshDatabase and enable FK enforcement in the test connection (see gotchas).

Queued/console cleanup. Deleting via a model instance works fine, but Product::query()->where(...)->delete() is a query-builder mass delete and bypasses the trait entirely. Chunk and call delete() per model when you need safe-delete semantics:

Product::where('discontinued', true)->chunkById(100, fn ($products) =>
    $products->each->delete()
);

Gotchas and Tips

  • PostgreSQL transactions are the big one. If the initial hard delete fails inside an open transaction, PostgreSQL aborts the transaction; the subsequent soft-delete fallback will then also fail. Run safe deletes outside explicit transactions, or wrap the attempt in a savepoint. MySQL and SQLite tolerate the pattern more forgivingly.
  • SQLite needs PRAGMA foreign_keys=ON. Without it there are no constraint violations to trigger, so everything hard-deletes. Enable it in config/database.php/test setup.
  • ON DELETE CASCADE changes the outcome. If children cascade, the hard delete succeeds and children disappear — no fallback, no soft delete. SafeDelete only protects you against RESTRICT/NO ACTION style constraints.
  • Events can fire asymmetrically. A failed hard-delete attempt may emit forceDeleting before falling back, then emit deleting/deleted from the soft-delete path. Observers that assume paired forceDeleting/forceDeleted events should be audited. Prefer deleted plus a trashed() check.
  • No exception on fallback. Callers that relied on delete() throwing to detect references will no longer see an error; inspect trashed() or the return state instead.
  • Unique indexes still apply to soft-deleted rows. The fallback keeps the row, so a unique sku/email remains occupied. Use partial/conditional unique indexes where appropriate.
  • Trait ordering / conflicts. Keep SoftDeletes and SafeDelete on the same model. If your Laravel version causes a method conflict, resolve it with insteadof and confirm the package trait's delete hook wins.
  • No meaningful config file. Behavior is driven by model traits and the DB; there is nothing to publish. Treat the trait source as documentation.
  • Maturity caveat. Low-star, single-maintainer package; pin the version, keep the trait thin, and add your own integration tests around the fallback so a future Laravel upgrade doesn't change semantics silently.
  • Log the fallback. A quiet soft delete on a "delete" button is surprising in production; log or report when the fallback path runs so you can spot records that are never actually purged.
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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk