Skip to content

Add soft delete support #158

Open
Open

Description

@usernane

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 NULL

Soft 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 = 5

Tables 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-deleted

Implementation 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions