chrisbaltazar/doctrine-soft-delete
A Symfony Bundle to handle soft-deleted records transparently with Doctrine ORM and MySQL.
Instead of permanently deleting rows, soft-delete marks records with a deleted_at timestamp and automatically excludes them from all queries via a Doctrine SQL filter.
deleted_at column filtering via a Doctrine SQL filterGENERATED column (auto_generated_active_flag) for safe unique indexes on soft-deletable tables#[SoftDeleteUniqueIndex] to declare unique constraints that respect soft deletes| Dependency | Version |
|---|---|
| PHP | >= 8.1 |
symfony/framework-bundle |
^6.4 |
doctrine/doctrine-bundle |
^2.18 |
doctrine/orm |
^3.6 |
doctrine/doctrine-migrations-bundle |
^3.7 |
composer require chrisbaltazar/doctrine-soft-delete
Please make sure the bundle gets registered in your config/bundles.php:
(normally handled by Symfony Flex)
return [
// ...
Database\SoftDelete\SoftDeleteBundle::class => ['all' => true],
];
To auto enable the soft delete function, simply implement the SoftDeletableInterface in your entity and add a nullable deletedAt property:
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;
}
}
Then just remember to update your entities accordingly when you want to soft-delete a record:
$product->setDeletedAt(new \DateTimeImmutable());
$entityManager->flush();
This will automatically exclude soft-deleted records from all queries.
To include them, you can disable the filter at any point during your request flow with:
$entityManager->getFilters()->disable('soft_delete');
...
$users = $entityManager->getRepository(User::class)->findAll(); // includes soft-deleted records
For unique items that should ignore soft-deleted records, use the #[SoftDeleteUniqueIndex] attribute:
This will create an auto-generated column in your entity table as a boolean flag + a table index,
that would control the uniqueness behavior of the specified fields, ignoring soft-deleted records (where deleted_at is not null).
use Database\SoftDelete\Core\Attribute\SoftDeleteUniqueIndex;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[SoftDeleteUniqueIndex(fields: ['email'])]
class User implements SoftDeletableInterface
{
// ...
}
After that just generate a new migration as normal and execute it to update your database schema.
Then simply handle the unique constraint violation properly as the following example:
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
...
// Perform form validation and any other necessary checks before attempting to persist the user entity
...
try {
$entityManager->persist($user);
$entityManager->flush();
} catch (UniqueConstraintViolationException) {
$this->addFlash('error', 'A user with this email already exists.');
}
// Continue with the rest of your controller logic, such as rendering a response or redirecting the user
A demo Symfony application is available in the demo/ directory of this repository, showcasing the bundle's features in action.
To run the demo locally just use the handy commands available in the Makefile:
(Make sure you have already a docker environment well configured with docker-compose and Make installed on your machine)
make demo-run
After that you should be able to access the demo app at http://localhost:8000 and see how soft-deleted records are handled in the UI.
(the port number can be also customized in the docker-compose.override.yml file if needed)
Then just go over: http://localhost:8000/user and try it out as any other CRUD 🚀
To run the tests, simply execute:
make test
This will run all PHPUnit tests defined in the tests/ directory, ensuring that the bundle's functionality is working as expected,
ensuring the special order and conditions needed to properly test the database custom components and the generated column behavior.
How can I help you explore Laravel packages today?