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. [Framework](/categories/framework)
4. /
5. tuzelko/yii2-softdelete

ActiveYii2-extension[Framework](/categories/framework)

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

Soft delete extension for Yii2 framework

v1.2.0(1mo ago)5144↓23.1%MITPHPPHP &gt;=8.2CI passing

Since Apr 3Pushed 1mo agoCompare

[ 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 (3)Dependencies (6)Versions (4)Used By (0)

Yii2 Soft-Delete extension
==========================

[](#yii2-soft-delete-extension)

[![Project Status: Active](https://camo.githubusercontent.com/39c688bf243eeb6d3bfc529dcf3cb27443613deb696c8fa9f49bccf1e63e3bef/68747470733a2f2f7777772e7265706f7374617475732e6f72672f6261646765732f6c61746573742f6163746976652e737667)](https://www.repostatus.org/#active)[![Tests](https://github.com/TuzelKO/yii2-softdelete/actions/workflows/tests.yml/badge.svg)](https://github.com/TuzelKO/yii2-softdelete/actions/workflows/tests.yml)[![Latest Version](https://camo.githubusercontent.com/d8449782af71db1e5f7cfc63c3ff3c091c57531553b1f0a73859ec11edf443e0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f74757a656c6b6f2f796969322d736f667464656c657465)](https://packagist.org/packages/tuzelko/yii2-softdelete)[![PHP Version](https://camo.githubusercontent.com/9c3cd6aa33ddbf82e2cefef00090852cee857e3f575320120af83736641709a3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f74757a656c6b6f2f796969322d736f667464656c6574652f706870)](https://packagist.org/packages/tuzelko/yii2-softdelete)[![Total Downloads](https://camo.githubusercontent.com/affa5732dca14f2ba89342d53fb849eeeb1f1efb21013591f1c998518cf9e37f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f74757a656c6b6f2f796969322d736f667464656c657465)](https://packagist.org/packages/tuzelko/yii2-softdelete)[![License](https://camo.githubusercontent.com/11f397583806288cbe478200c9747620e5d4f310d7bff4ae054e95acffc98c61/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f54757a656c4b4f2f796969322d736f667464656c657465)](https://github.com/TuzelKO/yii2-softdelete/blob/main/LICENSE)

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)

- **Four column strategies** — Unix timestamp (`int`), DB-native datetime, boolean flag, or zero-based timestamp (UNIQUE-index friendly)
- **Automatic query scope** — deleted records are invisible by default; opt in with `withDeleted()` / `onlyDeleted()`
- **Instance methods** — `softDelete()`, `hardDelete()`, `restore()`, `isSoftDeleted()`
- **Configurable `delete()`** — routes to `softDelete()` or `hardDelete()` depending on `defaultDeleteMethod()`
- **Bulk methods** — `softDeleteAll()`, `hardDeleteAll()`, `restoreAll()`, `updateAll()` (all scope-aware)
- **Configurable `deleteAll()`** — same routing as `delete()`
- **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.2
- 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``TYPE_TIMESTAMP_INT_ZERO``time()` (Unix timestamp)`0``TYPE_TIMESTAMP_INT_ZERO` keeps the deletion timestamp like `TYPE_TIMESTAMP_INT`, but stores `0` instead of `NULL` for non-deleted rows. Use it when the column must be part of a UNIQUE index: most engines exclude `NULL` from uniqueness checks, so a plain timestamp column lets duplicate "live" rows slip through — `0` does not.

```
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->softDelete();      // 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->hardDelete();      // permanent hard-delete (fires standard Yii2 before/afterDelete events)
```

`delete()` is a routing method — by default it calls `softDelete()`. See [Default delete routing](#default-delete-routing) to change this.

> **Deprecated:** `forceDelete()` is a deprecated alias for `hardDelete()`.

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::softDeleteAll(['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::hardDeleteAll(['is not', 'deleted_at', null]);

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

`deleteAll()` is a routing method — by default it calls `softDeleteAll()`. See [Default delete routing](#default-delete-routing) to change this.

> **Deprecated:** `forceDeleteAll()` is a deprecated alias for `hardDeleteAll()`.

> **Auto-scope behaviour:** `softDeleteAll()`, `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.

Default delete routing
----------------------

[](#default-delete-routing)

`defaultDeleteMethod()` controls what `delete()` and `deleteAll()` do. Override it in your model to change the behaviour:

Constant`delete()` behaviour`deleteAll()` behaviour`DELETE_METHOD_SOFT` *(default)*calls `softDelete()`calls `softDeleteAll()``DELETE_METHOD_HARD`calls `hardDelete()`calls `hardDeleteAll()``DELETE_METHOD_DISABLED`throws `NotSupportedException`throws `NotSupportedException````
class Post extends ActiveRecord
{
    use SoftDeleteTrait;

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

    // Always route delete() to hardDelete()
    public static function defaultDeleteMethod(): int
    {
        return self::DELETE_METHOD_HARD;
    }
}
```

```
class Post extends ActiveRecord
{
    use SoftDeleteTrait;

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

    // Disable delete() entirely — callers must choose softDelete() or hardDelete() explicitly
    public static function defaultDeleteMethod(): int
    {
        return self::DELETE_METHOD_DISABLED;
    }
}
```

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 `softDelete()` writes to the DB`SoftDeleteTrait::EVENT_AFTER_SOFT_DELETE`After `softDelete()` 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
    }
});
```

`hardDelete()` 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()`, `softDeleteAll()`, `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::softDeleteAll(['post_id' => $this->id]);
    }

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

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

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

`softDeleteAll()`, `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::softDeleteAll("deleted_at IS NULL AND category_id = 5");

// ✓ wrap in an array so the column is detected
Post::softDeleteAll(['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

44

—

FairBetter than 90% of packages

Maintenance92

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 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

Every ~28 days

Total

3

Last Release

37d ago

PHP version history (2 changes)v1.0.0PHP &gt;=8.0

v1.2.0PHP &gt;=8.2

### 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 (7 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.8k59](/packages/skeeks-cms)[miloschuman/yii2-highcharts-widget

Highcharts widget for Yii 2 Framework.

1761.6M15](/packages/miloschuman-yii2-highcharts-widget)

PHPackages © 2026

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