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

Doctrine Soft Delete Laravel Package

chrisbaltazar/doctrine-soft-delete

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Install the Bundle:

    composer require chrisbaltazar/doctrine-soft-delete
    

    Ensure Database\SoftDelete\SoftDeleteBundle::class is registered in config/bundles.php (handled automatically by Symfony Flex).

  2. Mark an Entity for Soft Deletes: Implement SoftDeletableInterface and add a deletedAt column to your entity:

    use Database\SoftDelete\Core\Contract\SoftDeletableInterface;
    use Doctrine\ORM\Mapping as ORM;
    
    #[ORM\Entity]
    class Product implements SoftDeletableInterface
    {
        #[ORM\Column(nullable: true)]
        private ?\DateTimeImmutable $deletedAt = null;
    
        public function setDeletedAt(\DateTimeImmutable $deletedAt): void { $this->deletedAt = $deletedAt; }
        public function getDeletedAt(): ?\DateTimeImmutable { return $this->deletedAt; }
    }
    
  3. Soft-Delete a Record:

    $product = $entityManager->find(Product::class, 1);
    $product->setDeletedAt(new \DateTimeImmutable());
    $entityManager->flush();
    
  4. Verify Filtering: All queries on Product will automatically exclude soft-deleted records. No manual WHERE clauses needed.


First Use Case: GDPR-Compliant User Deletion

  1. Entity Setup:

    #[ORM\Entity]
    #[SoftDeleteUniqueIndex(fields: ['email'])] // Ensures email uniqueness ignores soft-deleted users
    class User implements SoftDeletableInterface
    {
        #[ORM\Column(nullable: true)]
        private ?\DateTimeImmutable $deletedAt = null;
        // ... other fields
    }
    
  2. Delete User (Compliant with "Right to Erasure"):

    $user->setDeletedAt(new \DateTimeImmutable());
    $entityManager->flush();
    
  3. Temporarily Include Deleted Users (e.g., for admin audit):

    $entityManager->getFilters()->disable('soft_delete');
    $deletedUsers = $entityManager->getRepository(User::class)->findAll();
    $entityManager->getFilters()->enable('soft_delete'); // Re-enable
    

Where to Look First

  • Bundle Documentation: Focus on the README.md for installation and basic usage.
  • Entity Annotations: Study SoftDeletableInterface and #[SoftDeleteUniqueIndex] for advanced features.
  • Doctrine Filters: Understand how $entityManager->getFilters() works for temporary overrides.
  • Demo App: Run make demo-run to see the bundle in action (requires Docker).

Implementation Patterns

Core Workflow: Soft Delete in Symfony

  1. Entity Design:

    • Implement SoftDeletableInterface for all soft-deletable entities.
    • Add a nullable deletedAt column (timestamp type).
    • Use #[SoftDeleteUniqueIndex] for fields requiring uniqueness (e.g., email).
  2. Deletion Logic:

    // Service/Controller
    public function softDelete(Entity $entity): void
    {
        $entity->setDeletedAt(new \DateTimeImmutable());
        $this->entityManager->flush();
    }
    
  3. Query Handling:

    • All queries automatically exclude soft-deleted records via Doctrine’s SQL filter.
    • No need to manually add WHERE deletedAt IS NULL to every query.
  4. Bulk Operations:

    // Soft-delete all inactive users
    $inactiveUsers = $entityManager->createQueryBuilder()
        ->select('u')
        ->from(User::class, 'u')
        ->where('u.isActive = :active')
        ->setParameter('active', false)
        ->getQuery()
        ->getResult();
    
    foreach ($inactiveUsers as $user) {
        $user->setDeletedAt(new \DateTimeImmutable());
    }
    $entityManager->flush();
    

Advanced Patterns

1. Temporary Filter Disabling

Use cases: Admin dashboards, audit logs, or data exports requiring all records.

// Controller/Service
public function exportAllUsers(): void
{
    $entityManager->getFilters()->disable('soft_delete');
    $users = $entityManager->getRepository(User::class)->findAll();
    // Process data...
    $entityManager->getFilters()->enable('soft_delete');
}

2. Unique Index Handling

Prevents duplicate email entries even after soft deletes:

#[ORM\Entity]
#[SoftDeleteUniqueIndex(fields: ['email'])]
class User implements SoftDeletableInterface
{
    // ...
}
  • Migration: Run php bin/console doctrine:migrations:diff and apply the migration.
  • Error Handling:
    try {
        $entityManager->persist($user);
        $entityManager->flush();
    } catch (UniqueConstraintViolationException $e) {
        $this->addFlash('error', 'Email already exists (ignoring soft-deleted users).');
    }
    

3. Custom Soft-Delete Logic

Extend the interface or use traits for domain-specific behavior:

trait SoftDeletableWithReason
{
    #[ORM\Column(nullable: true)]
    private ?string $deletedReason = null;

    public function setDeletedAtWithReason(\DateTimeImmutable $deletedAt, string $reason): void
    {
        $this->deletedAt = $deletedAt;
        $this->deletedReason = $reason;
    }
}

4. Integration with Symfony Forms

Add a "Soft Delete" checkbox to forms:

// UserType.php
$builder->add('softDelete', CheckboxType::class, [
    'label' => 'Soft Delete (Archive)',
    'mapped' => false,
    'required' => false,
]);

// Controller
public function update(Request $request, User $user): Response
{
    $form = $this->createForm(UserType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        if ($request->request->get('soft_delete')) {
            $user->setDeletedAt(new \DateTimeImmutable());
        }
        $this->entityManager->flush();
    }
    // ...
}

5. Event Listeners for Soft Deletes

Trigger actions when an entity is soft-deleted (e.g., log to an audit table):

// src/EventListener/SoftDeleteListener.php
use Doctrine\ORM\Event\LifecycleEventArgs;

class SoftDeleteListener
{
    public function postPersist(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        if ($entity instanceof SoftDeletableInterface && $entity->getDeletedAt() !== null) {
            $this->logSoftDelete($entity);
        }
    }

    private function logSoftDelete(object $entity): void
    {
        // Log to audit table or external service
    }
}

Register the listener in services.yaml:

services:
    App\EventListener\SoftDeleteListener:
        tags:
            - { name: doctrine.event_listener, event: postPersist }
            - { name: doctrine.event_listener, event: postUpdate }

Integration Tips

1. Doctrine Migrations

  • The bundle includes a custom schema comparator to reduce migration noise. Ensure your migrations are compatible:
    php bin/console doctrine:migrations:diff
    php bin/console doctrine:migrations:migrate
    

2. Testing Soft Deletes

  • Use disable('soft_delete') in tests to verify queries include soft-deleted records:
    public function testSoftDeleteExclusion(EntityManagerInterface $em)
    {
        $em->getFilters()->disable('soft_delete');
        $deletedUser = $em->getRepository(User::class)->findOneBy(['deletedAt' => $this->deletedAt]);
        $this->assertNotNull($deletedUser);
    }
    

3. Performance Considerations

  • Indexing: Ensure deletedAt is indexed for performance:
    #[ORM\Column(nullable: true)]
    #[ORM\Index(name: 'idx_deleted_at')]
    private ?\DateTimeImmutable $deletedAt = null;
    
  • Batch Operations: For bulk soft deletes, use Doctrine batch processing to avoid memory issues:
    $conn = $entityManager->getConnection();
    $conn->executeStatement(
        'UPDATE user SET deleted_at = NOW() WHERE is_active = 0'
    );
    

4. Multi-Tenancy

For SaaS apps, combine soft deletes with tenant isolation:

#[ORM\Entity]
class User implements SoftDeletableInterface
{
    #[ORM\Column]
    private int $tenantId;

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $deletedAt = null;
}

Query with tenant scope:

$users = $
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.
besmartand-pro/php-quality-config
sentix/ai-chatbot
terminal42/code-quality-tools
codifyo/ts-generator-bundle
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity