Repository navigation
Add soft delete support #158
Description
Is your feature request related to a problem? Please describe.
Deleting records permanently is often undesirable (audit trails, undo, data recovery). Currently there is no built-in way to mark records as deleted without removing them from the database. Developers must manually add deleted_at columns and remember to filter them on every query.
Describe the solution you'd like
Soft delete support at three levels: Table definition, query builder, and repository.
1. Table-level declaration
Class-based:
class UsersTable extends MySQLTable {
public function __construct() {
parent::__construct('users');
$this->addColumns([...]);
$this->useSoftDelete(); // enables soft delete, expects 'deleted-at' column
}
}Attribute-based:
#[Table(name: 'users', softDelete: true)]
class User {
#[Column(type: 'int', primary: true, autoIncrement: true)]
public ?int $id = null;
#[Column(type: 'varchar', size: 100)]
public string $name;
#[Column(type: 'datetime', nullable: true)]
public ?string $deletedAt = null;
}2. Query builder integration (AbstractQuery)
Auto-filtering on SELECT:
// Table has soft delete enabled:
$db->table('users')->select()->execute();
// Generated: SELECT * FROM users WHERE deleted_at IS NULL
$db->table('users')->select()->withTrashed()->execute();
// Generated: SELECT * FROM users (no filter)
$db->table('users')->select()->onlyTrashed()->execute();
// Generated: SELECT * FROM users WHERE deleted_at IS NOT NULLSoft delete / restore / force delete:
$db->table('users')->softDelete()->where('id', 5)->execute();
// Generated: UPDATE users SET deleted_at = NOW() WHERE id = 5
$db->table('users')->restore()->where('id', 5)->execute();
// Generated: UPDATE users SET deleted_at = NULL WHERE id = 5
$db->table('users')->forceDelete()->where('id', 5)->execute();
// Generated: DELETE FROM users WHERE id = 5Tables without soft delete enabled behave exactly as before — delete() does a real DELETE.
3. Repository integration (AbstractRepository)
$repo->deleteById(5); // soft delete (if table supports it)
$repo->forceDelete(5); // real DELETE
$repo->restore(5); // clear deleted_at
$repo->findAll(); // excludes soft-deleted
$repo->withTrashed()->findAll(); // includes soft-deleted
$repo->onlyTrashed()->findAll(); // only soft-deletedImplementation details:
| File | Change |
|---|---|
Attributes/Table.php |
Add softDelete: bool = false constructor parameter |
Table.php |
Add useSoftDelete() and isSoftDelete(): bool methods |
Attributes/AttributeTableBuilder.php |
Read softDelete from attribute, call useSoftDelete() on built table |
AbstractQuery.php |
Auto-add WHERE deleted_at IS NULL on select if table uses soft delete. Add withTrashed(), onlyTrashed(), softDelete(), restore(), forceDelete() methods |
Repository/AbstractRepository.php |
deleteById() uses soft delete if enabled. Add forceDelete(), restore(), withTrashed(), onlyTrashed() |
Describe alternatives you'd considered
- Trait-based approach (developer adds trait to each repository) — more boilerplate, easy to forget
- Global query scope (always filter) — same as proposed but less explicit
- Leave to developers — repetitive, error-prone (forgetting the WHERE filter leaks deleted data)
Additional context
The soft delete column name defaults to deleted-at (framework key convention). The column must be declared as datetime, nullable, with default null. If useSoftDelete() is called but the column does not exist on the table, an exception should be thrown during table construction.
Activity