Soft Delete
Soft delete lets you mark entities as deleted without removing them from the database. Deleted records are automatically hidden from queries and can be restored at any time by clearing the delete marker.
What is Soft Delete?
A normal delete removes a row from the database permanently. A soft delete instead sets a column on the row — a timestamp or a boolean — to indicate that the record is no longer active. ObjectQuel filters those rows out of every query automatically, so the rest of your application sees no difference:
// Hard delete — row is gone permanently
$entityManager->remove($order);
$entityManager->flush();
// Soft delete — row stays in the database, but disappears from queries
$order->setDeletedAt(new \DateTime());
$entityManager->flush();
When soft delete is useful:
- You need an audit trail of deleted records
- Deletions should be recoverable by an administrator
- Regulatory requirements prohibit permanent data removal
- You want a recycle-bin or undo-delete feature
When a hard delete is better:
- The data is genuinely disposable (e.g. session records, temporary rows)
- Privacy regulations require true erasure (e.g. GDPR right to erasure)
- Table size is a concern and deleted rows should not accumulate
Annotating an Entity
Add a nullable column to your entity and mark it with @Orm\SoftDelete alongside the usual @Orm\Column. The annotation must sit on a column property — it has no effect on its own without a backing column.
ObjectQuel supports two column types for the soft-delete marker:
- datetime —
NULLmeans active; any timestamp means deleted. This is the recommended type because it records when the record was deleted. - boolean —
falsemeans active;truemeans deleted.
use Quellabs\ObjectQuel\Annotations\Orm\Column;
use Quellabs\ObjectQuel\Annotations\Orm\SoftDelete;
class OrderEntity {
// ... other properties ...
/**
* @Orm\Column(name="deleted_at", type="datetime", nullable=true)
* @Orm\SoftDelete
*/
protected ?\DateTime $deletedAt = null;
public function getDeletedAt(): ?\DateTime {
return $this->deletedAt;
}
public function setDeletedAt(?\DateTime $deletedAt): self {
$this->deletedAt = $deletedAt;
return $this;
}
}
Marking and Restoring
Soft delete is a regular property update. Set the column value and call flush() — no special methods needed:
// Mark as deleted
$order->setDeletedAt(new \DateTime());
$entityManager->persist($order);
$entityManager->flush();
// Restore — set back to null
$order->setDeletedAt(null);
$entityManager->persist($order);
$entityManager->flush();
For boolean soft-delete columns the pattern is identical:
// Mark as deleted
$order->setDeleted(true);
$entityManager->persist($order);
$entityManager->flush();
// Restore
$order->setDeleted(false);
$entityManager->flush();
Automatic Filtering
Once an entity has a @SoftDelete annotation, ObjectQuel adds the filter condition to every query for that entity automatically. No changes to your queries are required:
// Returns only active orders — soft-deleted rows are excluded automatically
$orders = $entityManager->executeQuery("
range of o is App\Entity\OrderEntity
retrieve (o)
");
// Conditions and joins work the same way
$orders = $entityManager->executeQuery("
range of o is App\Entity\OrderEntity
range of c is App\Entity\CustomerEntity via o.customer
retrieve (o) where c.id = :customerId
", ['customerId' => 42]);
OrderEntity and CustomerEntity have @SoftDelete, rows are excluded when either entity is soft-deleted.
Bypassing the Filter
There are two ways to see soft-deleted records when you need to.
The @ignoreSoftDelete directive disables the filter for a single query:
// Returns all orders, including soft-deleted ones
$allOrders = $entityManager->executeQuery("
@ignoreSoftDelete true
range of o is App\Entity\OrderEntity
retrieve (o)
");
// Useful for admin views or restore workflows
$deleted = $entityManager->executeQuery("
@ignoreSoftDelete true
range of o is App\Entity\OrderEntity
retrieve (o) where o.deletedAt is not null
");
find() always bypasses the filter. When you look up an entity by primary key, you always get it back regardless of its soft-delete state. This prevents confusing situations where an entity you just modified suddenly becomes unfindable:
// Returns the order even if it has been soft-deleted
$order = $entityManager->find(OrderEntity::class, 99);
if ($order !== null && $order->getDeletedAt() !== null) {
// order exists but is soft-deleted
}
Hard Delete
Soft delete and hard delete coexist without conflict. remove() always issues a real DELETE statement regardless of whether the entity has a @SoftDelete annotation:
// Permanently removes the row from the database
$entityManager->remove($order);
$entityManager->flush();
This means you can soft-delete an entity for day-to-day use and still permanently purge it when needed — for example during a data retention cleanup job — without any special handling.
Summary
- Annotation — place
@Orm\SoftDeletealongside@Orm\Columnon a nullabledatetimeorbooleanproperty - Mark deleted — set the property value and call
flush() - Restore — set the property back to
null/falseand callflush() - Filter — applied automatically to all queries; no query changes needed
- Bypass per query — use the
@ignoreSoftDelete truecompiler directive - Bypass for PK lookup —
find()always returns the entity regardless of state - Hard delete —
remove()always issues a realDELETE