Skip to content

Soft Delete

Soft delete allows you to "delete" records without actually removing them from the database. Instead, a timestamp is set in a designated column.

Setup

Enable soft delete by setting the $deletedAtColumn property:

php
class UserRepository extends BaseRepository
{
    protected ?string $deletedAtColumn = 'deleted_at';

    public function __construct(Connection $connection)
    {
        parent::__construct($connection, User::class, 'users');
    }
}

Database Column

Ensure your table has the deleted_at column:

sql
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;

Behavior Changes

When soft delete is enabled:

MethodWithout Soft DeleteWith Soft Delete
delete()Physical DELETESets deleted_at = NOW()
deleteBy()Physical DELETESets deleted_at = NOW()
find()Returns any recordExcludes deleted records
findBy()Returns any recordExcludes deleted records
findAll()Returns allExcludes deleted records
count()Counts allExcludes deleted records

Basic Usage

php
// Create a user
$user = $repo->create(['name' => 'John', 'email' => 'john@example.com']);

// Soft delete (sets deleted_at)
$repo->delete($user->id);

// User is now "invisible" to normal queries
$user = $repo->find($user->id); // Returns null!

// Restore the user
$repo->restore($user->id);

// User is visible again
$user = $repo->find($user->id); // Returns the user

Querying Records

Active Records Only (Default)

php
// Only non-deleted records
$users = $repo->findAll();
$users = $repo->findBy(['status' => 'active']);
$count = $repo->count([]);

Deleted Records Only

php
// Only soft-deleted records
$deleted = $repo->findBy(['deleted_at' => ['!=' => null]]);

All Records (Including Deleted)

Use the special '*' value:

php
// Active + deleted
$all = $repo->findBy(['deleted_at' => '*']);

// With other criteria
$all = $repo->findBy([
    'status' => 'premium',
    'deleted_at' => '*'  // Include deleted premium users
]);

Soft Delete Methods

restore()

Restore a soft-deleted record by setting deleted_at to NULL.

php
public function restore(int|string $id): int
php
$repo->delete(1);     // Soft delete
$repo->restore(1);    // Restore

// Returns 0 if soft delete not enabled or record not found

forceDelete()

Permanently delete a record, bypassing soft delete.

php
public function forceDelete(int|string $id): int
php
// Physical deletion (even with soft delete enabled)
$repo->forceDelete(1);

forceDeleteBy()

Permanently delete multiple records matching criteria.

php
public function forceDeleteBy(array $criteria): int
php
// Purge all soft-deleted records older than 30 days
$purged = $repo->forceDeleteBy([
    'deleted_at' => ['<' => date('Y-m-d', strtotime('-30 days'))]
]);

echo "Purged {$purged} records";

Practical Examples

Trash / Recycle Bin

php
class UserRepository extends BaseRepository
{
    protected ?string $deletedAtColumn = 'deleted_at';

    // Get all trashed users
    public function findTrashed(): array
    {
        return $this->findBy(['deleted_at' => ['!=' => null]]);
    }

    // Empty trash (permanently delete)
    public function emptyTrash(int $daysOld = 30): int
    {
        return $this->forceDeleteBy([
            'deleted_at' => ['<' => date('Y-m-d H:i:s', strtotime("-{$daysOld} days"))]
        ]);
    }
}

// Usage
$trashedUsers = $userRepo->findTrashed();
$purgedCount = $userRepo->emptyTrash(30);

Admin Panel with Restore

php
// List all users for admin (including deleted)
$allUsers = $repo->findBy(['deleted_at' => '*'], ['created_at' => 'DESC']);

// Restore user
$repo->restore($userId);

// Permanently delete
$repo->forceDelete($userId);

GDPR Compliance

php
// User requests account deletion
public function deleteUserAccount(int $userId): void
{
    // Soft delete first (grace period)
    $this->userRepo->delete($userId);
    
    // Schedule permanent deletion after 30 days
    $this->scheduler->schedule(
        new PermanentDeleteJob($userId),
        '+30 days'
    );
}

// After grace period
public function permanentlyDeleteUser(int $userId): void
{
    // Delete related data
    $this->orderRepo->forceDeleteBy(['user_id' => $userId]);
    $this->commentRepo->forceDeleteBy(['user_id' => $userId]);
    
    // Permanently delete user
    $this->userRepo->forceDelete($userId);
}

Combining with Eager Loading

Soft delete works seamlessly with eager loading:

php
// Active posts with their users (soft delete on both)
$posts = $postRepo->with(['user'])->findAll();

// Include deleted posts
$allPosts = $postRepo->with(['user'])->findBy(['deleted_at' => '*']);

Related Records

Eager loading respects soft delete on the related repository. If both PostRepository and UserRepository have soft delete enabled, deleted users won't be loaded.


Custom Column Name

You can use any column name:

php
class ArticleRepository extends BaseRepository
{
    protected ?string $deletedAtColumn = 'archived_at';
}

Just ensure the column exists in your table and your model handles it correctly.

Released under the MIT License.