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

Soft Deletable Bundle Laravel Package

incompass/soft-deletable-bundle

A Symfony bundle that adds soft-delete support to Doctrine entities. Mark records as deleted without removing them from the database, automatically filter them from normal queries, and restore them when needed, helping preserve data history and build safer deletion workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle is the Doctrine answer to Laravel's SoftDeletes trait: an interface + trait that stamps a deletedAt column instead of issuing a DELETE, plus a Doctrine SQLFilter that appends deleted_at IS NULL to every SELECT. Only three surfaces matter, and you should read them in this order before touching anything else:

  1. src/ interface + SoftDeletableTrait — the column and accessors.
  2. The Doctrine event subscriber that intercepts EntityManager::remove().
  3. The SQLFilter class — this is what makes deleted rows invisible.

Everything else is a consequence of how those three interact. Translate your Eloquent instincts up front:

Laravel This bundle
use SoftDeletes; implements SoftDeletableInterface + use SoftDeletableTrait
$model->delete() $em->remove($entity); $em->flush();
Global scope Doctrine SQLFilter
withTrashed() / onlyTrashed() Disable/re-enable the filter (no first-class API)
restore() / forceDelete() You write these — see Implementation Patterns

Minimal wiring, in order:

  1. composer require incompass/soft-deletable-bundle and register the bundle in config/bundles.php.
  2. Add the interface and trait to one entity. Do not redeclare the deletedAt property or its mapping — the trait carries it. Only re-declare if the bundle docs tell you to (attribute mapping on trait properties is inherited in PHP 8, but the bundle may predate that assumption).
  3. Enable the filter in config/packages/doctrine.yaml:
doctrine:
  orm:
    filters:
      soft_deletable:
        class: Incompass\SoftDeletableBundle\Doctrine\Filter\SoftDeletableFilter
        enabled: true

Verify the exact FQCN and filter key from the bundle's DependencyInjection/ config or README — a wrong class name here fails silently, leaving you with soft-delete-on-remove but completely unfiltered reads. That is the single most expensive mistake you can make with this package.

  1. Generate a migration with a nullable DATETIME column and an index on deleted_at.
  2. First use case: take one non-critical entity (a comment, a draft), wire it end to end, write an integration test that asserts the row still exists after remove(), and confirm reads filter it out. Only then roll out to more entities.

Implementation Patterns

Soft delete in a service. $em->remove($entity); $em->flush(); — the subscriber intercepts and stamps deletedAt instead of issuing DELETE. Wrap this in your repository/service so callers never touch the EM directly:

final class CommentService {
    public function __construct(private EntityManagerInterface $em) {}
    public function delete(Comment $c): void {
        $this->em->remove($c);
        $this->em->flush();
    }
}

Restore. There is no restore() method. Implement it on the entity or in a service:

public function restore(): void {
    $this->deletedAt = null;
}
// then $em->flush(); — but the filter hides the entity, so you
// usually restore via a dedicated path that disables the filter first.

Force delete (hard delete). Disable the filter for the operation, then remove() and flush(), or issue a raw DELETE. This is the purge path compliance needs — this package does not provide one.

$filters = $this->em->getFilters();
$filters->isEnabled('soft_deletable') && $filters->disable('soft_deletable');
$this->em->remove($entity);
$this->em->flush();
$filters->enable('soft_deletable'); // restore state

Toggling the filter for "recycle bin" / admin listings.

$filters = $em->getFilters();
$filters->disable('soft_deletable');
$trashed = $repo->findBy(['deletedAt' => $repo->/* ... */]); // now visible
$filters->enable('soft_deletable');

Do this inside a try/finally — a thrown exception that skips the re-enable leaks deleted rows into the rest of the request, including API responses.

Repository pattern. Centralize findActive, findTrashed, restore, purge methods in one repository per entity so the filter toggle logic lives in exactly one place instead of being sprinkled across controllers.

Integration tips. Prefer the bundle trait over hand-rolling deletedAt per entity so the filter and subscriber both recognize the entity. If you use laravel-doctrine/orm, register the subscriber manually in a service provider and skip Symfony bundle discovery entirely — see Gotchas.

Gotchas and Tips

Filter class mismatch fails silently. If the doctrine.orm.filters.soft_deletable.class value is wrong, Doctrine happily loads nothing and your SELECTs return deleted rows. Test it: soft-delete one fixture, then $repo->find($id) and assert null. If you get the entity back, your filter is not wired.

remove() becoming a hard DELETE. If the subscriber is not registered (easy in a Laravel+Doctrine setup where bundle auto-discovery never fires), remove() physically deletes. In CI, assert the row still exists in the table after delete — this is the guard that catches the catastrophic case.

Unique constraints still collide. A soft-deleted row keeps its unique key, so re-inserting "the same" record throws a DB error at runtime, not migration time. Either drop the unique constraint in favor of a partial/functional index (WHERE deleted_at IS NULL), or rewrite the key on delete (e.g. append a suffix). Plan this before rollout.

ORM bypass leaks deleted rows. Raw SQL, DB::raw, DQL with the filter disabled and forgotten, and bulk jobs all ignore the filter. Audit every query path — API serializers and reporting endpoints are the ones that most often leak.

No first-class withTrashed(). Toggling the filter is global for the EntityManager, not scoped to a query. Always restore state in finally. Consider a small wrapper method to make intent explicit.

Cascade behavior is unverified. Association cascades and orphan removal are not documented here. Test soft-deleting a parent with children explicitly; don't assume ordering or that children get soft-deleted.

Restore conflicts. Restoring an entity whose unique key now collides with a live record throws at flush. Catch and present a user-facing error rather than a 500.

Indexing. Add an index on deleted_at — every query carries the filter predicate, and unindexed tables degrade quickly.

Doctrine/PHP version drift. With 0 stars, 0 dependents and no maintenance signal, assume you own the code. Vendor it or fork it, assign an owner, and pin the Doctrine ORM version. A Doctrine upgrade that changes the event-subscriber contract silently flips your delete semantics — keep the row-exists-after-delete test in CI so the flip is caught.

Extension points. The filter and the subscriber are the two seams. If the bundle lacks behavior you need (cascade soft-delete, restore() convenience, per-query scope), extend those two classes in your own namespace rather than editing vendor, and register your subclass in doctrine.yaml and services.

Stack sanity check. This is a Symfony bundle for Doctrine entities. On a standard Laravel/Eloquent codebase it does not apply — use the native SoftDeletes trait. On Symfony/Doctrine the maintained equivalent is Gedmo's SoftDeleteable via StofDoctrineExtensionsBundle; weigh that before taking on an unmaintained dependency for a capability that costs roughly one day to build in-house.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core