stichtingsd/soft-deleteable-extension-bundle
A Symfony bundle from Stichting SD that adds soft-delete support to Doctrine entities. Records are flagged as deleted instead of being permanently removed, allowing them to be excluded from normal queries and restored later, with bundle configuration to integrate the behavior into an application.
Start by reading the package README and then inspect composer.json, src/, DependencyInjection/, and any Resources/config/services.* files. Because the package is small and has very low adoption, confirm whether it is an API Platform operation extension, a Doctrine/Gedmo integration bundle, or both before designing around it.
composer require stichtingsd/soft-deleteable-extension-bundle
// config/bundles.php
return [
// ...
StichtingSD\SoftDeleteableExtensionBundle\SoftDeleteableExtensionBundle::class => ['all' => true],
];
Verify the exact class name from the package source or README; the repository spelling and Composer package spelling differ.
deletedAt field to the entity and configure the soft-delete metadata. The exact attribute/annotation depends on what the README shows, but the typical pattern is a Gedmo-compatible field:use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
#[ORM\Entity]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false, hardDelete: false)]
class Article
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $deletedAt = null;
public function getDeletedAt(): ?\DateTimeImmutable
{
return $this->deletedAt;
}
public function setDeletedAt(?\DateTimeImmutable $deletedAt): void
{
$this->deletedAt = $deletedAt;
}
}
php bin/console make:migration
php bin/console doctrine:migrations:migrate
DELETE /api/articles/123
Expected behavior: the row remains in the database, deleted_at is populated, and normal Doctrine/API Platform reads no longer return the record. If that does not happen, inspect the bundle’s extension/service wiring and confirm the Doctrine soft-delete filter is enabled.
Use soft delete as the default delete path, but keep hard delete explicit. Let normal DELETE operations or domain services set deletedAt. Create a separate, permission-protected hard-delete operation for admins or cleanup jobs.
final class ArchiveArticle
{
public function __construct(private EntityManagerInterface $em) {}
public function __invoke(Article $article): void
{
$article->setDeletedAt(new \DateTimeImmutable());
$this->em->flush();
}
}
Restore with a custom operation or service. Soft-deleted rows are still present, so restoration is usually just clearing deletedAt:
final class RestoreArticle
{
public function __construct(private EntityManagerInterface $em) {}
public function __invoke(Article $article): Article
{
$article->setDeletedAt(null);
$this->em->flush();
return $article;
}
}
Expose it as a dedicated API Platform operation, for example POST /api/articles/{id}/restore, rather than allowing clients to PATCH deletedAt directly.
Use the Doctrine filter for normal reads and explicitly bypass it for admin views. The filter should hide deleted rows from day-to-day repositories and API collections. When you need trashed records, disable the filter intentionally:
$filters = $em->getFilters();
$filterName = 'soft_deleteable'; // verify exact name from package config
if ($filters->isEnabled($filterName)) {
$filters->disable($filterName);
}
$trashed = $articleRepository->findAll();
// Re-enable or use a fresh EntityManager if the request continues.
$filters->enable($filterName);
If the exact filter name differs, inspect config/packages/doctrine.yaml and the bundle’s service configuration.
Pattern admin screens as two scopes: active and trashed. Build repository methods such as findActive(), findTrashed(), and restore(). Keep the filter bypass isolated in those methods or a dedicated repository, not scattered through controllers.
Add API Platform operations deliberately. Use the package if it already wraps DELETE correctly. Otherwise keep the soft-delete decision in your application service and let API Platform call that service. Avoid having two delete paths: one that hard-deletes and one that soft-deletes.
Index deleted_at and any common filtered columns. Soft delete adds deleted_at IS NULL predicates to many queries. Add an index on deleted_at, and consider composite indexes for common list queries.
Protect deletedAt from mass assignment and serialization. Mark it read-only in API Platform or exclude it from write denormalization. Clients should not be able to restore or delete records by PATCHing a timestamp.
Test the lifecycle, not just the delete response. Assert that the row still exists, deleted_at is set, normal reads omit it, filtered admin reads include it, restore works, and hard delete removes it.
softdeleteable-extension-bundle, while the Composer package is soft-deleteable-extension-bundle. Confirm the bundle class and namespace from src/ before registering it.remove(), and flush. Do not assume remove() on a filtered-out entity will work.deletedAt from public output or expose it read-only. Also audit association serialization: a parent may be active while a child is soft-deleted.UniqueEntity constraint may not see soft-deleted rows when the filter is enabled, but the database still enforces uniqueness. Test duplicate creation after soft delete.DateTimeImmutable and a consistent timezone. If the package or Gedmo config uses timeAware, verify behavior in tests.php bin/console doctrine:query:sql "SELECT id, deleted_at FROM article" to confirm the column is populated.php bin/console cache:clear.How can I help you explore Laravel packages today?