PHPackages                             tuzelko/yii2-softdelete - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. tuzelko/yii2-softdelete

ActiveYii2-extension

tuzelko/yii2-softdelete
=======================

Soft delete extension for Yii2 framework

v1.0.0(yesterday)32↑2900%MITPHPPHP &gt;=8.0

Since Apr 3Pushed yesterdayCompare

[ Source](https://github.com/TuzelKO/yii2-softdelete)[ Packagist](https://packagist.org/packages/tuzelko/yii2-softdelete)[ RSS](/packages/tuzelko-yii2-softdelete/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

yii2-softdelete
===============

[](#yii2-softdelete)

[![Latest Stable Version](https://camo.githubusercontent.com/4c27283be05f43a369698be9e42e3b84dc145f614420dd1c201bbb13cadad67b/68747470733a2f2f706f7365722e707567782e6f72672f74757a656c6b6f2f796969322d736f667464656c6574652f762f737461626c652e737667)](https://packagist.org/packages/tuzelko/yii2-softdelete)[![Total Downloads](https://camo.githubusercontent.com/e05052bda5eb0711fac0b63534ae9317fc3c40732b413f6727e5b731877ce51a/68747470733a2f2f706f7365722e707567782e6f72672f74757a656c6b6f2f796969322d736f667464656c6574652f646f776e6c6f6164732e737667)](https://packagist.org/packages/tuzelko/yii2-softdelete)[![License](https://camo.githubusercontent.com/0fe70e8e571a753d6d0846d1b4c19509ff19ccbeaad99544507e060b2644550c/68747470733a2f2f706f7365722e707567782e6f72672f74757a656c6b6f2f796969322d736f667464656c6574652f6c6963656e73652e737667)](https://packagist.org/packages/tuzelko/yii2-softdelete)

Soft-delete extension for the [Yii2](https://www.yiiframework.com/) framework.

Adds soft-delete (and restore) behaviour to any `ActiveRecord` model via a single PHP trait, with an accompanying query class that automatically hides deleted records from all queries.

Features
--------

[](#features)

- **Three column strategies** — Unix timestamp (`int`), DB-native datetime, or boolean flag
- **Automatic query scope** — deleted records are invisible by default; opt in with `withDeleted()` / `onlyDeleted()`
- **Instance methods** — `delete()`, `restore()`, `forceDelete()`, `isSoftDeleted()`
- **Bulk methods** — `deleteAll()`, `restoreAll()`, `forceDeleteAll()`, `updateAll()` (all scope-aware)
- **Events** — `beforeSoftDelete`, `afterSoftDelete`, `beforeRestore`, `afterRestore`
- **Multi-database** — MySQL, PostgreSQL, SQLite, SQL Server, Oracle
- **Zero configuration** — sensible defaults, override only what you need

Requirements
------------

[](#requirements)

- PHP &gt;= 8.0
- yiisoft/yii2 ~2.0

Installation
------------

[](#installation)

```
composer require tuzelko/yii2-softdelete
```

Quick start
-----------

[](#quick-start)

### 1. Add the column to your migration

[](#1-add-the-column-to-your-migration)

```
// Unix timestamp (default)
$this->addColumn('{{%post}}', 'deleted_at', $this->integer()->null()->defaultValue(null));

// — or — boolean flag
$this->addColumn('{{%article}}', 'is_deleted', $this->boolean()->notNull()->defaultValue(false));
```

### 2. Apply the trait to your model

[](#2-apply-the-trait-to-your-model)

```
use tuzelko\yii\softdelete\SoftDeleteTrait;
use yii\db\ActiveRecord;

class Post extends ActiveRecord
{
    use SoftDeleteTrait;

    public static function tableName(): string
    {
        return 'post';
    }
}
```

That's it. `Post::find()` now returns only non-deleted records, and `$post->delete()` soft-deletes instead of hard-deletes.

Column strategies
-----------------

[](#column-strategies)

Override `softDeleteColumn()` and `softDeleteType()` when the defaults do not fit your schema.

ConstantColumn value when deletedColumn value when restored`TYPE_TIMESTAMP_INT` *(default)*`time()` (Unix timestamp)`NULL``TYPE_TIMESTAMP_DB``NOW()` / `datetime('now')` / etc.`NULL``TYPE_BOOL``1``0````
class Article extends ActiveRecord
{
    use SoftDeleteTrait;

    public static function tableName(): string { return 'article'; }

    public static function softDeleteColumn(): string { return 'is_deleted'; }
    public static function softDeleteType(): int      { return self::TYPE_BOOL; }
}
```

Instance methods
----------------

[](#instance-methods)

```
$post = Post::findOne(1);

$post->delete();          // soft-delete — sets deleted_at, hides from default scope
$post->isSoftDeleted();   // true

$post->restore();         // clears deleted_at, record becomes visible again
$post->isSoftDeleted();   // false

$post->forceDelete();     // permanent hard-delete (fires standard Yii2 before/afterDelete events)
```

Query scopes
------------

[](#query-scopes)

```
// Default — excludes soft-deleted records (no extra call needed)
Post::find()->all();

// Include soft-deleted records alongside active ones
Post::find()->withDeleted()->all();

// Only soft-deleted records
Post::find()->onlyDeleted()->all();
```

Bulk operations
---------------

[](#bulk-operations)

```
// Soft-delete all active records matching the condition
Post::deleteAll(['status' => 'spam']);

// Restore all soft-deleted records
Post::restoreAll();

// Restore specific records
Post::restoreAll(['id' => [3, 5, 7]]);

// Permanently delete all soft-deleted records
Post::forceDeleteAll(['is not', 'deleted_at', null]);

// updateAll() also skips soft-deleted records automatically
Post::updateAll(['status' => 'archived'], ['category_id' => 2]);
```

> **Auto-scope behaviour:** `deleteAll()`, `updateAll()`, and `restoreAll()` automatically add a "not deleted" (or "deleted") condition unless your `$condition` already references the soft-delete column. This prevents double-applying the scope when you target records explicitly.

Relations
---------

[](#relations)

Because `SoftDeleteTrait` overrides `find()` to return a `SoftDeleteActiveQuery`, the soft-delete scope is **automatically applied to every relation** that points to a soft-delete-enabled model — including eager loading via `with()` and join-based loading via `joinWith()`.

### Declaring relations

[](#declaring-relations)

```
class User extends ActiveRecord
{
    // hasMany — only active (non-deleted) posts are returned
    public function getPosts(): SoftDeleteActiveQuery
    {
        return $this->hasMany(Post::class, ['user_id' => 'id']);
    }

    // To include deleted records in a relation, call withDeleted() on it
    public function getAllPosts(): SoftDeleteActiveQuery
    {
        return $this->hasMany(Post::class, ['user_id' => 'id'])->withDeleted();
    }

    // hasOne — same rules apply
    public function getLatestPost(): SoftDeleteActiveQuery
    {
        return $this->hasOne(Post::class, ['user_id' => 'id'])
            ->orderBy(['created_at' => SORT_DESC]);
    }
}
```

```
class Post extends ActiveRecord
{
    use SoftDeleteTrait;

    public static function tableName(): string { return 'post'; }

    // Relation to a model that does NOT use soft-delete — works as usual
    public function getUser(): \yii\db\ActiveQuery
    {
        return $this->hasOne(User::class, ['id' => 'user_id']);
    }

    // Relation to another soft-delete model
    public function getComments(): SoftDeleteActiveQuery
    {
        return $this->hasMany(Comment::class, ['post_id' => 'id']);
    }
}
```

### Eager loading with `with()`

[](#eager-loading-with-with)

```
// Load users and only their active posts (soft-delete scope applied automatically)
$users = User::find()->with('posts')->all();

foreach ($users as $user) {
    foreach ($user->posts as $post) {
        // $post is never soft-deleted
    }
}

// Load users together with ALL their posts (including deleted)
$users = User::find()
    ->with(['posts' => fn($q) => $q->withDeleted()])
    ->all();

// Load users with only their deleted posts
$users = User::find()
    ->with(['posts' => fn($q) => $q->onlyDeleted()])
    ->all();
```

### JOIN-based loading with `joinWith()`

[](#join-based-loading-with-joinwith)

Both models' soft-delete scopes are applied **automatically**. The condition for each model is placed in the JOIN's **ON clause** (not WHERE), which preserves correct LEFT JOIN semantics: a user with no active posts still appears with NULL post columns rather than disappearing.

```
// LEFT JOIN (default) — all users appear; only active posts are joined
// Generated SQL: ... LEFT JOIN post ON user.id = post.user_id AND post.deleted_at IS NULL
//                    WHERE user.deleted_at IS NULL
$users = User::find()->joinWith('posts')->all();

// INNER JOIN — only users with at least one active post are returned
$users = User::find()->joinWith('posts', false, 'INNER JOIN')->all();
```

Column names are always **table-qualified** (`post.deleted_at`), so joining two tables that both have a soft-delete column never produces an "ambiguous column" SQL error.

The relation callback works the same as with `with()`:

```
// Include deleted posts in the JOIN
$users = User::find()->joinWith(['posts' => fn($q) => $q->withDeleted()])->all();

// JOIN only with deleted posts (e.g. to find users with pending cleanup)
$users = User::find()
    ->joinWith(['posts' => fn($q) => $q->onlyDeleted()], false, 'INNER JOIN')
    ->all();
```

Events
------

[](#events)

All four events receive a `yii\base\ModelEvent`. Setting `$event->isValid = false` in a `before*` handler cancels the operation.

ConstantWhen`SoftDeleteTrait::EVENT_BEFORE_SOFT_DELETE`Before `delete()` writes to the DB`SoftDeleteTrait::EVENT_AFTER_SOFT_DELETE`After `delete()` succeeds`SoftDeleteTrait::EVENT_BEFORE_RESTORE`Before `restore()` writes to the DB`SoftDeleteTrait::EVENT_AFTER_RESTORE`After `restore()` succeeds```
$post->on(Post::EVENT_BEFORE_SOFT_DELETE, function (\yii\base\ModelEvent $event) {
    if (!Yii::$app->user->can('deletePost')) {
        $event->isValid = false; // cancel the soft-delete
    }
});
```

`forceDelete()` fires the standard Yii2 `ActiveRecord::EVENT_BEFORE_DELETE` / `EVENT_AFTER_DELETE` events.

Performance
-----------

[](#performance)

Add an index on the soft-delete column so that the automatic scope does not cause a full-table scan:

```
// In your migration
$this->createIndex('idx_post_deleted_at', 'post', 'deleted_at');

// Boolean column — a partial index (supported by PostgreSQL and SQLite) is even more efficient
$this->createIndex('idx_article_is_deleted', 'article', 'is_deleted');
```

Without the index every `find()`, `deleteAll()`, `updateAll()`, and `restoreAll()` call will scan the whole table once it grows large.

Cascade soft-deletes
--------------------

[](#cascade-soft-deletes)

Soft-deleting a parent record does **not** automatically soft-delete its children. If you need cascading behaviour, implement it in the `afterSoftDelete` hook:

```
class Post extends ActiveRecord
{
    use SoftDeleteTrait;

    public static function tableName(): string { return 'post'; }

    public function afterSoftDelete(): void
    {
        Comment::deleteAll(['post_id' => $this->id]);
    }

    public function afterRestore(): void
    {
        Comment::restoreAll(['post_id' => $this->id]);
    }
}
```

String conditions and auto-scope
--------------------------------

[](#string-conditions-and-auto-scope)

`deleteAll()`, `updateAll()`, and `restoreAll()` automatically add a soft-delete scope unless your `$condition` already references the soft-delete column. This detection works for **array conditions only**. Plain SQL strings are not inspected.

If you pass a raw string that already targets the soft-delete column, wrap it in an array to prevent the scope from being added twice:

```
// ✗ scope is added twice — the string is not inspected
Post::deleteAll("deleted_at IS NULL AND category_id = 5");

// ✓ wrap in an array so the column is detected
Post::deleteAll(['and', ['is', 'deleted_at', null], ['category_id' => 5]]);
```

Running tests
-------------

[](#running-tests)

```
make test
```

Tests run inside Docker (PHP 8.3 + SQLite) with no local setup required.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/38176435?v=4)[Eugene Frost](/maintainers/TuzelKO)[@TuzelKO](https://github.com/TuzelKO)

---

Top Contributors

[![TuzelKO](https://avatars.githubusercontent.com/u/38176435?v=4)](https://github.com/TuzelKO "TuzelKO (3 commits)")

---

Tags

yii2extensionsoftdelete

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tuzelko-yii2-softdelete/health.svg)

```
[![Health](https://phpackages.com/badges/tuzelko-yii2-softdelete/health.svg)](https://phpackages.com/packages/tuzelko-yii2-softdelete)
```

###  Alternatives

[skeeks/cms

SkeekS CMS — control panel and tools based on php framework Yii2

13825.6k46](/packages/skeeks-cms)[dmstr/yii2-cookie-consent

Yii2 Cookie Consent Widget

1452.6k](/packages/dmstr-yii2-cookie-consent)[richardfan1126/yii2-js-register

Yii2 widget to register JS into view

1357.2k7](/packages/richardfan1126-yii2-js-register)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
