andanteproject/soft-deletable-bundle
Installation:
composer require andanteproject/soft-deletable-bundle
(Symfony Flex will auto-register the bundle; otherwise, add it to config/bundles.php.)
Enable Soft Deletes on an Entity:
use Andante\SoftDeletableBundle\SoftDeletable\{SoftDeletableInterface, SoftDeletableTrait};
class Article implements SoftDeletableInterface
{
use SoftDeletableTrait;
// ... existing fields
}
Update Schema:
php bin/console doctrine:schema:update --force
(Or use a migration.)
Test Soft Delete:
$article = new Article('Test');
$entityManager->persist($article);
$entityManager->flush();
$entityManager->remove($article); // Soft deletes (sets `deleted_at` to current timestamp)
$entityManager->flush();
Default Workflow (Trait-Based):
SoftDeletableTrait for 90% of cases. It auto-maps deletedAt to deleted_at column.class User implements SoftDeletableInterface
{
use SoftDeletableTrait;
// ...
}
Custom Property/Column Names:
andante_soft_deletable.yaml:
andante_soft_deletable:
entity:
App\Entity\Log:
property_name: deletedAt
column_name: soft_deleted_at
Conditional Soft Deletes:
$filter = $entityManager->getFilters()->getFilter(SoftDeletableFilter::NAME);
$filter->disableForEntity(Article::class);
// Query will include soft-deleted records.
$filter->enableForEntity(Article::class); // Re-enable.
Bulk Soft Deletes:
Doctrine\ORM\QueryBuilder with SoftDeletableFilter disabled:
$qb = $entityManager->createQueryBuilder();
$qb->update(Article::class, 'a')
->set('a.deletedAt', ':now')
->where('a.id IN (:ids)')
->setParameter('now', new \DateTimeImmutable())
->setParameter('ids', [1, 2, 3]);
Restoring Entities:
deletedAt to null and flush:
$article->setDeletedAt(null);
$entityManager->flush();
deletedAt in API responses to indicate soft-deleted status (e.g., {"deleted_at": "2023-01-01T00:00:00Z"}).class ArticleRepository extends ServiceEntityRepository
{
public function findNonDeleted(): array
{
return $this->createQueryBuilder('a')
->where('a.deletedAt IS NULL')
->getQuery()
->getResult();
}
}
preRemove to customize soft-delete behavior:
$entityManager->getEventManager()->addEventListener(
\Doctrine\ORM\Events::preRemove,
function ($event) {
$entity = $event->getObject();
if ($entity instanceof SoftDeletableInterface) {
$entity->setDeletedAt(new \DateTimeImmutable());
}
}
);
DQL Queries:
EntityManager::createQueryBuilder() instead:
// ❌ Bypasses filter
$query = $entityManager->createQuery('SELECT a FROM App\Entity\Article a');
// ✅ Applies filter
$qb = $entityManager->createQueryBuilder();
$qb->select('a')->from(Article::class, 'a');
Cascading Soft Deletes:
@OneToMany) are not automatically soft-deleted. Handle manually:
$article->setComments(null); // Detach before soft-deleting
$entityManager->remove($article);
Date Awareness:
deletedAt (past/future) excludes records. Set deleted_date_aware: true in config to exclude only past dates:
andante_soft_deletable:
deleted_date_aware: true
Schema Updates:
doctrine:schema:update after adding the trait will cause deleted_at column to be missing.Testing:
SoftDeletableFilter in tests to control soft-delete behavior:
$filter = $this->createMock(SoftDeletableFilter::class);
$filter->method('isEnabled')->willReturn(false);
$entityManager->getFilters()->setFilter(SoftDeletableFilter::NAME, $filter);
Check Filter Status:
$filter = $entityManager->getFilters()->getFilter(SoftDeletableFilter::NAME);
var_dump($filter->isEnabled()); // Should be true
Inspect Generated SQL:
deleted_at is included in WHERE clauses:
$entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Column Name Mismatch:
deleted_at isn’t auto-created, check:
SoftDeletableInterface.column_name in config is conflicting with Doctrine’s naming strategy.Custom Logic on Soft Delete:
setDeletedAt in your entity:
public function setDeletedAt(?\DateTimeImmutable $deletedAt = null): void
{
if ($deletedAt !== null) {
// Add custom logic (e.g., log deletion)
\Log::info("Entity {$this->id} soft-deleted at {$deletedAt}");
}
$this->deletedAt = $deletedAt;
}
Dynamic Filtering:
class ConditionalSoftDeleteFilter
{
public function apply(EntityManager $em, bool $enabled): void
{
$filter = $em->getFilters()->getFilter(SoftDeletableFilter::NAME);
$filter->{$enabled ? 'enable' : 'disable'}();
}
}
Bulk Restore:
public function restore(array $ids): void
{
$qb = $this->createQueryBuilder('a')
->where('a.id IN (:ids)')
->setParameter('ids', $ids);
$qb->update()
->set('a.deletedAt', null)
->getQuery()
->execute();
}
Soft Delete Events:
$entityManager->getEventManager()->addEventListener(
\Doctrine\ORM\Events::preRemove,
function ($event) {
$entity = $event->getObject();
if ($entity instanceof SoftDeletableInterface) {
$dispatcher->dispatch(new SoftDeletedEvent($entity));
}
}
);
always_update_deleted_at:
true, deleting an already soft-deleted entity updates deleted_at to the current time. Useful for audit logs.andante_soft_deletable:
default:
always_update_deleted_at: true
Indexing:
table_index (e.g., for large tables) may slow queries:
andante_soft_deletable:
default:
table_index: false
Property Naming:
deleted if using deleted_date_aware: true, as it may conflict with Doctrine’s reserved keywords. Stick to deletedAt.How can I help you explore Laravel packages today?