evence/soft-deleteable-extension-bundle
Symfony bundle that extends Gedmo's SoftDeleteable listener, adding onSoftDelete annotations to entity associations. Supports CASCADE, SET NULL, and SUCCESSOR behaviors so references are handled cleanly when a parent is soft-deleted, avoiding Doctrine errors.
First, the stack reality check — read this before anything else. Despite appearing in a "PHP package" evaluation, this is not a Laravel package. There is no ServiceProvider, no config/*.php, no composer.json extra.laravel block. It is a Symfony bundle (Evence\Bundle\SoftDeleteableExtensionBundle) that decorates Gedmo's Doctrine SoftDeleteable listener. It solves a real 2012-era Doctrine gap: when Gedmo soft-deletes a row (sets deletedAt), the database's ON DELETE CASCADE never fires, so child associations end up pointing at soft-deleted parents and blow up with EntityNotFoundException / proxy hydration errors.
So as a Laravel developer, you only reach for this if all of the following are already true in your app:
laravel-doctrine/orm (you have abandoned Eloquent for these models).SoftDeleteable on those entities.If you are on Eloquent, close the tab — this does not apply, and the equivalent behaviour is a few lines in a deleting model event or observer (see Patterns below).
Minimal steps (Doctrine-on-Laravel path):
composer require evence/soft-deleteable-extension-bundle
You will not add it to config/app.php providers. You register the subscriber/listener with your Doctrine manager, typically in config/doctrine.php's managers.*.event_subscribers (or via a custom service provider that pulls the manager and calls $em->getEventManager()->addEventSubscriber(...)), registering before Gedmo's listener so it can intercept the pre-soft-delete event.
Where to look first:
Advertisement entity example — it's the canonical use case: a Shop is soft-deleted, and its Advertisement rows must be cascaded.use Evence\Bundle\SoftDeleteableExtensionBundle\Mapping\Annotation as Evence;CASCADE, SET NULL, SUCCESSOR.First use case: the marketplace/catalog pattern — soft-delete a seller/brand/shop and have its listings cascade-soft-delete rather than leaving orphaned shop_id references. Reproduce one real production EntityNotFoundException first, then confirm the annotation resolves it. That 1-day spike is the whole validation.
Per-relationship policy is the point. Each association on the owning side gets exactly one of three annotations. Pick intentionally — these are product/data-retention decisions, not implementation details:
use Evence\Bundle\SoftDeleteableExtensionBundle\Mapping\Annotation as Evence;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Entity
* @Gedmo\SoftDeleteable(fieldName="deletedAt")
*/
class Advertisement
{
/**
* @ORM\ManyToOne(targetEntity="Shop")
* @ORM\JoinColumn(name="shop_id", referencedColumnName="id")
* @Evence\onSoftDelete(type="CASCADE")
*/
private $shop; // deleting the shop archives its ads
/**
* @ORM\ManyToOne(targetEntity="Brand")
* @Evence\onSoftDelete(type="SET NULL")
*/
private $brand; // unlinking is not deleting — keep the ad
/**
* @ORM\ManyToOne(targetEntity="Account")
* @Evence\onSoftDelete(type="SUCCESSOR")
*/
private $account; // repoint to a merged/replacement account
}
Pattern 1 — CASCADE for archival. Parent soft-delete fans out to children. Use when children are meaningless without the parent (listings without a seller). This is the most common and the one that mirrors a DB ON DELETE CASCADE.
Pattern 2 — SET NULL for "unlink, don't delete". Child record must survive (audit trail, historical order, compliance record). Use when the association is optional and the child has independent value.
Pattern 3 — SUCCESSOR for merges and migrations. The one mode with no clean built-in equivalent anywhere else. Classic uses: account merges (A merged into B → B's records inherit A's), plan migrations, catalog consolidation. You designate a successor property of the same entity class, and the bundle repoints the reference instead of nulling or deleting.
Testing pattern — this is where you earn your keep. Wrap each relationship policy in a functional test: soft-delete the parent, flush, refresh/re-fetch the child, assert (a) it is soft-deleted for CASCADE, (b) getBrand() is null for SET NULL, (c) getAccount() equals the successor for SUCCESSOR. Do this before widening annotations beyond one relation — the whole risk here is unverified edge cases.
Laravel-side integration tip. If you're straddling Eloquent and Doctrine, keep the policy in one place. For Eloquent models the equivalent is:
static::deleting(function ($shop) {
if ($shop->isForceDeleting()) return; // let DB cascade handle hard deletes
$shop->advertisements()->delete(); // Gedmo-ish soft-delete cascade
});
Do not introduce Doctrine solely to gain this bundle's behaviour. If you're already 95% Eloquent, a deleting observer is cheaper and you own it.
SUCCESSOR migration pattern. For account merges (the roadmap-de-risking case in the product notes), annotate the association once, then write the merge as a normal service that soft-deletes the losing account. The successor pointer handles the reassignment — no bespoke UPDATE ... SET account_id = ? migration script per merge.
Not a Laravel package. No Artisan commands, no php artisan vendor:publish, no facade. If your onboarding doc says "add to providers array," it's wrong. Registration is Doctrine event-manager level.
Version-pin aggressively. This is an MIT, 57-star, single-maintainer, zero-dependent project. Last release 2024-10-16 is a liveness signal, not a durability signal. Pin the exact version and vendor or fork the source — you will be patching Doctrine/Gedmo compatibility yourself sooner or later.
Attribute vs. annotation is a real fork point. README only shows docblock annotations (@Evence\onSoftDelete). Attribute support landed only in 3.2.0 / 3.2.3. On PHP 8+ with Doctrine ORM 3, the annotation reader is removed — if you're not on #[Evence\onSoftDelete(type: 'CASCADE')] (and a version that supports it), it's a dead end until you fork.
Coupling to Gedmo internals. Extending a listener means tracking Gedmo's event signatures and UnitOfWork internals across upgrades. This is the fragile position. Whenever you bump Doctrine ORM / DBAL / Gedmo, re-run your cascade tests, not just your unit suite.
Bulk DQL DELETE bypasses the listener. Programmatic $em->remove() and pre-soft-delete events are the covered path; DELETE ... WHERE ... DQL will skip it entirely. Audit any query-builder deletes on affected entities.
Bulk operations, recursive cascades, and cycles are unverified. CASCADE→CASCADE chains, cyclic associations, and SUCCESSOR chains (A→B→C) are exactly the semantics the README calls "behaves like SQL ON DELETE" as an aspiration, not a guarantee. Add cycle protection at the data model level if you rely on it.
Transactions. Confirm (with a test, not the README) that a cascade runs in a single transaction and can't partially commit. If unsure, wrap the soft-delete call in an explicit $em->transactional(...).
Association-type coverage. Examples only show ManyToOne. OneToOne and ManyToMany are not evidenced — verify before annotating them, or expect surprises.
Interaction with SoftDeleteableFilter. Queries that filter out soft-deleted parents still need your joins to behave. Test reads after a cascade, not just the delete itself.
Debugging shortcut. If a cascade silently doesn't fire, check registration order of the event subscriber relative to Gedmo's listener first — that's the most common cause of "annotation present, nothing happens."
The honest fallback. If compatibility checks fail or your team can't absorb a single-maintainer dependency on a critical path, the three behaviours are ~1–3 days as an internal listener. The package's real value is that it has already specified the semantics (CASCADE / SET NULL / SUCCESSOR) for you — you can borrow that design without the dependency.
How can I help you explore Laravel packages today?