Laravelでデリートフラグを使って論理削除を実現する手順

## 概要
Laravelでは論理削除を実現するための```SoftDeletes```トレイトが用意されています。
論理削除の判定に利用される```deleted_at```カラムを別のカラムに変更するのはクラス定数の```DELETED_AT```を変更すればできるのですが判定はそのカラムがnullかどうかに限られます。

今回はDBの構造は変更せず、過去の遺産として残っているデリートフラグを使って論理削除を実現する手順を整理していきます。

## 前提条件
今回対象としているデリートフラグはカラム名を```delete_flag```とし、デフォルトが「0」で、論理削除の場合は「1」に設定されるものとして記述していきます。

## トレイトの作成

まずはデリートフラグによる論理削除用トレイト```DeleteFlagTrait.php```を作成します

### トレイトの継承

```SoftDeletes``` トレイトに用意されたメソッドを流用したいのでトレイト継承しています。

``` php
    // ソフトデリートを継承する
    use \Illuminate\Database\Eloquent\SoftDeletes;
```

### 起動処理を変更

Laravelではモデルに追加したトレイトに対しモデルのbootメソッドで```boot＋トレイト名```メソッドが呼ばれるようになっています。

```SoftDeletes``` トレイトを継承していることよにり```SoftDeletes```も「モデルに追加されたトレイト」として扱われるため```bootSoftDeletes``` メソッドが呼び出されることになります。

```bootSoftDeletes``` メソッドでは起動処理としてグローバルスコープを設定していますが、このスコープは利用しないので呼び出されないようにしておき、代わりに```bootDeleteFlagTrait```を作成してデリートフラグ用のスコープをグローバルスコープとして設定します。

デリートフラグ用スコープについては後述

``` php
    /**
     * ソフトデリートの起動処理
     */
    public static function bootSoftDeletes()
    {
        // デフォルトの処理を無効にするため空のメソッドを記述
    }

    /**
     * 起動処理
     */
    public static function bootDeleteFlagTrait()
    {
        // グローバルスコープを設定する
        static::addGlobalScope(new DeleteFlagScope());
    }
```

### 初期化処理の変更

起動処理と同様、モデルに追加されたトレイトの```initialize＋トレイト名```メソッドが初期化処理として呼び出されます。
```SoftDeletes``` トレイトの```initializeSoftDeletes```メソッドでは```deleted_at```カラムの属性を```DateTime/Carbon```インスタンスにするため```dates```プロパティに追加していますが、デリートフラグは日時を扱うカラムではないのでこの処理は不要になります。
他に初期化処理として追加する必要はないので何も処理されないようにだけしておきます。

```php
    /**
     * ソフトデリートの初期化処理
     */
    public function initializeSoftDeletes()
    {
        // デフォルトの処理を無効にするため空のメソッドを記述
    }
```

### デリートフラグのカラム名を指定する

既存のメソッドを継承しているため```getDeletedAtColumn```メソッドでデリートフラグのカラム名を返すように変更する必要があります。
もしモデルごとにカラム名が違う場合、このメソッドを修正する必要があります。

```php
    /**
     * デリートフラグのカラム名を返す
     *
     * @return string
     */
    public function getDeletedAtColumn()
    {
        return 'delete_flag';
    }
```

### 削除の判定処理を変更

```SoftDeletes``` トレイトでは```deleted_at```カラムがnullかどうかで判定している処理を、```delete_flag```が「1」かどうかで判定する処理に変更する必要があります。

### トレイトのまとめ

<details><summary>以下が作成したトレイトのソースになります</summary><div>

``` php:app/Models/Traits/DeleteFlagTrait.php

<?php
namespace App\Models\Traits;

use App\Models\Scopes\DeleteFlagScope;

trait DeleteFlagTrait
{
    // ソフトデリートを継承する
    use \Illuminate\Database\Eloquent\SoftDeletes;

    /**
     * ソフトデリートの起動処理
     */
    public static function bootSoftDeletes()
    {
        // デフォルトの処理を無効にするため空のメソッドを記述
    }

    /**
     * ソフトデリートの初期化処理
     */
    public function initializeSoftDeletes()
    {
        // デフォルトの処理を無効にするため空のメソッドを記述
    }

    /**
     * 起動処理
     */
    public static function bootDeleteFlagTrait()
    {
        // グローバルスコープを設定する
        static::addGlobalScope(new DeleteFlagScope());
    }

    /**
     * 論理削除処理
     */
    protected function runSoftDelete()
    {
        $query = $this->setKeysForSaveQuery($this->newModelQuery());

        $columns = [$this->getDeletedAtColumn() => 1];

        $this->{$this->getDeletedAtColumn()} = 1;

        $query->update($columns);
    }

    /**
     * 論理削除復帰処理
     */
    public function restore()
    {
        if ($this->fireModelEvent('restoring') === false) {
            return false;
        }

        $this->{$this->getDeletedAtColumn()} = 0;

        $this->exists = true;

        $result = $this->save();

        $this->fireModelEvent('restored', false);

        return $result;
    }

    /**
     * 論理削除されているかどうか確認する
     *
     * @return bool
     */
    public function trashed()
    {
        return (1 == $this->{$this->getDeletedAtColumn()});
    }

    /**
     * デリートフラグのカラム名を返す
     *
     * @return string
     */
    public function getDeletedAtColumn()
    {
        return 'delete_flag';
    }
}
```
</div></details>

## スコープの作成

続けて先ほど設定したデリートフラグ用スコープ```DeleteFlagScope.php```を作成します。

### スコープの継承

こちらも```SoftDeletes``` トレイト用のスコープのメソッドを流用したいので継承しています。

### スコープの設定

デリートフラグが「0（＝削除されていない）」ものだけを抽出するようにスコープを作成します。

```php
    /**
     * デリートフラグがOFFのものだけ取得するスコープ
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where($model->getQualifiedDeletedAtColumn(), 0);
    }
```

### マクロの変更

```SoftDeletes``` トレイト同様、```deleted_at```カラムがnullかどうかで判定している処理を、```delete_flag```が「1」かどうかで判定する処理に変更していきます。


### スコープのまとめ

<details><summary>以下が作成したスコープのソースになります</summary><div>

```php:app\Models\Scopes\DeleteFlagScope.php
<?php
namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class DeleteFlagScope extends SoftDeletingScope
{
    /**
     * デリートフラグがOFFのものだけ取得するスコープ
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where($model->getQualifiedDeletedAtColumn(), 0);
    }

    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);

            return $builder->update([
                $column => 1,
            ]);
        });
    }

    protected function addRestore(Builder $builder)
    {
        $builder->macro('restore', function (Builder $builder) {
            $builder->withTrashed();

            return $builder->update([$builder->getModel()->getDeletedAtColumn() => 0]);
        });
    }

    protected function addWithoutTrashed(Builder $builder)
    {
        $builder->macro('withoutTrashed', function (Builder $builder) {
            $model = $builder->getModel();

            $builder
                ->withoutGlobalScope($this)
                ->where($model->getQualifiedDeletedAtColumn(), 0);

            return $builder;
        });
    }

    protected function addOnlyTrashed(Builder $builder)
    {
        $builder->macro('onlyTrashed', function (Builder $builder) {
            $model = $builder->getModel();

            $builder
                ->withoutGlobalScope($this)
                ->where($model->getQualifiedDeletedAtColumn(), 1);

            return $builder;
        });
    }
}
```

</div></details>

## 参照
https://readouble.com/laravel/6.x/ja/eloquent.html#soft-deleting