Weave Code
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

andanteproject/soft-deletable-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require andanteproject/soft-deletable-bundle
    

    (Symfony Flex will auto-register the bundle; otherwise, add it to config/bundles.php.)

  2. Enable Soft Deletes on an Entity:

    use Andante\SoftDeletableBundle\SoftDeletable\{SoftDeletableInterface, SoftDeletableTrait};
    
    class Article implements SoftDeletableInterface
    {
        use SoftDeletableTrait;
        // ... existing fields
    }
    
  3. Update Schema:

    php bin/console doctrine:schema:update --force
    

    (Or use a migration.)

  4. 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();
    

Implementation Patterns

Workflows

  1. Default Workflow (Trait-Based):

    • Use SoftDeletableTrait for 90% of cases. It auto-maps deletedAt to deleted_at column.
    • Example:
      class User implements SoftDeletableInterface
      {
          use SoftDeletableTrait;
          // ...
      }
      
  2. Custom Property/Column Names:

    • Override defaults via andante_soft_deletable.yaml:
      andante_soft_deletable:
        entity:
          App\Entity\Log:
            property_name: deletedAt
            column_name: soft_deleted_at
      
  3. Conditional Soft Deletes:

    • Disable filtering for specific queries:
      $filter = $entityManager->getFilters()->getFilter(SoftDeletableFilter::NAME);
      $filter->disableForEntity(Article::class);
      // Query will include soft-deleted records.
      $filter->enableForEntity(Article::class); // Re-enable.
      
  4. Bulk Soft Deletes:

    • Use 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]);
      
  5. Restoring Entities:

    • Manually set deletedAt to null and flush:
      $article->setDeletedAt(null);
      $entityManager->flush();
      

Integration Tips

  • APIs: Use deletedAt in API responses to indicate soft-deleted status (e.g., {"deleted_at": "2023-01-01T00:00:00Z"}).
  • Admin Panels: Add a "Restore" button for soft-deleted records in admin interfaces.
  • Repositories: Extend base repositories to include soft-delete-aware methods:
    class ArticleRepository extends ServiceEntityRepository
    {
        public function findNonDeleted(): array
        {
            return $this->createQueryBuilder('a')
                ->where('a.deletedAt IS NULL')
                ->getQuery()
                ->getResult();
        }
    }
    
  • Events: Listen to 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());
            }
        }
    );
    

Gotchas and Tips

Pitfalls

  1. DQL Queries:

    • The filter does not apply to raw DQL queries. Use 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');
      
  2. Cascading Soft Deletes:

    • Relationships (e.g., @OneToMany) are not automatically soft-deleted. Handle manually:
      $article->setComments(null); // Detach before soft-deleting
      $entityManager->remove($article);
      
  3. Date Awareness:

    • By default, any deletedAt (past/future) excludes records. Set deleted_date_aware: true in config to exclude only past dates:
      andante_soft_deletable:
        deleted_date_aware: true
      
  4. Schema Updates:

    • Forgetting to run doctrine:schema:update after adding the trait will cause deleted_at column to be missing.
  5. Testing:

    • Mock SoftDeletableFilter in tests to control soft-delete behavior:
      $filter = $this->createMock(SoftDeletableFilter::class);
      $filter->method('isEnabled')->willReturn(false);
      $entityManager->getFilters()->setFilter(SoftDeletableFilter::NAME, $filter);
      

Debugging

  1. Check Filter Status:

    • Verify the filter is enabled:
      $filter = $entityManager->getFilters()->getFilter(SoftDeletableFilter::NAME);
      var_dump($filter->isEnabled()); // Should be true
      
  2. Inspect Generated SQL:

    • Enable Doctrine logging to see if deleted_at is included in WHERE clauses:
      $entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
      
  3. Column Name Mismatch:

    • If deleted_at isn’t auto-created, check:
      • The entity implements SoftDeletableInterface.
      • No custom column_name in config is conflicting with Doctrine’s naming strategy.

Extension Points

  1. Custom Logic on Soft Delete:

    • Override 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;
      }
      
  2. Dynamic Filtering:

    • Create a custom filter service to conditionally enable/disable soft deletes:
      class ConditionalSoftDeleteFilter
      {
          public function apply(EntityManager $em, bool $enabled): void
          {
              $filter = $em->getFilters()->getFilter(SoftDeletableFilter::NAME);
              $filter->{$enabled ? 'enable' : 'disable'}();
          }
      }
      
  3. Bulk Restore:

    • Add a repository method to restore multiple entities:
      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();
      }
      
  4. Soft Delete Events:

    • Dispatch events when soft-deleting:
      $entityManager->getEventManager()->addEventListener(
          \Doctrine\ORM\Events::preRemove,
          function ($event) {
              $entity = $event->getObject();
              if ($entity instanceof SoftDeletableInterface) {
                  $dispatcher->dispatch(new SoftDeletedEvent($entity));
              }
          }
      );
      

Config Quirks

  • always_update_deleted_at:

    • If true, deleting an already soft-deleted entity updates deleted_at to the current time. Useful for audit logs.
    • Example:
      andante_soft_deletable:
        default:
          always_update_deleted_at: true
      
  • Indexing:

    • Disabling table_index (e.g., for large tables) may slow queries:
      andante_soft_deletable:
        default:
          table_index: false
      
  • Property Naming:

    • Avoid naming the property deleted if using deleted_date_aware: true, as it may conflict with Doctrine’s reserved keywords. Stick to deletedAt.
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.
daikazu/eloquent-salesforce-objects
unseen-codes/chat
romalytar/yammi-jobs-monitoring-laravel
kisame76/filament-db-table-state
nqxcode/laravel-lucene-search
dpfx/laravel-livewire-wizards
workos/workos-php-laravel
sofa/laravel-global-scope
nawasara/auth-primitives
adhocrat-io/arkhe-main
make-dev/orca-harpoon
itsemon245/lamet
baks-dev/dashboard
amoifr/pickle-panther-bundle
make-dev/orca
dmstr/symfony-system-resources-bundle
dmstr/symfony-job-queue-bundle
dmstr/openapi-json-schema-bundle
dmstr/keycloak-security-bundle
dmstr/doctrine-audit-log-bundle