korotovsky/gedmo-twig-softdeleteable-bundle
Symfony bundle that brings Gedmo SoftDeleteable into Twig, adding template helpers to detect and filter soft-deleted Doctrine entities. Keeps views clean by handling deleted records directly in templates without extra controller logic.
What it is. A small Symfony bundle that registers a Twig block tag for temporarily disabling Gedmo's softdeleteable Doctrine filter while a template renders. That is the whole feature: it flips $em->getFilters()->disable('softdeleteable') on entry to a template block and restores it on exit. It is a convenience wrapper, not a soft-delete implementation — Gedmo and (usually) stof/doctrine-extensions-bundle still do the real work.
Minimal setup
composer require korotovsky/gedmo-twig-softdeleteable-bundle
AppKernel::registerBundles(). Symfony 4+/Flex:
// config/bundles.php
return [
// ...
Korotovsky\GedmoTwigSoftdeleteableBundle\GedmoTwigSoftdeleteableBundle::class => ['all' => true],
];
The bundle namespace/class name is the thing to copy verbatim from the README — this package predates Flex and its registration instructions are easy to get wrong.softdeleteable must be enabled on the entity manager, normally via Stof:
# config/packages/stof_doctrine_extensions.yaml
stof_doctrine_extensions:
orm:
default:
softdeleteable: true
bin/console debug:twig
Look for the custom tag (and any function/filter) in the "Tags" section. The package is small enough that reading its Twig extension and token-parser classes is a five-minute job; do that rather than trusting examples.First use case: an admin "trash" panel. Controller code normally fetches entities before Twig runs, so a template tag only helps when the query executes during rendering — an embedded controller, lazy association traversal, or a Twig function that queries. The canonical pattern is wrapping an embedded controller:
{# templates/admin/post/index.html.twig #}
{% disable_softdeleteable %}
{{ render(controller('App\\Controller\\Admin\\TrashController::panel')) }}
{% enddisable_softdeleteable %}
While the block renders, the subrequest shares the same EntityManager, so its repository query sees the soft-deleted rows. The tag name above is the expected shape; verify the exact spelling (disable_softdeleteable vs. another form, and the matching end... tag) in debug:twig before committing it to many templates.
Prefer the controller over the template. For anything that mutates data or is security-sensitive, disable the filter explicitly with a guaranteed restore:
$filters = $em->getFilters();
$wasEnabled = $filters->isEnabled('softdeleteable');
$wasEnabled && $filters->disable('softdeleteable');
try {
$post = $postRepository->find($id); // includes trashed rows
// inspect, restore, or render
} finally {
$wasEnabled && $filters->enable('softdeleteable');
}
Wrap this in a small service (SoftDeleteableScope::withoutFilter(callable $fn)) and use it from controllers, commands, and tests. The Twig tag then becomes a thin adapter over the same service, so behaviour is consistent everywhere.
Use the Twig tag for read-only composition. It fits when a page composes fragments and only one fragment needs trashed rows:
{# Dashboard: active posts + a small "recently deleted" audit strip #}
{% disable_softdeleteable %}
{{ render(controller('App\\Controller\\Admin\\AuditController::recentlyDeleted', {class: 'Post'})) }}
{% enddisable_softdeleteable %}
Keep the scope as narrow as possible: one render() call, not the whole page.
Lazy-loading pattern. If an association points at soft-deleted children and you need to show them (for example, a deleted category's audit history), the traversal must happen inside the disabled scope. If the entity is already loaded and the collection already initialised, the tag does nothing — the proxy/collection won't re-query.
Restore workflow. A "restore" action is a two-phase operation: read the entity with the filter off, then clear deletedAt. Don't attempt it purely from Twig. Do it in a controller/service, and re-enable the filter before flush() so unrelated pending changes don't accidentally include entities that were only meant to be inspected.
Integration with admin generators. EasyAdmin, SonataAdmin, and Sylius all build list queries before rendering, so the template tag cannot help there. Add a custom action/controller that runs its query inside the disabled scope, then hands plain arrays/DTOs to the template. The tag is only useful for the fragments those tools render via subrequests.
Read-only reporting. For audit/export screens that must show deleted rows without allowing edits, disable the filter around the query, map results into read-only DTOs, re-enable, then render. This avoids leaving managed deleted entities in the UnitOfWork while a form or serializer runs.
try/finally.flush() or serializer can surface it unexpectedly. Call $em->clear() or detach when crossing scope boundaries.deletedAt stays populated. Gedmo's listener still treats the entity as soft-deleted for lifecycle purposes; only the SQL filter is bypassed. Set deletedAt to null and flush to actually restore.disable() throws if the filter is not enabled. Always guard with $em->getFilters()->isEnabled('softdeleteable'). This is a common failure in tests and in apps where Stof is configured per-entity-manager.deleted_at is indexed and paginate; a filter-disabled list view is exactly where an accidental full table scan appears.tearDown(); a leaked disabled filter turns unrelated tests green for the wrong reason. Assert both directions: visible in the trash view, invisible in the normal view.gedmo/doctrine-extensions versions early; if it doesn't fit, the functionality is roughly a Twig extension plus a token parser and node (tens of lines) — vendor it into your app or expose a softdeleteable_scope() function over your own service. That is often the lower-risk path.softdeleteable_scope(false)) backed by a service that saves/restores filter state, with the bundled tag kept only if it works on your versions. You can also decorate the bundle's extension service if you want to keep the tag and fix restore/nesting behaviour.How can I help you explore Laravel packages today?